SQL 事务的 ACID 特性解析

2次阅读

事务需显式开启,否则begin/commit无效;mysql默认autocommit=1,须set autocommit=0或用start transaction包裹;selectfor update锁行取决于索引,无索引可能锁表,范围查询可能锁间隙。

SQL 事务的 ACID 特性解析

事务必须显式开启,否则 BEGINCOMMIT 不起作用

MySQL 默认是自动提交模式(autocommit=1),每条 SQL 执行完立刻生效,BEGIN 后跟的语句其实已经单独提交了。这时候你 ROLLBACK 也没用——因为根本没在事务里。

实操建议:

  • 先查当前模式:SELECT @@autocommit;,返回 1 就得关
  • 临时关闭:SET autocommit = 0;,之后所有语句都在同一事务中,直到 COMMITROLLBACK
  • 更稳妥的做法是始终用 START TRANSACTION(等价于 BEGIN)显式包裹,而不是依赖 autocommit 设置
  • 注意:某些 ORM(如 djangoatomic)会自动管理,但裸写 SQL 时这点极易漏掉

SELECT ... FOR UPDATE 锁的是哪些行?不是“查到的行”那么简单

这个语句加的是当前读 + 行锁,但具体锁住什么,取决于执行计划和索引是否命中。没走索引?可能升级成表锁;范围查询?可能锁住间隙(gap lock),导致其他事务插入被阻塞。

常见错误现象:

  • 事务 A 执行 SELECT * FROM orders WHERE status = 'pending' FOR UPDATE,status 字段无索引 → 锁全表 → 事务 B 插入新订单被卡住
  • 事务 A 查询 WHERE id > 100 AND id ,id 是主键 → 锁住 (100,200) 这个间隙,B 想插 id=150 就失败

实操建议:

  • FOR UPDATE 前,务必 EXPLAIN 看是否走了索引
  • 只锁定真正需要修改的行,避免 SELECT * ... FOR UPDATE,改用带明确主键或唯一索引条件的语句
  • 并发场景下,间隙锁可能引发死锁,要结合业务接受“重试”逻辑

隔离级别不是越高越好:REPEATABLE READ 下幻读仍可能真实发生

MySQL 默认是 REPEATABLE READ,它用 MVCC 解决了快照读下的幻读,但只要你用了 SELECT ... FOR UPDATEUPDATE ... WHERE 这类当前读,幻读就回来了——因为当前读看到的是最新版本,且加锁逻辑会暴露新插入的行。

使用场景差异:

  • 纯读操作(无 FOR UPDATE)+ REPEATABLE READ → 幻读被 MVCC 屏蔽
  • 读-改-写流程(比如先查余额再扣款)+ REPEATABLE READ → 若中间有别人插入符合查询条件的新记录,当前读会看到它,导致逻辑错乱
  • SERIALIZABLE 能彻底禁止幻读,但代价是所有 SELECT 都隐式加共享锁,吞吐暴跌,一般不用

实操建议:

  • 别迷信默认隔离级别,关键业务逻辑要自己验证当前读行为
  • 想规避幻读又不想降级到 SERIALIZABLE?把查询条件尽量收紧到唯一索引上,减少间隙锁影响范围
  • 必要时用 SELECT ... FOR UPDATE 加锁前,先用 SELECT ... LOCK IN SHARE MODE 探路,降低冲突概率

事务超时不是数据库管的,wait_timeoutinnodb_lock_wait_timeout 完全不同

这两个配置常被混为一谈,但作用对象完全不同:wait_timeout 控制空闲连接断开时间,而 innodb_lock_wait_timeout 才决定一个事务等待锁多久后报错 Lock wait timeout exceeded

性能与兼容性影响:

  • 默认 innodb_lock_wait_timeout=50 秒,线上服务等 50 秒才失败太长,容易拖垮调用方
  • 设太小(比如 1 秒)又会导致正常争抢被误判,尤其在批量更新或慢查询期间
  • 这个值不能全局乱调,建议按业务接口粒度控制:长事务走专用连接池并调大,短平快接口保持默认或略降

实操建议:

  • 查当前值:SELECT @@innodb_lock_wait_timeout;
  • 会话级调整:SET innodb_lock_wait_timeout = 10;(仅对当前连接有效)
  • 应用层必须捕获 Deadlock found when trying to get lockLock wait timeout exceeded 两种错误,并实现退避重试

事情说清了就结束。ACID 不是开关,是每个环节都要对齐的契约:隔离级别、锁策略、超时设置、连接生命周期——少对一环,事务就只是看起来可靠。

text=ZqhQzanResources