优惠券发放接口调优实战

本篇记录了一个高并发接口的调优过程,从单节点100个并发响应时间5秒以上,到最后单节点100个并发响应时间200ms,完成了既定的调优目标,积累了宝贵的经验。抛砖引玉,供大家探讨。
优化结果架构。
优惠券发放接口调优实战_第1张图片
本篇作者微盟-李浩(微信号: li_able), 欢迎转载,转载请注明作者。联系作者请加微信号。
  • 接口的业务场景,发放优惠券到用户。
  • 接口的开发语言及主要使用的组件如下
    • Java 1.7
    • Sping 及 Sping MVC 3.2.4
    • Mybatis 3.3.0
    • Redis aliyun KV
    • Mysql aliyun 
    • 测试用机器: 阿里云4核16G有云服务器。
  • 接口的详细业务步骤:接口输入参数 商户ID,卡券模板ID,用户手机号,我们把预先生成的未发放卡券,关联到用户,并更改卡券状态。
    1. 查询卡券模板。(已做redis 缓存)
    2. 查询此用户已经领取此卡券数量,如果用户已领卡数量达到领取限制,立即返回出错信息。
    3. 去数据库拿到下一个可以发放的卡券(测试用卡数量1000万张)
    4. 减少此种卡券的库存数量。
    5. 更改发放卡券的状态。 由可发放状态改为不可发放状态。
    6. 建立用户和此卡券的关联。 (因为分表,用户可以拿到不同表中的卡券,方便以用户维度做查询,所以需要此关联)
  • 此接口的直接压测结果是 100个并发访问,响应时间在5s以上,并且有报错。
优惠券发放接口调优实战_第2张图片
优惠券发放接口调优实战_第3张图片

优惠券发放接口调优实战_第4张图片
报错分析:在高并发的场景下,不同线程在步骤3中拿到的可发放卡券有可能相同,步骤5对卡券的写操作,会改变数据库中同一个卡的状态,第一个线程写入成功后,其余的拿到此卡的线程提交时都会报乐观锁异常,从而导致事物失败。
调优第一步,先解决高并发下步骤3拿卡的冲撞问题。想法很简单,在找到一个可用卡后,在redis中查询此卡是否有效,如果卡已经被其他线程发放,就再拿一个卡来查询有效,直到拿到可以发放的卡,然后注册此卡到redis中。
这样做后进行压测,结果是100个并发,响应时间上升到15s,最高有到30s,但是没有报错发生,cpu利用率在40%。
经过分析log,我们压测发出了2300多张卡,但冲撞次数达到了25000多次。详细分析log后,发现有很多数据库中的可用卡,在redis中注册为不可用,导致每个线程都需要趟过一堆redis中的无用卡,才能拿到有效卡。
由测试结果和分析结果得知,此次调优失败。这种方法致命的弱点是,一旦数据库和redis中产生了不一致,会成倍的延长此接口的响应时间。随着不一致卡的增加,会最终导致系统崩溃。

优惠券发放接口调优实战_第5张图片
优惠券发放接口调优实战_第6张图片

第二次调优,调优思想,步骤3一次拿多个卡,随机选其中一张发给用户,减少冲撞的概率。
但这样做后的压测结果并无明显的改善。

第三次优化,优化方案,用一个守护线程,查询出所有可能发放的卡券模板,并为每个模板在redis中创建并维护一个可用卡id的队列,保证这个队列中有足够的卡可以使用。
当每次需要拿可用卡时,步骤3从redis对应卡模板的队列中lpop出一个可以使用的卡。
压测结果,100个并发,响应时间缩短到1.7s,无报错,cpu20%。
压测分析,redis的lpop是个原子操作,从而保证了每个线程都拿到不同的发放卡。也去掉了从1000万卡中查询下一个可用卡的时间,所以响应时间也缩短了。
到此,离公司要求的100个并发200ms还有很大的距离。

第四次优化,数据库字段读写优化。
在读取可用卡,和更新卡状态,都使用了字自动生成的全字段mapper方法,造成不必要的开销,重新写对应的mapper方法,只拿和更新必要的字段。
提交代代码发布后,再次压测, 100个并发,响应时间1.3s,无报错,cpu15%。

第五次优化,提升log leve等级
因为log等级为debug,因此输出了大量的sql log和调试信息。把log level提升到info后再次压测。
100个并发响应时间为1.1s,无报错,cpu15%。

第六次优化,调试到现在这个阶段,组员都认为此结果可以接受,再调下去并无空间,因为已经是三次数据库写入,和数次查询的必要开销时间,加之数据库使用的是机械硬盘,高并发下磁头移动不可避,就打算放弃了。
但是离我们的既定目标还有5倍的差距,为了完成目标,我决定采取分步式架构来完成。 
步骤4和步骤6如果不在方法中同步执行,并不会产生严重的不一致后果,会造成卡券库存和用户卡包里的卡,在高并发场景下,有延时。
这次优化取消了步骤4和步骤6,在方法完成后,把相关的数据发送到一个消息队列中,队列的另外一端构建一组处理器,来处理消息,完成步骤4和步骤6的写入。
迅速修改完代码后,提交重发,再次压测。
100个并发访问接口,响应时间在200ms,cpu70%。

到此,已经完成了CTO提出的性能指标。过程中经历了多次期望到失望的过程。有一次当压测的兄弟发来500时,我们惊喜的一位是500ms,但实际上是500server error。 为了完成既定目标,我们只能硬着头皮往前冲,而不满足于但节点100并发1s的响应结果,拿着此结果去和CTO解释。

通过此次调优,我们得出以下心得。
  • 高并发下竞争的资源,我们使用异步队列分配给每一个线程。
  • 剥离可以异步执行的步骤,使用消息队列,构建分步式系统来提高响应速度。
  • 优化数据库写入和读取字段,有助于提高性能。
  • debug log对系统性能有影响。
以上心得抛砖引玉,供大家讨论学习。

你可能感兴趣的:(设计架构)