• 高并发下竞争的资源


    转一下这个高并发的处理过程,有可能对我们大有益处。

    作者:浩李
    链接:https://www.zhihu.com/question/22988790/answer/70790970

    本篇记录了一个高并发接口的调优过程,从单节点100个并发响应时间5秒以上,到最后单节点100个并发响应时间200ms,完成了既定的调优目标,积累了宝贵的经验。抛砖引玉,供大家探讨。
    • 接口的业务场景,发放优惠券到用户。
    • 接口的开发语言及主要使用的组件如下。
      • 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以上,并且有报错。

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


    第二次调优,调优思想,步骤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对系统性能有影响。

    看一上文章对我们的领会是:

    1.在高并发下,代码的质量问题,设计模式问题,数据库的性能,以及 异步出的消息队列。

    2.性能的提示必然消耗一定的内存空间来弥补。

    3、多线程中的处理机制至关重要。

    优化结果架构。
  • 相关阅读:
    获取服务结合随机算法
    服务发现
    使用第三方库来支持平滑重启
    简易配置中心Confd入手
    8、SQL基础整理(约束)
    7、SQL基础整理(子查询)
    6、SQL基础整理(日期时间数据类型,转换函数)
    5、SQL基础整理(字符串函数)
    4、SQL基础整理(规范函数)
    3、SQL基础整理(分组)
  • 原文地址:https://www.cnblogs.com/huojg-21442/p/7473751.html
Copyright © 2020-2023  润新知