一、背景

去年疫情后,为了减速启动游览市场,湖北在全域范畴内发展“与爱同行 惠游湖北”流动——全省所有A级游览景区向全国游客免门票,敞开怀抱欢送全国人民。本文将介绍在这一流动期间,线上预约抢票零碎遇到的外围问题,零碎的革新过程以及施行的一些教训。这是高并发、高可用场景下,晋升零碎稳定性的一次实战优化,心愿能给面对同样问题的同学提供一些借鉴思路。

流动页面

二、危险与挑战

在流动初期,零碎面临以下四类危险:

  • 流量大,入口流量霎时增长100倍,远超零碎承载能力;
  • 高并发下,服务稳定性升高;
  • 限购谬误;
  • 热门门票、热门出行日期扣库存热点;

高并发下零碎的挑战

上面咱们一起来看下每个问题的影响和解决策略。

2.1 入口流量增长100倍

问题

流动开始时入口流量增长100倍,以后零碎无奈通过程度扩大解决问题。

申请量监控

指标

晋升入口利用吞吐能力,升高上游调用量。

策略

缩小依赖

1)去除0元票场景不须要的依赖。例如:优惠、立减;

2)合并反复的 IO(SOA/ Redis/DB),缩小一次申请中雷同数据的反复拜访。

上下文传递对象缩小反复IO

晋升缓存命中率

这里说的是接口级缓存,数据源依赖的是上游接口,如下图所示:

服务层-接口级缓存-固定过期

接口级缓存个别应用固定过期+懒加载形式来缓存上游接口返回对象或者自定义的DO对象。当一个申请进来,先从缓存中取数据,若命中缓存则返回数据,若没命中则从上游获取数据从新构建缓存,因为是接口级的缓存,个别过期工夫设置都比拟短,流程如下图:

固定过期+懒加载缓存

这种缓存计划存在击穿和穿透的危险,在高并发场景下缓存击穿和缓存穿透问题会被放大,上面会别离介绍一下这几类常见问题在零碎中是如何解决的。

1)缓存击穿

形容:缓存击穿是指数据库中有,缓存中没有。例如:某个 key访问量十分高,属于集中式高并发拜访,当这个 key 在生效的霎时,大量的申请就击穿了缓存,间接申请到上游(接口/数据库),造成上游压力过大。

解决方案:对缓存减少被动刷新机制,在缓存实体对象中减少上一次刷新工夫,申请进来后从缓存获取数据返回,后续判断缓存是否满刷新条件,若满足则异步获取数据从新构建缓存,若不满足,本次不更新缓存。通过用户申请异步刷新的形式,续租过期工夫,防止缓存固定过期。

例如:商品形容信息,以前缓存过期工夫为5min,当初缓存过期工夫为24H,被动刷新工夫为1min,用户每次申请都返回上一次的缓存,但每1min都会异步构建一次缓存。

2)缓存穿透

形容:缓存穿透是指数据库和缓存中都没有的数据,当用户一直发动申请,比方获取id不存在的数据,导致缓存无奈命中,造成上游压力过大。

解决方案:当缓存未命中,在上游也没有取到数据时,缓存实体内容为空对象,缓存实体减少穿透状态标识,这类缓存过期工夫设置比拟短,默认30s过期,10s刷新,避免不存在的id重复拜访上游,大部分场景穿透是大量的,然而有些场景刚好相同。例如:某一类规定配置,只有大量商品有,这种状况下咱们对穿透类型的缓存过期工夫和刷新工夫设置同失常的过期和刷新工夫一样,避免上游无数据始终频繁申请。

3)异样降级

当上游出现异常的时候,缓存更新策略如下:

缓存更新:

  • 上游是非核心:超时异样写一个短暂的空缓存(例如:30s 过期,10s刷新),避免上游超时,影响上游服务的稳定性。
  • 上游是外围:异样时不更新缓存,下次申请再更新,避免写入空缓存,阻断了外围流程。

4)缓存模块化治理

将缓存key依照数据源做分类,每一类key对应一个缓存模块名, 每个缓存模块能够动静设置版本号、过期工夫和刷新工夫,并对立埋点与监控。模块化治理后,缓存过期工夫粒度更为粗疏,通过剖析缓存模块命中率监控,能够反推过期和刷新工夫是否正当,最终通过动静调整缓存过期工夫与刷新工夫,让命中率达到最佳。

