申请演示
了解详情
400-600-9585
QQ咨询 联系我们
官方微信扫一扫

热点订单库存方案设计

2022/06/20百胜软件

涉及大量的库存扣减,因为订单量、库存量数千万,mysql单表已无法应对,所有系统已经完成了订单、库存数据的水平分表方案,分解了数据库压力。

 

背景
基本情况
电商场景下日千万级订单需要快速流转处理,分解之后每小时需要处理100多万订单,涉及大量的库存扣减,因为订单量、库存量数千万,mysql单表已无法应对,所有系统已经完成了订单、库存数据的水平分表方案,分解了数据库压力。
 
但目前还会遇到爆款SKU库存在极短的时间需要对数据库更新操作,从业务角度这个数据已无法再做分散,必须从技术层面能热点库存更新的方案,同时要保障避免超卖、库存数据的准确性。而热点商品的库存扣减本质上就是热点行更新的能力,高并发的同行更新会造成严重的行锁等待现象,从而导致数据库的threads_running和rt飙升,造成雪崩。在当前的官方mysql中,一般单行更新的QPS在500以内,对于热点商品的秒杀需求,这个量往往是不达标的。
 
在此背景下需要设计一个妥善的方案,既能实现库存的高并发更新,又能保障库存数据库一致性、准确性,保障库存的准确性。并且当数据库、服务异常的情况发生时,系统能尽快恢复;并且当可能因为各种原因发生了库存不一致时,有一套完备的监控机制,能快速发现差异、核对差异、快速修复。
 
核心需求
· 1、同一个仓库下爆款SKU支持高并发扣减库存
· 2、最大努力保障防止超卖(贯穿整个流程)
· 3、库存同步要尽可能及时
 
百胜软件
图源:千图网
 
整体设计
关键难点
· mysql单行update只能支持500 QPS,需要有一个机制来转移规避数据单行热点更新;
· 如果引入其他服务如redis来抗并发,如何保障事务一致性;
· 如果库存数据冗余存储,数据一致性如何保障;
· 结合业务场景,如何最大程度不超卖;
· 如何快速修复数据尽量减少影响(不管是分布式系统可能的各种意外还是数据问题、程序逻辑产生的异常);
· 一套有效的监控机制,能无人值守的自动检测、自动修复,特殊情况预警人工介入,并提供辅助工具;
· 在代码工程细节时如何有效落地这些设计以及各种异常分支场景能充分测试。
 
业界基本思路
目前业界主流做法基本上是引入redis来做库存更新,所有的库存更新和查询都走redis,redis库存更新完成会异步写入DB,再加消息重试及补偿机制保障一致性。这一套机制比较复杂,一致性也会存在延时,特别是延时较高的时候不少库存增量在途中(MQ里)。如果性能需要进一步扩展,需要对库存数据从技术层面做进一步拆分,就更加复杂了。写这方面的文章很多,这里不做详述。还是需要结合具体场景来看,不同的方案应对的业务场景还是有差异,带来的复杂度也是不一样。需要基于业务场景来评估,适合的才是最好的。
 
设计思考
综合来看,目前看到的更多是互联网秒杀架构的一些大致方案,整个运维细节控制比较麻烦,且db和redis差异控制的不好会引发单据超卖、数据异常,运维成本较高。业务系统还是需数据准确、性能够用、运维成本低,所以希望简化此机制,在运维成本、性能中做一个平衡。
 
基本思路是引入redis来解决热点库存更新,同时利用insert性能(8C32G DRDS + 8C32G RDS 大约 1.8w/s)更高的特性来在一个数据库事务内记录“库存更新缓存流水”,且能充分利用数据库事务特点保障数据库库存数据强一致性,这样不存在异步途中的库存,可以直接 库存表+sum(库存更新缓存流水) 来和redis进行核对。很容易快速地进行差异检测。这样的话,整体性能会比纯redis库存性能低一些,但是数据一致性保障更强。
 
这么设计的一个基础前提假设
关系型数据库比redis更可靠,对于业务系统来说数据一致性要高(不因追求暂时不需要的性能指标去降低数据一致性),运维要尽可能简单。
 
