信息发布→ 登录 注册 退出

mysql中使用事务控制解决并发修改问题

发布时间:2026-01-12

点击量:
MySQL默认REPEATABLE READ无法避免更新丢失,需用SELECT ... FOR UPDATE加行锁确保库存扣减等并发操作安全,且须关闭autocommit、统一加锁顺序、捕获死锁重试。

事务的隔离级别直接影响并发修改行为

MySQL 默认的 REPEATABLE READ 隔离级别不能完全避免“更新丢失”——两个事务读取同一行后各自修改并提交,后提交的会覆盖前一个的修改结果。这不是 bug,而是该级别下不加锁读(快照读)导致的逻辑冲突。

真正能阻止并发覆盖的,是让读操作带上写锁,即使用 SELECT ... FOR UPDATESELECT ... LOCK IN SHARE MODE。它们只在当前事务持有锁期间生效,且仅对索引列起作用(无索引会锁全表)。

  • 必须确保 WHERE 条件命中索引,否则升级为表级锁,严重拖慢并发性能
  • FOR UPDATE 是排他锁,其他事务无法读或写该行;LOCK IN SHARE MODE 允许其他事务加共享锁,但不允许加排他锁
  • 锁会在事务结束(COMMITROLLBACK)时自动释放,不能手动解锁

典型场景:库存扣减必须用事务+行锁

电商下单扣库存是最常见的并发修改问题。如果只用 UPDATE product SET stock = stock - 1 WHERE id = 123 AND stock >= 1,看似原子,但多个请求同时执行时仍可能超卖——因为 WHERE 判断和赋值不是同一个原子锁操作。

正确做法是先显式加锁再判断再更新:

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

  • 所有涉及多行更新的事务,必须约定统一的加锁顺序(例如按主键 ID 升序)
  • 尽量减少事务内操作步骤,避免在事务中调用外部服务或做耗时计算
  • 捕获 Deadlock found when trying to get lock 错误,在应用层做有限重试(如 3 次),不要无限循环

autocommit 关闭是前提,但别忘了显式控制

MySQL 客户端默认开启 autocommit=1,此时每个语句都是独立事务,SELECT ... FOR UPDATE 加的锁在语句结束就释放,起不到保护作用。

必须先执行:

SET autocommit = 0;
START TRANSACTION;

然后才执行带锁查询和后续更新。漏掉 START TRANSACTION 或忘记 COMMIT,会导致连接长期持有锁、阻塞其他事务,甚至引发连接池耗尽。

最容易被忽略的是:ORM 框架(如 Django、SQLAlchemy)通常封装了事务控制,但若手动拼 SQL 并用原生连接执行,必须自己管理 autocommit 和事务边界。

标签:# bug  # 这不是  # 会在  # 多个  # 升序  # 都是  # 的是  # 重试  # 应用层  # 加锁  # 死锁  # mysql  # 并发  # 循环  # select  # 封装  # for  # sql  # 有锁  # 一加  # django  # go  
在线客服
服务热线

服务热线

4008888355

微信咨询
二维码
返回顶部
×二维码

截屏,微信识别二维码

打开微信

微信号已复制,请打开微信添加咨询详情!