MySQL默认REPEATABLE READ无法避免更新丢失,需用SELECT ... FOR UPDATE加行锁确保库存扣减等并发操作安全,且须关闭autocommit、统一加锁顺序、捕获死锁重试。
MySQL 默认的 REPEATABLE READ 隔离级别不能完全避免“更新丢失”——两个事务读取同一行后各自修改并提交,后提交的会覆盖前一个的修改结果。这不是 bug,而是该级别下不加锁读(快照读)导致的逻辑冲突。
真正能阻止并发覆盖的,是让读操作带上写锁,即使用 SELECT ... FOR UPDATE 或 SELECT ... LOCK IN SHARE MODE。它们只在当前事务持有锁期间生效,且仅对索引列起作用(无索引会锁全表)。
WHERE 条件命中索引,否则升级为表级锁,严重拖慢并发性能FOR UPDATE 是排他锁,其他事务无法读或写该行;LOCK IN SHARE MODE 允许其他事务加共享锁,但不允许加排他锁COMMIT 或 ROLLBACK)时自动释放,不能手动解锁电商下单扣库存是最常见的并发修改问题。如果只用 UPDATE product SET stock = stock - 1 W,看似原子,但多个请求同时执行时仍可能超卖——因为 WHERE 判断和赋值不是同一个原子锁操作。
HERE id = 123 AND stock >= 1
正确做法是先显式加锁再判断再更新:
START TRANSACTION; SELECT stock FROM product WHERE id = 123 FOR UPDATE; -- 应用层检查 stock 是否足够 UPDATE product SET stock = stock - 1 WHERE id = 123; COMMIT;
注意:SELECT ... FOR UPDATE 必须在 UPDATE 前执行,且在同一事务中;如果应用层判断失败,记得 ROLLBACK 释放锁。
当事务 A 先锁行 100 再锁行 200,而事务 B 先锁行 200 再锁行 100,就可能触发 MySQL 的死锁检测并回滚其中一个事务。错误日志里会出现 Deadlock found when trying to get lock。
Deadlock found when trying to get lock 错误,在应用层做有限重试(如 3 次),不要无限循环MySQL 客户端默认开启 autocommit=1,此时每个语句都是独立事务,SELECT ... FOR UPDATE 加的锁在语句结束就释放,起不到保护作用。
必须先执行:
SET autocommit = 0; START TRANSACTION;
然后才执行带锁查询和后续更新。漏掉 START TRANSACTION 或忘记 COMMIT,会导致连接长期持有锁、阻塞其他事务,甚至引发连接池耗尽。
最容易被忽略的是:ORM 框架(如 Django、SQLAlchemy)通常封装了事务控制,但若手动拼 SQL 并用原生连接执行,必须自己管理 autocommit 和事务边界。