下面有几个关键要点:
· 以redis的高可用、高速内存操作来抗高并发更新,以redis的原子特性来保障规避在高并发时能的脏读带来的超卖、错误锁定。
· 为了降低redis、数据库两份库存差异的概率,从设计上有一个热点标记的逻辑,只有标记为热点才会使用redis来抗并发。同时热点库存记录数比全量库存记录小很多,核对上也更加快速。
· redis更新和库存操作在同一个事务内完成,尽可能保障redis和db的一致性。
· 规避超卖的基本原则:扣减库存时先扣减redis、再扣减数据库,增加库存时先增加db库存、再增加redis。
· redis补偿机制(遵循防超卖原则,宁愿先少一点,再快速加回来,也不要超卖):
(1)扣减库存时,如果redis成功,db失败需要事务回滚,同时对redis操作做一次逆向补偿,如果补偿失败异步做自动重试补偿,超过重试上限,自动预警人工介入补偿;
(2)增加库存时,如果db成功,redis失败,认为成功提交事务,然后自动重试redis操作,如果超过重试上限,自动预警通知人工介入处理。
 
主要改造点
· 所有涉及库存查询的地方必须通过库存服务的库存查询接口来(热点的具体可用数是查的redis),不能直接读取数据库。
· 调整现有库存更新逻辑,主要是优化几个关键操作的顺序、事务一致性的细节控制。
· 在同一个事务内完成插入库存更新缓冲流水的逻辑。
· 优化库存同步逻辑,减低超卖概率:热点库存异动触发库存同步的时机是redis库存更新成功时触发。
 
其他关键点
1、强幂等控制(2道防线)
在系统的实际运行过程中,可能因为一些网络故障等其他原因导致在数据库的扣减成功以后并没有成功返回给用户时,用户可能会有重试操作,这时就必须避免库存记录的重复扣减情况。
由于库存更新操作一定是应用的库存单据发起的,首先在库存单据层面就会一层幂等校验,单据本身就会做并发控制,理论上不会出现同一个单据在一个场景状态下冗余操作了库存。
为了进一步防止意外,可以在库存更新服务层面,可以结合库存更新缓冲流水再做一次校验。
 
2、order库、stock库、redis事务如何有效保证一致性
 
性能的扩展性
(1)如果后续热点更新并发更高的时候,可以将单个库存数量进行拆分后实现“库存分桶扣减方案”,但会带来复杂性,并且会产生库存量碎片,需要定时收集合并数量。工程实现细节要求较高。
(2)如果热点TPS进一步提高的话,可以考虑放弃redis和数据库“强一致性”,redis成功及认为成功,异步刷入db。容忍更高延迟的一致性,接受可能引发的一些异常。
 
结合上面两个可能的扩展思路,可以满足未来可预见的并发量级,当然方案越复杂实现就越复杂,出现问题拍错更难,异常时运维成本更高。
 
目前业务需求的数据量级应该还不需要,后续可以根据业务场景、数据量进行方案迭代。
 
核心机制或流程说明
整体流程
 
以库存扣减场景来做具体说明:
 百胜软件
关键:
1、任何一个操作步骤失败,事务回滚结束。
2、操作步骤的顺序结合实际进行优化降低无效资源操作回滚几率、降低超卖:
· 库存扣减场景:先扣减redis库存,再数据库扣减库存 --- 避免数据库成功redis失败后需要数据库回滚;
· 库存增加场景:先增加db库存,再增加redis库存 --- 避免db失败,redis库存被使用导致超卖。
3、如果redis成功,数据库事务失败,记录补偿日志,异步自动重试逆向redis更新操作。
 

数据库库存数据同步到redis

系统初次启动时,加载库存表数据同步到redis。
后续有异常或有差异时可以自动或手动触发同步。
 
redis失败时的操作重试及补偿机制
redis库存更新时的重试消息队列,通过消费重试队列消息来自动重试,重试失败后发送延迟消息触发下次继续重试,可以在消息中记录重试次数信息,超过最大重试次数(比如5次),记录需人工补偿文件日志并写入一张需要人工处理的异常记录表,触发一个通知给运维人员。
 
[库存更新缓冲流水]回归库存表机制
额外记录库存更新缓冲流水的考虑是利用数据库事务保障db库存数据完整性,同时又能保证较高的性能(insert qps比update高很多)。由于整个库存更新缓冲流水是设计目的是缓冲,不应该长期存在,且为了保持性能库存更新缓冲流水表的记录需要控制一个较小的数量级,所以需要一个策略定时将流水回归到库存表上,这个机制有点像JVM的垃圾回收、或者说mysql的binlog恢复数据机制。
 百胜软件
