干货 | 1分钟售票8万张!门票抢票背后的技术思考
作者简介
HongLiang,携程高级技术专家,专注系统性能、稳定性、承载能力和交易质量,在技术架构演进、高并发等领域有丰富的实践经验。
活动页面
二、风险与挑战
在活动初期,系统面临以下四类风险:
流量大,入口流量瞬间增长100倍,远超系统承载能力;
高并发下,服务稳定性降低;
限购错误;
热门门票、热门出行日期扣库存热点;
高并发下系统的挑战
下面我们一起来看下每个问题的影响和解决策略。
2.1 入口流量增长100倍
问题
活动开始时入口流量增长100倍,当前系统无法通过水平扩展解决问题。
请求量监控
目标
提升入口应用吞吐能力,降低下游调用量。
策略
减少依赖
1)去除0元票场景不需要的依赖。例如:优惠、立减;
2)合并重复的 IO(SOA/ Redis/DB),减少一次请求中相同数据的重复访问。
上下文传递对象减少重复IO
提升缓存命中率
这里说的是接口级缓存,数据源依赖的是下游接口,如下图所示:
服务层-接口级缓存-固定过期
接口级缓存一般使用固定过期+懒加载方式来缓存下游接口返回对象或者自定义的DO对象。当一个请求进来,先从缓存中取数据,若命中缓存则返回数据,若没命中则从下游获取数据重新构建缓存,由于是接口级的缓存,一般过期时间设置都比较短,流程如下图:
固定过期+懒加载缓存
这种缓存方案存在击穿和穿透的风险,在高并发场景下缓存击穿和缓存穿透问题会被放大,下面会分别介绍一下这几类常见问题在系统中是如何解决的。
1)缓存击穿
描述:缓存击穿是指数据库中有,缓存中没有。例如:某个 key访问量非常高,属于集中式高并发访问,当这个 key 在失效的瞬间,大量的请求就击穿了缓存,直接请求到下游(接口/数据库),造成下游压力过大。
解决方案:对缓存增加被动刷新机制,在缓存实体对象中增加上一次刷新时间,请求进来后从缓存获取数据返回,后续判断缓存是否满刷新条件,若满足则异步获取数据重新构建缓存,若不满足,本次不更新缓存。通过用户请求异步刷新的方式,续租过期时间,避免缓存固定过期。
例如:商品描述信息,以前缓存过期时间为5min,现在缓存过期时间为24H,被动刷新时间为1min,用户每次请求都返回上一次的缓存,但每1min都会异步构建一次缓存。
2)缓存穿透
3)异常降级
下游是非核心:超时异常写一个短暂的空缓存(例如:30s 过期,10s刷新),防止下游超时,影响上游服务的稳定性。
下游是核心:异常时不更新缓存,下次请求再更新,防止写入空缓存,阻断了核心流程。
缓存模块命中率可视化埋点
缓存使用对比
处理性能提升50%
2.2 高并发下服务稳定性低
问题
数据库线程波动
思考
DB 连接池为什么会被打满?
API为什么会超时?
是DB不稳定影响了API,还是API流量过大影响了DB?
问题分析
1)DB 连接池为什么会被打满?分析三类SQL日志。
Insert 语句过多 – 场景:限购记录提交,将限购表单独拆库隔离后,商品API依然超时(排除)
Update 语句耗时过长 – 场景:扣减库存热点引起(重点排查)
Select 高频查询 – 场景:商品信息查询
为什么缓存会被击穿?
梳理系统架构后发现,由于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进行压测,找出问题点,优化改进;
限流配置:为系统配置安全的、符合业务需求的限流阀值;
应急预案:收集各个域的可能风险点,制作应急处理方案;
监控:活动时观察各项监控指标,如有异常,按预案处理;
复盘:活动后分析日志,监控指标,故障分析,持续改进;
本文阐述了在抢票活动中遇到的四个具有代表性的问题,在优化过程中,不断地思考和落地技术细节,沉淀核心技术,以最终达到让用户预订及入园顺畅,体验良好的目标。
团队招聘信息
我们是携程旅游事业群-旅游研发团队,致力于用技术改变旅游预订体验。
我们研发广泛应用于各核心业务中的微服务,数十万QPS的高并发、基于海量数据的实时计算、数十亿规模的消息数据处理;利用业界领先的大前端技术,打造追求极致体验的用户端产品,跨越App、H5、小程序、PC多端,满足国内和国际用户的旅行需求;通过丰富的数据驱动和前沿的模型构建方法,不断挖掘数据和AI技术潜力,促进业务价值增长。
期待你的加入,目前后端/前端/测开/SRE/数据挖掘等职位均有开放。简历投递邮箱:tech@trip.com,邮件标题:【姓名】-【携程旅游研发】- 【职位】
【推荐阅读】
日均流量200亿,携程高性能全异步网关实践
数据为王,携程国际火车票的Sharding-Sphere之路
后微服务时代,领域驱动设计在携程国际火车票的实践 Reactive模式在Trip.com消息推送平台上的实践
“携程技术”公众号
分享,交流,成长