存储过程必须显式控制事务,开头用start transaction,结尾配对commit或rollback;扣减库存须用update where原子校验并检查row_count();stock字段需unsigned;返回明确结果码而非依赖异常。

存储过程里必须显式控制事务
MySQL 默认 autocommit=1,但存储过程内部执行多条 SQL 时,不加 BEGIN ... COMMIT 就等于每条语句各自提交——扣减库存成功、生成订单失败,就会出现“库存没了但单没建”的数据不一致。所以第一步永远是手动开启事务。
- 开头写
START TRANSACTION或BEGIN(注意不是BEGIN ... END的存储过程块起始) - 结尾必须配对
COMMIT;出错时用ROLLBACK,不能依赖客户端回滚 - 别在存储过程里调用
SET autocommit = 0—— 它只影响当前会话,且容易被后续连接复用污染
扣减逻辑必须用 UPDATE WHERE 做原子校验
先 SELECT stock 再 UPDATE 是经典竞态陷阱。两个并发请求同时读到 stock=10,都执行 UPDATE SET stock = 8,结果变成 6 而不是预期的 4。正确做法是把校验和更新合并成一条语句。
- 用
UPDATE inventory SET stock = stock - 1 WHERE sku_id = 'A001' AND stock >= 1 - 执行后检查
ROW_COUNT():返回 0 表示库存不足或已被扣完,直接ROLLBACK并报错 -
stock字段必须定义为UNSIGNED,否则stock - 1可能变成负数,绕过WHERE stock >= 1校验
需要返回明确结果码而非仅靠异常判断
存储过程调用方(比如 Go 或 Java)没法直接捕获 SQL 层面的 “库存不足” 这类业务错误,因为这不是数据库异常,而是正常执行但影响行为为 0。必须主动暴露状态。
- 声明输出参数:
OUT p_result INT,成功设为 1,库存不足设为 -1,其他错误设为 0 - 不要依赖
SIGNAL SQLSTATE抛自定义异常——很多 ORM(如 GORM)对它的解析不稳定,容易转成泛化错误 - 示例片段:
IF ROW_COUNT() = 0 THEN SET p_result = -1; ROLLBACK; ELSE SET p_result = 1; COMMIT; END IF;
高并发下慎用 SELECT FOR UPDATE
它确实能锁住行,但前提是查询条件走索引。如果 WHERE sku_id = ? 中 sku_id 没建索引,MySQL 会升级为表锁,整个库存表卡死。
- 优先用
UPDATE ... WHERE方案,轻量且足够应对大多数场景 - 真要用
SELECT ... FOR UPDATE,务必确认执行计划中type是ref或const,不是ALL - 事务内不要混用锁模式:在同一个事务里先
SELECT FOR UPDATE,再执行另一条没加锁的UPDATE,可能导致间隙锁范围扩大或死锁
实际写的时候,最易被忽略的是 ROW_COUNT() 的检查时机——它必须紧跟在 UPDATE 之后立刻读取,中间插入任何其他语句(哪怕是 SELECT 1)都会覆盖上一次影响行数。