库存异常监控及修复机制
异常发现:
A、定期从库存更新缓冲流水回归库存表时,可以触发校验redis和数据库是否有差异,触发自动修复及预警
B、定期扫描redis与数据库可用数差异diff,有差异时触发自动修复及预警
 
diff = redis - db<库存表+SUM(库存更新流水)]>
 
热点库存差异监控页面
· 在此页面上可以直接查看热点库存在redis和db里的值、差异
· 可配置预警规则(待补充细节)
· 可配置自动修复规则
 
redis热点库存差异监控
 
异常修复:
标准参照:以数据库为准
1、redis比db多(某些SKU库存已分配到订单里需要逆向处理)
· 1.1 如果diff >= redis可用数,直接redis扣减掉
· 1.2 如果diff < redis可用数,先redis扣到0,且订单里某个SKU无法足够库存,需要做逆向处理
2、db比redis多(某些sku库存无法使用,需要更新到redis)
· 2.1 更新redis增加diff数量
 
上面的1.1、2.1步骤可以自动检测、自动修复,1.2 可自动检测、人工修复。
 
触发库存同步流程
redis可用库存量变化时直接触发现有库存同步逻辑。
 
关键数据结构
数据库表结构:
{库存更新缓冲表}方案1:
直接在现有stm_stock_log\stm_stock_lock_log里增加一个状态字段status, 默认0,
对于热点库存更新,直接更新redis,写入库存日志、库存锁定日志时status=1。
 
相当于status=1的库存流水是库存更新缓冲流水。
 
{库存更新缓冲表}方案2:
 
a、实仓库存更新缓冲表 stm_stock_buffer
 

 
可能的唯一校验:uk(owner_id,warehouse_id, area_id, sku_id, billno, qty_type, version),
同一个单内对同一个sku可能会所逆向反复的情况,所以如果希望数据能校验唯一性更严谨,需要写入的时候维护version,后面就可以做唯一性校验。
 
b、数据库:虚仓库存更新缓冲表 stm_vstock_buffer
 
表结构完全同实仓库存更新缓冲表,只是表名不一样。
 
需人工补偿的异常记录表:
百胜软件
error_type:(预定义错误类型,不同错误类型有不同的data、不同的处理逻辑)
STOCK_REDIS_UPDATE - redis库存更新。
redis库存结构
门店仓:
<商品+省, <实仓,可用数>>
<商品+省, <虚仓,可用数>>
<商品+省, <共享虚仓,可用数>>
电商仓:
<商品, <实仓,可用数>>
<商品, <虚仓,可用数>>
<商品, <共享虚仓,可用数>>
 
具体的redis key结构:
 
1、虚仓库存可用数:
类型:Hash
key: VSR_{{sku_id}}
field: {{warehouse_id}}_{{whtype_id}}
value: {{qty}}
 
2、实仓库存可用数
类型:Hash
key: RSR_{{sku_id}}
field: {{warehouse_id}}_{{whtype_id}}
value: {{qty}}
 
现有结构是hash, 已经逻辑是只把正品库存库存放进来且假设只有一个正品库位,简化了逻辑,但如果业务需要有多个正品库存可能会有问题。
 
如果确定后面基本不会动整个,可以继续保持这个简单的结构。
 
关键技术实现说明
 
参考
 
·阿里云RDS MySQL 8.0测试结果( https://help.aliyun.com/document_detail/150351.html?spm=a2c4g.11186623.6.1517.21f05daf2yknX1)
·如何保障mysql和redis之间的数据一致性?(https://zhuanlan.zhihu.com/p/91770135)
·计算TPS,QPS的方式(https://www.cnblogs.com/asea123/p/10572766.html)
·mysql每秒最多能插入多少条数据 ? 死磕性能压测(https://my.oschina.net/u/867417/blog/758690)
·mysql千万级数据库插入速度和读取速度的调整记录。(https://blog.csdn.net/en_joker/article/details/78481007?utm_medium=distribute.pc_relevant.none-task-blog-BlogCommendFromBaidu-8&depth_1-utm_source=distribute.pc_relevant.none-task-blog-BlogCommendFromBaidu-8)