update where stock >= 1通过mysql行级更新原子性避免超卖:先加行锁、再读值、判断条件、执行更新,全程不可中断;必须同时满足主键条件和库存判断,且执行后检查affected_rows是否为1。

UPDATE WHERE stock >= 1 是怎么避免超卖的
它靠的是 MySQL InnoDB 的行级更新原子性:整个 UPDATE 语句执行时,会先定位到目标行、加行锁(X 锁)、读取当前值、判断 WHERE 条件、执行更新、释放锁 —— 这个过程不可中断。只要 WHERE 中包含库存判断逻辑,就天然把“读→判→改”三步压进一次原子操作里。
常见错误是只写 UPDATE goods SET stock = stock - 1 WHERE id = 123,漏掉 stock >= 1 条件。这样即使库存为 0,语句仍会执行(变成 -1),且影响行数返回 1,应用层误判成功。
-
WHERE必须同时包含主键(或唯一索引)条件和库存判断,例如WHERE id = 123 AND stock >= 1 - 执行后必须检查
affected_rows:等于 1 表示扣减成功;等于 0 表示库存不足或记录不存在 - 不要在应用层再查一次库存做二次判断——这又退回了非原子的老路
为什么不用 SELECT FOR UPDATE + UPDATE 分两步
分两步看似清晰,但实际引入了锁持有时间长、死锁风险、事务膨胀等问题。尤其当业务逻辑稍复杂(比如查完库存还要查用户等级、算优惠),SELECT FOR UPDATE 后的等待窗口就变成了性能黑洞。
典型问题现象:SHOW ENGINE INNODB STATUS 显示大量 waiting for table metadata lock 或 lock wait timeout exceeded 错误;数据库连接数缓慢爬升直至耗尽。
-
SELECT FOR UPDATE必须在事务内使用,且后续必须有写操作,否则锁无效 - 锁从
SELECT开始就持有了,直到COMMIT才释放——中间任何延迟(如网络抖动、日志打印、远程调用)都会拖慢整个队列 - 若多个事务按不同顺序加锁(如事务 A 先锁商品 1 再锁商品 2,事务 B 反之),极易触发死锁
stock 字段必须加 UNSIGNED 和 CHECK 约束吗
不是“必须”,但不加就是留了个隐患口子。仅靠 WHERE stock >= 1 能防大部分并发超卖,但拦不住绕过 SQL 层的直接写入、SQL 注入、或开发误用 SET stock = -100 这类硬编码。
MySQL 8.0.16+ 支持 CHECK (stock >= 0),配合 UNSIGNED INT 类型,能确保任何非法更新在语句执行阶段就被拒绝,报错 Check constraint 'goods_chk_1' is violated。
-
UNSIGNED防止负数插入,但不能阻止UPDATE ... SET stock = stock - 1 WHERE id = X在 stock=0 时变成 4294967295(溢出回卷)——所以仍需WHERE条件兜底 -
CHECK约束在 INSERT/UPDATE 时实时校验,比应用层判断更可靠,且不增加运行时开销 - 低版本 MySQL(
高并发下 UPDATE WHERE 还会超卖吗
在纯 MySQL 单实例、无其他绕过手段的前提下,不会。它的原子性由 InnoDB 引擎保证,不是应用层模拟出来的。
但真实系统中容易被忽略的点在于:事务隔离级别、批量扣减、以及“库存”定义是否统一。比如一个请求要扣 3 件,却只写 stock >= 1,那库存剩 2 时也会成功扣成 -1。
- 扣多件时,
WHERE条件必须匹配实际需求,例如WHERE id = 123 AND stock >= 3 - 事务隔离级别建议用默认的
REPEATABLE READ,避免READ COMMITTED下幻读干扰判断 - 如果库存字段被多个服务共用(如订单、售后、采购都改它),约束和条件必须全局对齐,否则某一方绕过就失效
真正难的不是写出那条 UPDATE,而是让所有路径都走同一条原子路径,且没人偷偷开了后门。











