高并发场景下的库存扣减是很多业务系统需要面对的典型问题,当大量用户同时发起下单请求时,如果没有做好并发控制,很容易出现库存被过度扣减的超卖情况,影响业务的正常开展。mysql提供的悲观锁机制中的select_for_update语法,可以在一定程度上保证库存操作的原子性,避免超卖问题。

超卖问题的产生原因
超卖问题的核心原因是多个并发请求同时读取到相同的库存值,然后各自基于这个旧值进行扣减操作,最终覆盖彼此的更新结果。我们可以通过一个简单的流程来理解:
- 请求A查询当前库存为10,准备扣减1,计算后库存为9
- 请求B同时查询当前库存也为10,准备扣减1,计算后库存为9
- 请求A先将库存更新为9,请求B随后也将库存更新为9
- 最终库存显示为9,但实际已经售出2件商品,出现超卖
select_for_update悲观锁的工作原理
select_for_update是mysql InnoDB引擎提供的行级锁语法,属于悲观锁的一种实现。它的作用是:在执行查询时,对符合条件的行加上排他锁,其他事务如果要对这些行执行update、delete或者也加for update的查询,都会被阻塞,直到当前事务提交或者回滚释放锁。
需要注意的是,select_for_update只有在事务中才会生效,并且查询条件必须命中索引,才能触发行级锁,否则会升级为表级锁,严重影响并发性能。
使用select_for_update防止超卖的实现步骤
1. 准备测试表结构
首先创建库存表,用于存储商品的库存信息:
-- 创建商品库存表 CREATE TABLE `product_stock` ( `id` int(11) NOT NULL AUTO_INCREMENT, `product_id` int(11) NOT NULL COMMENT '商品ID', `stock` int(11) NOT NULL DEFAULT '0' COMMENT '库存数量', PRIMARY KEY (`id`), UNIQUE KEY `idx_product_id` (`product_id`) COMMENT '商品ID唯一索引' ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='商品库存表'; -- 插入测试数据,商品ID为1001的商品库存为10 INSERT INTO `product_stock` (`product_id`, `stock`) VALUES (1001, 10);
2. 核心扣减库存逻辑实现
在事务中使用select_for_update查询库存并加锁,然后判断库存是否充足,再进行扣减操作:
-- 开启事务 START TRANSACTION; -- 查询商品库存并加悲观锁,条件命中idx_product_id索引,触发行级锁 SELECT `stock` FROM `product_stock` WHERE `product_id` = 1001 FOR UPDATE; -- 假设查询到的stock为10,判断库存是否大于等于扣减数量 -- 这里可以在应用层判断,也可以在sql中判断,以下是在应用层判断的逻辑示例 -- 如果库存充足,执行扣减操作 UPDATE `product_stock` SET `stock` = `stock` - 1 WHERE `product_id` = 1001 AND `stock` >= 1; -- 判断更新是否生效,如果受影响行数为1,说明扣减成功 -- 如果受影响行数为0,说明库存不足,回滚事务 -- 提交事务 COMMIT;
3. 应用层代码示例(以Python为例)
结合应用层的代码,完整实现防止超卖的逻辑:
import pymysql
def deduct_stock(product_id, deduct_num):
# 建立数据库连接
conn = pymysql.connect(
host='127.0.0.1',
user='root',
password='123456',
database='test_db',
charset='utf8mb4'
)
cursor = conn.cursor()
try:
# 开启事务
conn.begin()
# 执行加锁查询
cursor.execute("SELECT stock FROM product_stock WHERE product_id = %s FOR UPDATE", (product_id,))
result = cursor.fetchone()
if not result:
raise Exception("商品不存在")
current_stock = result[0]
if current_stock < deduct_num:
raise Exception("库存不足")
# 执行扣减更新
cursor.execute(
"UPDATE product_stock SET stock = stock - %s WHERE product_id = %s AND stock >= %s",
(deduct_num, product_id, deduct_num)
)
# 提交事务
conn.commit()
return True
except Exception as e:
# 异常回滚事务
conn.rollback()
print(f"扣减库存失败:{e}")
return False
finally:
cursor.close()
conn.close()
# 调用示例,扣减商品1001的1个库存
deduct_stock(1001, 1)
使用select_for_update的注意事项
- 必须开启事务,否则for update锁会立即释放,无法起到并发控制作用
- 查询条件必须命中索引,避免行锁升级为表锁,影响整体并发性能
- 锁的持有时间要尽可能短,事务中不要包含无关的业务逻辑,减少阻塞其他请求的时间
- 如果库存扣减后需要创建订单等其他操作,要评估这些操作的耗时,避免长事务
- 该方案适合并发量不是特别高的场景,超高并发场景下可以考虑结合redis等缓存组件做前置库存控制
与其他方案的对比
除了select_for_update悲观锁,还可以使用乐观锁、分布式锁等方案防止超卖,以下是简单的对比:
| 方案 | 实现方式 | 适用场景 | 优缺点 |
|---|---|---|---|
| select_for_update悲观锁 | 事务中加行级排他锁 | 中等并发,数据一致性要求高 | 实现简单,一致性有保障;并发高时阻塞多,性能下降 |
| 乐观锁(版本号/库存条件判断) | 更新时带版本号或库存条件 | 并发较高,冲突概率低 | 无阻塞,性能好;冲突多时会大量重试,影响体验 |
| redis分布式锁 | 基于redis的setnx等实现锁 | 高并发,分布式场景 | 性能好,支持分布式;实现复杂,需要处理锁过期等问题 |
在实际业务中,可以根据自身的并发量级和业务需求选择合适的方案,select_for_update是中小并发场景下实现简单、可靠性高的选择。
mysqlselect_for_update悲观锁超卖原子性修改时间:2026-06-10 13:45:35