缓存模块命中率可视化埋点

咱们将以上性能封装为了缓存组件,在应用的时只须要关怀数据拜访实现,既解决了应用缓存自身的一些共性问题,也升高了业务代码与缓存读写的耦合度。

下图为优化前后缓存应用流程比照:

缓存应用比照

成果

通过解决缓存穿透与击穿、异样降级、缓存模块化治理,最终缓存命中率晋升到98%以上,接口性能 (RT) 晋升50% 以上,上下游调用量比例从1 : 3.9 升高为 1 : 1.3,上游接口调用量升高70%。

解决性能晋升50%

2.2 高并发下服务稳定性低

问题

在每天上午8:00抢票流动开始时,DB连接池被打满,线程稳定大,商品服务超时。

数据库线程稳定

思考

  • DB 连接池为什么会被打满?
  • API为什么会超时?
  • 是DB不稳固影响了API,还是API流量过大影响了DB?

问题剖析

1)DB 连接池为什么会被打满?剖析三类SQL日志。

  • Insert 语句过多 – 场景:限购记录提交,将限购表独自拆库隔离后,商品API仍然超时(排除)
  • Update 语句耗时过长 – 场景:扣减库存热点引起(重点排查)
  • Select 高频查问 – 场景:商品信息查问

2)API为什么会超时?

排查日志能够看到,8:00流动开始后,大量热门商品信息查问到DB与Select高频查问统一。

3)是DB不稳固影响了API,还是API流量过大影响了DB?

依据#2初步判断是因为缓存击穿,导致大量流量穿透到DB。

为什么缓存会被击穿?

梳理零碎架构后发现,因为8:00定时可售通过离线Job管制,8:00商品上线引发数据变更,数据变更导致缓存被刷新(先删后增),在缓存生效霎时,服务端流量击穿到DB,导致服务端数据库连接池被打满,也就是上文所说的缓存击穿的景象。

数据拜访层-表级缓存-被动刷新

如下图所示,商品信息变更后被动让缓存过期,用户拜访时从新加载缓存:

数据拜访层缓存刷新架构(旧)- 音讯变更删除缓存Key

指标

为了避免流动时缓存被删除导致缓存击穿,流量穿透到DB,采纳了以下2种策略:

1)避开流动时数据更新导致缓存生效

咱们将商品可售状态拆分商品可见、可售状态。

  • 可见状态:7:00提前上线对外可见,避开顶峰;
  • 可售状态:逻辑判断定时售卖,既解决定时上线批改数据后,导致缓存被刷新的问题,也解决了Job上线后,商品可售状态提早的问题。

逻辑判断定时可售避开顶峰缓存击穿

2)调整缓存刷新策略

原缓存刷新计划(先删后增)存在缓存击穿的危险,所以前面缓存刷新策略调整为笼罩更新,防止缓存生效导致缓存击穿。新缓存刷新架构,通过Canal监听 MySQL binlog 发送的MQ音讯,在生产端聚合后,从新构建缓存。

数据拜访层缓存刷新架构(新)- 音讯变更从新构建缓存

成果

服务(RT)失常,QPS晋升至21w。

下面两类问题与具体业务无关,上面咱们介绍一下两个业务痛点:

  • 如何避免歹意购买(限购)
  • 如何避免库存少买/超买(扣库存)

2.3 限购

什么是限购?

限购就是限度购买,规定购买的数量,往往是一些特价和提价的产品,为了避免歹意抢购所采取的一种商业伎俩。

限购规定(多达几十种组合)例如:

1)同一出行日期同一景区每张身份证只能预订1张;

2)7天内(预订日期)某地区只能预约3个景区且最多限购20份;

3)流动期间,预约超过5次,没有去玩耍noshow限购;

问题

扣库存失败,限购勾销胜利(理论数据不统一),再次预订被限购了。

起因

限购提交是Redis和DB双写操作,Redis是同步写,DB是线程池异步写,当申请量过大时,线程队列会呈现积压,最终导致Redis写胜利,DB延时写入。在提交限购记录胜利,扣库存失败后,须要执行勾销限购记录。

如下图所示:

限购查看-提交限购-勾销限购

在高并发的场景下,提交限购记录在线程池队列中呈现积压,Redis写入胜利后,DB并未写入实现,此时勾销限购Redis删除胜利,DB删除未查到记录,最终提交限购记录后被写入,再次预订时,又被限购。

如下图:

