高并发下扣减库存

需求

系统中有商品,商品有库存数量,用户买商品扣减商品的库存。

扣减库存的几种方式

下单扣减

下单后立马扣减。有的人下单后不付款,恶意的人直接把库存下单完,影响商品销售。需要结合安全和反作弊的措施。

比如:

给经常下单不付款的买家进行标识(不扣减付款后再扣减)

某此类目最大购买数量

下单不付款的操作进行限制

付款扣减

付款后再扣减。影响用户体验,用户下单后以为成功了,会出现下单后付不了款。

超卖现象。超卖之后补货。

如果不允许超卖,会出现付款后扣减失败,退款流程,进一步影响用户体验。

预扣库存

用户下单后,扣减库存为其保留一定的时间(如10分钟),超过这个时间后,订单失效,这时候其他用户下单就可以购买,付款后扣减库存生效。

同样会有下单扣减同样的问题,恶意的人直接把库存下单完,影响商品销售。

秒杀场景

秒杀场景一般都是抢到就是赚到,所以成功下单后却不付款的情况比较少,再加上卖家对秒杀商品的库存有严格限制,所以秒杀商品采下单扣减更加合理。

下单扣减比预扣库存以及涉及第三方支付的付款扣减更为简单,所以性能上更占优势。

技术上方案

1
2
3
4
5
6
7
8
9
CREATE TABLE `item` (
`id` int NOT NULL,
`name` varchar(50) DEFAULT NULL COMMENT '商品名称',
`inventory` int DEFAULT NULL COMMENT '库存',
`version` int DEFAULT NULL COMMENT '版本',
`create_time` datetime DEFAULT NULL COMMENT '创建时间',
`update_time` datetime DEFAULT NULL COMMENT '修改时间',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='商品表';

单体架构

方法一

使用synchronized或者lock

1
update item set inventory = inventory-#{数量} where id = #{id}

会出现超卖情况

方法二

1
2
update item set inventory = inventory-#{数量} where id = #{id} and (inventory-#{数量}) >= 0
update item set inventory = inventory-#{数量} where id = #{id} and inventory >= #{数量}

存在ABA问题,允许后台增加库存,后台用户看到剩10个了,购买用户A购买了5个并成功后,后台用户增加10个并成功后,购买用户B购买了5个并成功,这时后台用户看商品数量还是10个。

方法三

1
update item set inventory = inventory-#{数量}, version=version+1 where id = #{id} and inventory >= #{数量} and version = #{version}

不会出现ABA问题,但是会出现失败的情况。

集群架构

单体架构的方法一将失效。单体架构的方法二、方法三还是可以使用。方法二、三本质上是把并发交给mysql, 一般到了集群,mysql的并发也不能承受。

redis扣减

先看redis中是否有该商品的库存,没有就加载进redis,如果有就减库存。

redis处理逻辑的模块为单线程。

setnx 命令的函义为指定的 key 不存在时,为 key 设置指定的值。

1
2
3
SETNX 商品Key 商品库存数量
INCRBY 商品Key 增加库存数量
DECRBY 商品Key 减少库存数量

增加了redis后,就会出现redis与mysql一致性的问题。可以看《如何保证数据库与缓存的数据一致性》文章。

这种增加缓存组件的场景是减库存逻辑非常单一,比如没有复杂的SKU和总库存这种联动关系的情况。

分布式锁

使用分布式锁,增加减少库存数量都加分布式锁。

常见的分布式锁有zookeeper、redis锁。


高并发下扣减库存
http://hanqichuan.com/2023/10/18/系统设计/高并发下扣减库存/
作者
韩启川
发布于
2023年10月18日
许可协议