目录
- 扣减库存需要注意的点
- 方案一: 纯mysql扣减实现
- 原理
- 实现
- 优点
- 缺点
- MYSQL架构升级
- 读写分离
- 再次升级
- 代码实现:
- 方案二:缓存实现扣减
- 方案三:数据库+缓存
- 顺序写的架构
- 扣减流程
- 总结
- 扣减库存的操作节点
- 下单减库存
- 付款减库存
- 预扣减库存
- 防范恶意用户
- 小结
高并发场景下,商品展示页上面的信息,除了库存的其他信息属于静态数据,静态数据是可以缓存的。动态数据只有库存。
电商项目对并发数据处理要求较高。
扣减库存需要注意的点
- 剩余库存要大于等于当前需要扣减的库存,不允许超卖
- 对于同一个商品,用户并发扣减时,要保证并发的一致性
- 保证可用性和性能,性能至少是秒级
- 一次扣减包含多个商品
- 扣减多个商品时,一个不成功则全部不成功,需要回滚
- 下单时必须产生了扣减,退款时才能归还,归还的数量必须加回去,不能丢失
- 下单时的一次扣减,可以多次归还
- 归还时需要保证幂等
方案一: 纯mysql扣减实现
扣减业务完全依赖MYSQL数据库来实现,不依赖中间件或缓存。
原理
- 基于数据库乐观锁方式保证并发扣减的强一致性
- 基于数据库的事务实现批量扣减失败进行回滚
实现

- 流程图
一次完整的流程就是先进行数据校验,做接口开发的时候,要保持一个不信任原则,一切数据都不要相信,都要做校验判断,其次还可以进行库存扣减的前置校验,如果库存只有8个,用户要买10个,此时的数据校验中,可以拦截,减少对数据库的写操作。纯读不会加锁,性能较高。 - 关键sql
update xxx set 库存 = 库存-10 where skuid = 'xxx' and 库存>= 10用户每次扣减的时候,需要传入一个uuid作为流水号,全局唯一:
- 当用户退单时,传回此编号,用来标识属于历史上的哪次扣减
- 进行幂等控制,用户调用扣款接口时,出现超时,不知道成功了没,可以通过此编号进行重试或反查,重试时可通过此标识防重
优点
逻辑简单,开发部署成本低。
缺点
无法支持高并发,单机数据库并发1000,2000压力就非常大了,如果AB两个用户同时购买同一个商品,校验通过,后续购买时,只会有一个人成功,导致另外一个人失败,数据库也就多了一次查询,降低性能
MYSQL架构升级
根据场景分析,读库操作一般是浏览商品时产生,扣减库存是在购买时产生,用户购买请求的业务价值大,要保障写操作。
读写分离

根据二八原则,80%为读流量,主库压力降低了80%,但采用读写分离会导致读取的数据不准确,不过库存本身就在变,短暂差异,业务上可以允许,最终的扣减会保证数据的准确性。
再次升级
初次升级支持并发并不太高,我们可以引入缓存

加缓存reids,高并发,单机redis每秒支持并发可在3,4W
代码实现:
version做控制之类的,其实用不上,我们只需要
update where id and 库存>0.
下单失败了,给你返回执行的行数就是0。
if==0
return 下单失败
else
下单成功
方案二:缓存实现扣减

- 和前面的扣减库存其实一样,这里依赖redis,不依赖数据库。
- redis的hash结构不支持多个key批量操作,我们可采用redis+lua脚本来实现批量扣减单线程。
升级成纯redis实现扣减也会有问题
- redis挂了,如果还没执行到redis扣减挂了,直接返回前端失败; 如果执行到redis扣减后,挂了,接口返回的失败,redis扣减成功了,但是没有触发异步更新逻辑,数据库不会扣减,数据库是准确的,这个时候需要一个对账程序,通过对比redis和数据库库存是否一致,并结合扣减日志表,发现扣减失败了,将数据库库存比redis多的库存加回到redis中。
- redis扣减完成,异步刷新数据库失败了,redis此时是准的,数据库库存是多的,结合扣减日志,将数据库比redis多的库存数据在数据库中进行扣减。
方案三:数据库+缓存
在磁盘写数据时,向文件末尾不断追加写入的性能远大于随机修改。对于传统机械硬盘来说,每次随机更新都需要磁头寻址,向文件末尾追加数据,只需要寻址一次。
对固态硬盘来说,虽然避免了磁头移动,但依然存在寻址过程。对文件的随机更新和数据库表更新比较类似,都存在加锁带来的性能消耗。
顺序写的架构

与纯缓存架构的区别是,写入任务数据库不是异步,而是在扣减的时候同步写入,用的是顺序写,不是update做数据库数量的更改,所以性能更好。
insert任务数据库,只记录操作,不进行真实扣减。
扣减流程

insert是顺序写,将update异步化,所以可以很大提高并发,这样会用到数据库事务来进行redis中的数据修改,所以不会出问题,不会出现少卖的问题。
总结
可以用方案1,但是后期业务量上来了,可以考虑后面用方案2,方案3。
大部分电商目前是基于方案2。
扣减库存的操作节点
扣减库存的节点分为
- 下单减库存
- 付款减库存
- 预扣库存
下单减库存
用户下单了,未必会付款
付款减库存
用户明明购买成功了,却不能付款。
预扣减库存
用户下单后,为用户预留库存,占用数量就是购买的数量,例如预留10分钟,超过10分钟释放用户库存,其他用户继续下单。
用户下单预扣减库存后,在付款时检查是否存在有效的预留库存,如果存在则扣减库存并付款。如果不存在则再次尝试预扣减库存,如果库存不足,则不付款。如果预扣减成功则真正扣减库存并付款。
防范恶意用户
- 经常下单不付款的用户打标签,这些用户特殊处理,不扣减库存等
- 秒杀期间,设置同一个人的最大购买数
- 不付款重复下单的用户进行限制,如果存在未付款的订单,并且是同一商品,提示用户先付款再提交订单
小结
大部分秒杀系统会采用下单减库存的方式。
- 扣减库存时在程序中判断库存是否为负,如果变为负数,回滚事务不再扣减库存
- 数据库设置库存字段为无符号整数,从数据库层面保证无法出现负数。
