线程队列积压,先提交的“提交限购”申请晚于“勾销限购”

指标

服务稳固,限购精确。

策略

确保勾销限购操作Redis/DB最终统一。

因为提交限购记录可能会呈现积压,勾销限购时提交限购记录还未写入,导致勾销限购时未能删除对应的提交记录。咱们通过提早音讯弥补重试,确保勾销限购操作(Redis/DB)最终统一。在勾销限购的时候,删除限购记录影响行数为0时,发送MQ提早音讯,在Consumer端生产音讯,重试勾销限购,并通过埋点与监控检测外围指标是否有异样。

如下图所示:

下单-提交限购与勾销限购

成果

限购精确,没有误拦挡投诉。

2.4 扣减库存

问题

  • 商品后盾显示1w已售完,理论卖出5000,导致库存未售完。
  • MySQL呈现热点行级别锁,影响扣减性能。

起因

  • 扣库存与库存明细SQL不在一个事务外面,大量扣减时容易呈现局部失败的状况,导致库存记录和明细不统一的状况。
  • 热门景点热门出行日期被集中预订,导致MySQL呈现扣减库存热点。

指标

库存扣减精确,晋升解决能力。

策略

1)将扣减库存记录和扣减明细放在一个事务外面,保证数据一致性。

DB事务扣减库存

成果

长处:数据统一。

毛病:热点资源,热门日期,扣减库存行级锁工夫变长,接口RT变长,解决能力降落。

2)应用分布式缓存,在分布式缓存中预减库存,缩小数据库拜访。

秒杀商品异步扣减,打消DB峰值,非秒杀走失常流程。

商品上线的时候将库存写入Redis,在流动扣减库存时,应用incrby原子扣减胜利后将扣减音讯MQ收回,在Consumer端生产音讯执行DB扣减库存,若下单失败,执行还库存操作,也是先操作Redis,再发MQ,在Consumer端,执行DB还库存,如果未查问到扣减记录(可能扣库存MQ有提早),则延时重试,并通过埋点与监控检测外围指标是否有异样。

异步扣减库存

成果

  • 服务RT安稳,数据库IO安稳
  • Redis 扣减有热点迹象

3)缓存热点分桶扣减库存

当单个Key流量达到Redis单实例承载能力时,须要对单key做拆分,解决单实例热点问题。因为热点门票热门日期产生热点Key问题,察看监控后发现并不是特地重大,长期采纳拆分Redis集群,缩小单实例流量,缓解热点问题,所以缓存热点分桶扣减库存本次暂未实现,这里简略形容一下过后探讨的思路。

如下图所示:

缓存热点分桶扣减

分桶分库存:

秒杀开始前提前锁定库存批改,并执行分桶策略,依照库存Id取模分为N个桶, 每个分桶对应缓存的Key为Key [0~ N-1],每个分桶保留m个库存初始化到Redis,秒杀时依据 Hash(Uid)%N 路由到不同的桶进行扣减,解决所有流量拜访单个Key对单个Redis实例造成压力。

桶缩容:

失常状况下,热门流动每个桶中的库存通过几轮扣减都会扣减为0。

非凡场景下,可能存在每个桶只剩下个位数库存,预订时候份数大于残余库存,导致扣减不胜利。例如:分桶数量为100个,每个桶有1~2个库存,用户预订3份时扣减失败。当库存小于十位数时,缩容桶的数量,避免用户看到有库存,扣减始终失败。

优化前后比照

扣减库存计划比照

三、回顾总结

回顾“与爱同行 惠游湖北”整个流动,咱们整体是这样备战的:

  • 梳理危险点:包含零碎架构、外围流程,辨认进去后制订应答策略;
  • 流量预估:依据票量、历史PV、节假日峰值预估流动峰值QPS;
  • 全链路压测:对系统进行全链路压测,对峰值 QPS进行压测,找出问题点,优化改良;
  • 限流配置:为系统配置平安的、合乎业务需要的限流阀值;
  • 应急预案:收集各个域的可能危险点,制作应急解决计划;
  • 监控:流动时察看各项监控指标,如有异样,按预案解决;
  • 复盘:流动后剖析日志,监控指标,故障剖析,继续改良;

本文论述了在抢票流动中遇到的四个具备代表性的问题,在优化过程中,一直地思考和落地技术细节,积淀核心技术,以最终达到让用户预订及入园顺畅,体验良好的指标。