记活动中心性能坑
** 今天是立夏,有点燥热的天气,备上一杯清茶,来细细分析一下。 **

最近接手一个新的项目,项目的人员辞职的辞职,甩锅的甩锅。甚是无奈,然而这个项目第一期的开发实现功能在五月初就要上线。既然锅甩到我们这边,还是得勇敢地接锅。
活动的业务逻辑是这样的:用户做任务获取抽奖资格(抽奖机会),根据抽奖机会就行抽奖。
截图如下:


出现的问题
由于昨天资讯的推送,推送量很大每秒推送2000个用户,总共有用户1000w左右,一时间同时参加活动的人数就马上上去了。每个接口的相应时间都超过平时,有些比较多操作的接口都直接超时。用户很多吐槽的。问题有:
- ** 接口响应过慢 **(主要有用户任务列表接口,最新中奖用户接口,抽奖接口)
- ** 抽奖接口问题 ** ,抽奖接口相应时间超过5秒,导致前端直接置为抽奖初始状态,让用户在前面点击抽奖接口没返回又点击了一次抽奖,这时上一个接口没返回,导致两次点击用的是同一个抽奖机会,结果第二个请求校验抽奖机会已经被第一个用了,第二个请求直接返回给用户(“抽奖券状态不对”),然而这是前端是抽奖初始状态(3个图都是手机pro6),用户还以为自己中奖,所以导致大部分用户投诉。
- ** 数据库压力过大和慢SQL **后面查一下几台jetty的负载情况,发现几台jetty的负载情况正常,然后马上叫DBA看一下数据库的,果然数据库的连接数从300彪到1K+,网络流量也是急速上涨,然后DBA看了一下慢SQL,在user_award这张表产生了大量的查询慢sql。


- ** 没有索引 **慢sql的原因主要还是因为,user_award表访问得很多,而且TM的居然没有索引。
现阶段活动中心的架构
现在的架构是前面一台LVS做负载均衡,3台nginx做反向代理,6台jetty(一台nginx对应两台jetty),还有一台nginx+jetty作为灰度。数据库的结构是读写都在主库并且做了高可用,缓存用的是redis,一主一从。 架构上体现的问题有:
- 读写都在同一个库,如果产生查询慢SQL的话,就会同时影响到写操作和读操作。(这次由于user_award表没有加索引,这个表有大量的读操作,所以产生了慢SQL,导致数据库堵塞,数据库连接数疯狂飙升到1K+)
业务场景
这也是一个** 读多写少 **业务场景,读写比例大概为8:2,也是十分适合大量用缓存的。核心矛盾是写操作,主要涉及到的有用户做任务这一块,以及抽奖这一块,出现的问题有:
- 用户相关的数据没有用缓存,比如用户做任务的列表数据,中奖名单没有做缓存,用户积分信息没有做缓存,老虎机奖品布局接口。
由于这些接口没有做缓存处理,导致直接影响到我们的这个项目的核心矛盾:抽奖接口。 下面谈谈抽奖接口的业务逻辑:
- 检查用户是否有抽奖机会(查询用户任务完成的状态)
- 检查用户是否中奖达到阀值。
- 更新用户抽奖券状态为已经使用。
- 抽奖:拿出抽奖的奖品按照权重以及随机数进行抽奖
- 如果抽中奖品的话,会检查奖品的库存,如果库存不够就不会中奖,如果库存够机会数据库记录一条记录。
- 更新用户该任务为已经抽奖状态。
我们可以看到抽奖接口的读操作以及写操作都比较多,所以抽奖接口是这所有接口使用时间最长的接口,开发的时候我也知道这个接口肯定会有大量的并发请求。所以我加一个5秒抽奖只能抽奖一次的限制,就是同一个用户5秒之内只能请求一次。
我的做法是用户每次抽奖将用户的ID作为key记录到redis里面,5秒之后过期,如果redis查询到说明用户5秒被已经抽过奖所以不能再抽一次,要等到redis里面的key过期才能抽奖。
然而由于上述的** 数据库架构 , 表没加索引 , 用户相关数据没有大量使用缓存 **导致出现性能的问题。
处理方法
✔实时问题的处理
- 由于这次是由于大量推送,导致参加的用户过多,所以当时马上停止用户消息推送。
- 马上定位问题所在,通过查看服务器的负载情况,发现服务器负载正常,然后赶紧联系DBA查看DB的负载情况,定位到慢SQL,赶紧加上索引。
- 运营安抚用户的情绪。
✔后期的优化
- ** 数据库架构的调整 :由于业务场景读多写少,采用 一主多从 **的数据库结构比较适合。
- ** 数据表的水平拆分 **:用户数据表进行分库或者分表。
- ** 利用好缓存 ** : 用户的任务列表可以存到redis,用户奖品信息也可以存到redis,用户抽奖券也可以存到redis,抽奖的相关信息也可以存到redis,不同的数据设置好不同的过期时间,数据更新时先淘汰缓存再写数据库,避免脏数据的产生。
- ** 静态资源CDN **:H5这些静态资源放在CDN上面。
- ** 大并发的控制 :用户调用抽奖接口应有限制,前端:比如转盘老虎机之类的抽奖,可以控制同个10秒之内只能请求一次(因为转盘老虎机这些转动都需要时间);后端:为了防止别人刷接口,也要加上10秒的访问控制,用redis过期策略来做就行,当发现接口比较慢,可以直接返回不中奖以减少数据库的压力,这样对用户的影响也是很小的, 想方设法在大并发接口减少数据库的操作 **同时应该保障接口的基本业务逻辑不受影响,平衡好这两点。