在批量订单处理场景中,按顺序扣减库存是避免超卖、保障库存分配公平性的关键操作。当多个订单同时请求扣减同一商品的库存时,如果没有合理的顺序控制,可能会出现库存被无序分配,甚至超出实际库存的情况。下面将从实现原理到具体代码逐步说明如何在数据库中完成这一操作。
核心实现思路
按顺序扣减库存的核心目标是:同一商品的库存扣减操作按照指定顺序(比如订单创建时间、订单优先级)执行,且每次扣减前校验剩余库存是否充足,扣减过程具备原子性,避免并发问题。
实现需要依赖三个关键技术点:
- 排序查询:先按照指定顺序获取需要处理的订单列表,保证处理顺序符合业务要求
- 行级锁:对库存记录加行级锁,避免并发场景下多个事务同时修改同一库存数据
- 事务控制:将库存查询、校验、扣减操作放在同一个事务中,保证操作的原子性
数据库表结构设计
首先我们需要两张核心表,分别是商品库存表和订单表,结构如下:
| 表名 | 字段名 | 类型 | 说明 |
|---|---|---|---|
| product_stock | id | bigint | 库存记录ID,主键 |
| product_id | bigint | 商品ID | |
| stock_num | int | 商品剩余库存数量 | |
| order_info | id | bigint | 订单ID,主键 |
| product_id | bigint | 商品ID | |
| order_num | int | 订单购买数量 | |
| create_time | datetime | 订单创建时间 | |
| status | tinyint | 订单状态,0未处理,1已处理 |
具体实现步骤
步骤1:按指定顺序获取待处理订单
首先我们需要按照业务要求的顺序(这里以订单创建时间升序为例)获取待处理的批量订单,避免处理顺序混乱。查询语句如下:
-- 查询未处理的订单,按照创建时间升序排序,保证先创建的订单先处理 SELECT id, product_id, order_num, create_time FROM order_info WHERE status = 0 ORDER BY create_time ASC LIMIT 100; -- 每次处理100条,避免一次性处理过多数据
步骤2:逐单扣减库存并保证顺序和一致性
获取到排序后的订单列表后,我们需要逐单处理,每处理一个订单就执行一次库存扣减操作,且每次扣减都要加行级锁,防止并发问题。这里以MySQL数据库为例,实现代码如下:
-- 开启事务
START TRANSACTION;
-- 定义变量存储当前处理的订单信息
SET @current_order_id = 123; -- 当前处理的订单ID,实际从步骤1的查询结果中获取
SET @current_product_id = 456; -- 当前订单对应的商品ID
SET @current_order_num = 2; -- 当前订单的购买数量
-- 查询并锁定对应商品的库存记录,FOR UPDATE 加行级锁,其他事务无法修改该记录
SELECT stock_num INTO @current_stock
FROM product_stock
WHERE product_id = @current_product_id
FOR UPDATE;
-- 校验库存是否充足
IF @current_stock >= @current_order_num THEN
-- 库存充足,执行扣减
UPDATE product_stock
SET stock_num = stock_num - @current_order_num
WHERE product_id = @current_product_id;
-- 更新订单状态为已处理
UPDATE order_info
SET status = 1
WHERE id = @current_order_id;
-- 提交事务
COMMIT;
SELECT '库存扣减成功' AS result;
ELSE
-- 库存不足,回滚事务
ROLLBACK;
SELECT '库存不足,扣减失败' AS result;
END IF;
步骤3:批量处理的完整逻辑封装
如果是处理批量订单,我们可以将上述逻辑封装为存储过程,自动遍历排序后的订单列表依次处理:
DELIMITER //
CREATE PROCEDURE batch_deduct_stock()
BEGIN
DECLARE done INT DEFAULT 0;
DECLARE v_order_id BIGINT;
DECLARE v_product_id BIGINT;
DECLARE v_order_num INT;
DECLARE v_stock_num INT;
-- 定义游标,获取排序后的待处理订单
DECLARE order_cursor CURSOR FOR
SELECT id, product_id, order_num
FROM order_info
WHERE status = 0
ORDER BY create_time ASC;
DECLARE CONTINUE HANDLER FOR NOT FOUND SET done = 1;
OPEN order_cursor;
read_loop: LOOP
FETCH order_cursor INTO v_order_id, v_product_id, v_order_num;
IF done THEN
LEAVE read_loop;
END IF;
-- 开启事务处理单个订单
START TRANSACTION;
-- 锁定库存记录
SELECT stock_num INTO v_stock_num
FROM product_stock
WHERE product_id = v_product_id
FOR UPDATE;
IF v_stock_num >= v_order_num THEN
UPDATE product_stock
SET stock_num = stock_num - v_order_num
WHERE product_id = v_product_id;
UPDATE order_info
SET status = 1
WHERE id = v_order_id;
COMMIT;
ELSE
ROLLBACK;
END IF;
END LOOP;
CLOSE order_cursor;
END //
DELIMITER ;
-- 调用存储过程处理批量订单
CALL batch_deduct_stock();
注意事项
在实际落地时还需要注意以下几点:
- 行级锁的范围要精准,尽量只锁定需要扣减的库存记录,避免锁表影响性能
- 事务的粒度要控制好,单个订单的扣减操作放在一个事务中即可,不要将整个批量处理放在一个大事务里,否则会导致锁持有时间过长
- 如果库存扣减后需要记录库存变动日志,可以在事务中增加插入日志表的操作,保证日志和扣减操作的原子性
- 对于高并发场景,可以结合消息队列将订单处理异步化,避免数据库压力过大,但依然要保证库存扣减的顺序和一致性
提示:如果使用的是PostgreSQL数据库,行级锁的语法为SELECT ... FOR UPDATE,和MySQL一致,事务控制逻辑也基本相同,只需要调整部分语法细节即可。