高并发场景下库存扣减的核心诉求是保证库存操作的原子性和一致性,避免出现超卖或者库存扣减后未实际下单的问题。SQL存储过程可以将扣减逻辑封装在数据库端执行,减少应用与数据库的交互次数,结合原子更新和悲观锁可以有效应对高并发的库存操作需求。

原子更新方案实现
原子更新的核心思路是利用SQL语句本身的原子性,在更新语句中直接判断库存是否充足,只有库存足够时才执行扣减操作,整个过程由数据库引擎保证原子性,不需要额外的锁机制。
存储过程实现
下面以MySQL为例,编写原子更新方式的库存扣减存储过程:
DELIMITER //
CREATE PROCEDURE deduct_stock_atomic(
IN p_product_id INT, -- 商品ID
IN p_deduct_num INT, -- 扣减数量
OUT p_result INT -- 返回结果:1成功 0库存不足 -1参数错误
)
BEGIN
DECLARE v_current_stock INT DEFAULT 0;
-- 参数校验
IF p_product_id <= 0 OR p_deduct_num <= 0 THEN
SET p_result = -1;
ELSE
-- 原子更新库存,只有库存大于等于扣减数量时才执行
UPDATE product_stock
SET stock_num = stock_num - p_deduct_num
WHERE product_id = p_product_id AND stock_num >= p_deduct_num;
-- 判断更新影响的行数
IF ROW_COUNT() > 0 THEN
SET p_result = 1;
ELSE
SET p_result = 0;
END IF;
END IF;
END //
DELIMITER ;
方案优缺点
- 优点:实现简单,不需要额外的锁操作,性能较高,适合并发量中等、库存扣减逻辑简单的场景。
- 缺点:如果扣减逻辑除了更新库存还需要关联其他表操作,原子更新的方式难以保证多表操作的原子性。
悲观锁方案实现
悲观锁的核心思路是假设并发冲突概率高,在操作库存记录前先对记录加排他锁,其他事务需要等待锁释放才能操作同一条记录,从而保证同一时间只有单个事务处理该商品的库存。
存储过程实现
同样以MySQL为例,使用SELECT ... FOR UPDATE实现悲观锁的库存扣减存储过程:
DELIMITER //
CREATE PROCEDURE deduct_stock_pessimistic(
IN p_product_id INT, -- 商品ID
IN p_deduct_num INT, -- 扣减数量
OUT p_result INT -- 返回结果:1成功 0库存不足 -1参数错误
)
BEGIN
DECLARE v_current_stock INT DEFAULT 0;
-- 参数校验
IF p_product_id <= 0 OR p_deduct_num <= 0 THEN
SET p_result = -1;
ELSE
-- 开启事务,对目标库存记录加排他锁
START TRANSACTION;
-- 查询当前库存并加锁,注意这里要显式指定主键条件触发行锁
SELECT stock_num INTO v_current_stock
FROM product_stock
WHERE product_id = p_product_id FOR UPDATE;
-- 判断库存是否充足
IF v_current_stock >= p_deduct_num THEN
UPDATE product_stock
SET stock_num = stock_num - p_deduct_num
WHERE product_id = p_product_id;
SET p_result = 1;
COMMIT;
ELSE
SET p_result = 0;
ROLLBACK;
END IF;
END IF;
END //
DELIMITER ;
方案优缺点
- 优点:可以保证多步库存相关操作的原子性,适合扣减逻辑复杂、需要关联其他业务表操作的场景。
- 缺点:加锁会增加事务持有时间,高并发下容易出现锁等待甚至死锁问题,性能相对原子更新方案更低。
两种方案的选择建议
如果业务场景只需要更新库存表,没有额外的关联操作,优先选择原子更新方案,性能更好;如果扣减库存的同时需要操作订单表、流水表等多个表,需要保证多表操作的原子性,再选择悲观锁方案。另外需要注意,使用悲观锁时要尽量缩小事务范围,避免长事务导致锁持有时间过长,影响整体并发性能。