在库存管理相关的业务开发中,扣减库存是最核心的操作之一,需要同时保证库存充足、扣减后库存不为负数,还要避免高并发下的超卖问题。传统的先查询再更新的方式存在事务间隙,很容易出现数据不一致的情况,而使用SQL嵌套查询可以在同一条SQL语句中完成库存校验和扣减,同时配合行锁机制保证操作的原子性。

基础库存扣减的嵌套查询实现
最基础的库存扣减场景是校验商品库存是否大于等于需要扣减的数量,满足则扣减对应数量。我们可以通过嵌套子查询先锁定符合条件的库存行,再执行更新操作。
假设我们有如下的库存表inventory,表结构如下:
| 字段名 | 类型 | 说明 |
|---|---|---|
| id | bigint | 库存记录ID,主键 |
| product_id | bigint | 商品ID |
| stock_num | int | 当前库存数量 |
| version | int | 版本号,用于乐观锁 |
以下是使用嵌套查询实现基础库存扣减的SQL语句,假设要扣减商品ID为1001的库存,扣减数量为5:
-- 嵌套子查询先锁定符合条件的库存行,再执行扣减
UPDATE inventory
SET stock_num = stock_num - 5
WHERE id = (
SELECT id
FROM (
-- 子查询先查询库存充足的记录,同时加行锁(InnoDB引擎下FOR UPDATE会锁行)
SELECT id
FROM inventory
WHERE product_id = 1001
AND stock_num >= 5
FOR UPDATE
) AS tmp
);
这里需要注意,MySQL不允许直接在UPDATE语句的WHERE子句中引用同一个表的子查询,因此我们需要再嵌套一层子查询,将内层查询结果作为临时表,避免语法错误。同时FOR UPDATE会对查询到的库存行加排他锁,直到当前事务提交才会释放,防止其他事务同时修改该行数据。
带版本控制的库存扣减实现
如果业务中已经使用了乐观锁机制,我们也可以结合嵌套查询和版本号实现库存扣减,避免长事务持有行锁影响性能。
-- 结合版本号的嵌套查询扣减逻辑
UPDATE inventory
SET stock_num = stock_num - 3,
version = version + 1
WHERE id = (
SELECT id
FROM (
SELECT id, version
FROM inventory
WHERE product_id = 1002
AND stock_num >= 3
) AS tmp
)
AND version = (
SELECT version
FROM (
SELECT version
FROM inventory
WHERE product_id = 1002
AND stock_num >= 3
) AS tmp2
);
这条SQL会先通过子查询获取目标库存行的版本号,更新时同时校验版本号是否和查询时一致,如果一致则更新成功,版本号加1;如果其他事务已经修改了该行数据,版本号会变化,当前更新就会失败,业务层可以捕获失败后进行重试。
多商品批量库存扣减实现
在订单包含多个商品的场景下,需要批量扣减多个商品的库存,同样可以使用嵌套查询实现,保证所有扣减操作要么全部成功,要么全部失败。
-- 批量扣减库存的嵌套查询实现,假设需要扣减商品1001数量2,商品1002数量3
UPDATE inventory
SET stock_num = CASE product_id
WHEN 1001 THEN stock_num - 2
WHEN 1002 THEN stock_num - 3
END
WHERE product_id IN (1001, 1002)
AND id IN (
SELECT id
FROM (
-- 子查询校验所有商品的库存是否充足
SELECT product_id, id
FROM inventory
WHERE (product_id = 1001 AND stock_num >= 2)
OR (product_id = 1002 AND stock_num >= 3)
FOR UPDATE
) AS tmp
);
这条SQL通过CASE表达式对不同商品执行不同的扣减数量,子查询中同时校验两个商品的库存是否都满足要求,并且加行锁,只要有任意一个商品库存不足,整个更新操作就不会生效,保证批量扣减的一致性。
注意事项
- 嵌套查询中的子查询如果涉及
FOR UPDATE,需要确保查询使用的是主键或者唯一索引,否则可能会升级为表锁,影响并发性能。 - 不同的数据库对嵌套查询的语法支持略有差异,比如PostgreSQL不需要额外嵌套一层临时表,可以直接在UPDATE的WHERE子句中使用子查询,实际使用时需要根据数据库类型调整语法。
- 如果扣减后库存可能为0,需要额外校验
stock_num - 扣减数量 >= 0,避免出现负库存的情况。 - 高并发场景下,行锁的持有时间不宜过长,尽量将库存扣减的事务逻辑写简单,减少事务执行时间。
使用SQL嵌套查询实现库存扣减,核心是把校验和更新操作合并到同一条SQL中,减少事务间隙,同时配合锁机制或者版本号保证数据一致性,相比业务层的先查后更逻辑,能更高效地避免超卖问题。