本文总结库存领域建设库存预占能力时遇到的问题以及解决方案。感谢京东物流金鹏、孙静、陈瑞同学在本文撰写中提供的内容及帮助!导读
消费者拍下商品订单后,库存系统先为该订单预留库存,这个预留库存的动作被称为库存预占。
多个线程并发对同一个数据库商品数据做库存扣减时,数据库中会加锁来保障数据被正确操作。当商品数据足够【热】时,大量的锁等待会导致性能问题,见下图:
在业务上允许的情况下,减缓预占操作速度,从而降低热点热度,缓解库存系统的性能压力。见下图:
优点:逻辑简单,改造风险较小。从整个调用链路的角度去优化问题,而不是只优化瓶颈点
(1)商品入库时,将数量拆分为N份,放入N个表或者一个表的N行中
假如单条记录支撑的性能是50单/秒,那么拆分成3份以后,支撑的性能就能提升到100+单/秒。见下图:
优点:上游无感知,改造可控制在库存领域;
缺点:
1)逻辑相对复杂,改造风险高;
热点商品预占的耗时主要集中在数据库操作上,使用处理速度更快的redis缓存来替代数据库来提供预占能力,见下图:
优点:上游无感知,改造可控制在库存领域;
方案 | 是否能无损支持业务 | 实现成本 |
| 异步限流 | 否,仅无损支持与下单方异步交互的场景 | 低 |
| 商品库存横向拆分 | 否,会出现有库存但无法预占的情况 | 中 |
| 缓存抗写流量 | 是 | 高 |
橙色部分为优化后的结果:
问题定义:多个线程操作查询、操作同一个商品的库存,使库存数据混乱
解决方案:利用mysql事务、行锁机制来避免线程之间互相影响,在sql语句中操作变化量
b、操作库存。根据库存id扣减库存,set 当前库存=当前库存+操作量。该步骤mysql会在id上加互斥锁,避免不同线程之间的互相影响。这里使用批量更新,来提升一单操作多商品的性能UPDATE stock SET stock_num = stock_num + CASE id WHEN 1 THEN 'value1' WHEN 2 THEN 'value2' WHEN 3 THEN 'value3' ENDWHERE id IN (1,2,3)
c、校验库存。为了防止超卖,根据库存id查询库存,如果订单中任一商品库存被扣减为小于0,则抛出异常,使用数据库事务机制进行回滚
解决方案:将redis操作放入lua脚本中,利用redis单线程执行以及lua脚本执行过程中不会被其他操作语句插入的特性,避免线程间互相影响
1、预占流程间死锁。多个订单预占商品,包含多个相同商品,多线程并发请求时,线程之间持有对方依赖的锁,然后等待对方释放自己依赖的锁。见下图:
注:当前物流库存平台需要进行操作的库存数据可以分为仓库库存、逻辑库存、批次库存。其中逻辑库存、批次库存可以看作对某一个仓库库存进行不同维度的拆分。
锁排序,保持锁的顺序一致。在多个事务请求资源的情况下,要保持锁的请求顺序一致,从而保障线程顺序执行。伪代码如下:public Result handleOccupyRequest(List<CalcOccupyRequest> paramList) { //XX业务逻辑 //Long类型比较器,根据库存id进行排序 Comparator<Long> comparator = new Comparator<Long>() { public int compare(Long o1, Long o2) { return o1.compareTo(o2); } }; //对要操作的各类库存进行排序 if(saleableStockIds!=null){ Collections.sort(saleableStockIds, comparator); } if (otherStockIds!=null){ Collections.sort(otherStockIds, comparator); } //XX业务逻辑}
这个问题又可以拆解为:
1、如何从流程处理机制上保障redis-db之间的数据最终一致性?
库存充足的商品,设定比对时间间隔,在间隔内区间内,只进行一次比对,避免影响系统性能
微信扫一扫
关注该公众号