大家有没有遇到过,库存异常的情况:
- 系统重试,导致库存扣了多次;
- 系统并发,导致库存设置错误;
今天和大家聊一聊库存扣减里的方案设计。
库存微服务一般提供什么接口?
提供库存的查询、扣减、设置等RPC接口:
- 库存查询接口,微服务一般执行:
select num from stock where sid=$sid
- 库存扣减接口,微服务一般执行:
update stock set num=num-$reduce where sid=$sid
- 库存设置接口,微服务一般执行:
update stock set num=$num_new where sid=$sid
库存操作,一般是什么业务场景?
用户下单前,一般会对库存进行查询,有足够的存量才允许扣减:
如上图所示,通过查询接口,得到库存是5。
用户下单时,接着会对库存进行扣减:
如上图所示,购买3单位的商品,通过扣减接口,最终得到库存是2。
简言之,一般是“先查后减”。
库存“先查后减”会遇到什么问题?
系统往往有重试机制,这个重试机制可能实现在系统底层,例如:服务连接池重试,数据库连接池重试,业务代码不可控。
如果通过扣减接口来修改库存,在重试时会导致重复扣减:
如上图所示,try和retry,导致一次扣减执行两次,最终得到一个错误库存。
如何解决“重试导致库存异常”的问题?
这里的根本原因:“reduce”操作是一个非幂等的操作,不能够重复执行,可以升级为“set”操作:
如上图所示,同样是购买3单位的商品,通过set操作,即使有重试机制,也不会得到错误的库存,“set”操作是一个幂等操作。
因此,应该“先查后设”。
库存“先查后设”会遇到什么问题?
并发量很大时,还是可能导致库存异常:
如上图所示,两个并发的操作,查询库存,都得到了库存是5。
接下来多个用户发生了并发的购买动作:
画外音:秒杀类业务特别容易出现。
如上图所示:
- 用户1购买了3个库存,库存要设置为2;
- 用户2购买了2个库存,库存要设置为3;
- 这两个设置库存的接口并发执行,库存会先变成2,再变成3,导致数据不一致(实际卖出了5件商品,但库存只扣减了2,最后一次设置库存会覆盖和掩盖前一次并发操作);
如何解决“并发导致库存异常”的问题?
这里的根本原因:设置操作发生的时候,没有检查库存与查询出来的库存有没有变化,理论上:
- 库存为5时,用户1的库存设置才能成功;
- 库存为5时,用户2的库存设置才能成功;
实际执行的时候:
- 库存为5,用户1的set stock 2确实应该成功;
- 库存变为2了,用户2的set stock 3应该失败掉;
画外音:有条件的成功。
接口实现优化升级,将库存设置接口执行的:
update stock set num=$y where sid=$sid
升级为:
update stock set num=$num_new where sid=$sid
and num=$num_old
画外音:加了一个初始条件比对。
简言之,“先查后设,有条件的设”。
这正是大家常说的“Compare And Set”(CAS),是一种常见的降低读写锁冲突,保证数据一致性的方法。
总结
- “先查后减”,在重试,并发的场景下,容易出现异常;
- “先查后设”,幂等性优化,能够解决重试的问题;
- “先查后设,有条件的射”,CAS优化,能够解决并发的问题;
知其然,知其所以然。
思路比结论更重要。