SQL 事务与锁机制高级应用

2次阅读

read committed 是多数业务的合理起点,它避免脏读且不默认加间隙锁,而 mysql 的 repeatable read 会因间隙锁导致并发阻塞,postgresql 同名级别实为无锁快照隔离。

SQL 事务与锁机制高级应用

事务隔离级别怎么选才不拖慢查询

不同隔离级别直接影响锁的粒度和持有时间。READ COMMITTED 是多数业务的合理起点,它避免脏读,又不像 REPEATABLE READ 那样在 MySQL 中默认加间隙锁(Gap Lock),导致 UPDATE 范围扩大、并发阻塞变多。PostgreSQL 的 REPEATABLE READ 实际是快照隔离,不加锁,但 MySQL 不是——这点常被忽略。

  • READ UNCOMMITTED 基本不用:读未提交数据可能引发业务逻辑错乱,比如扣款前看到未确认的余额
  • SERIALIZABLE 会把很多 select 变成锁读,高并发下极易出现 Lock wait timeout exceeded
  • 如果只是防止幻读且不想锁太多,用 SELECT ... for UPDATE 显式加锁比盲目升隔离级别更可控

FOR UPDATE 和 LOCK IN SHARE MODE 的实际差异

FOR UPDATE 加的是排他锁(X 锁),阻止其他事务读写;LOCK IN SHARE MODE 加的是共享锁(S 锁),允许其他事务加 S 锁但拒绝 X 锁。关键不是“能不能读”,而是“别人还能不能改”。

  • 在更新前校验库存时,用 SELECT stock FROM items WHERE id = 123 FOR UPDATE,能确保后续 UPDATE 不会覆盖并发修改
  • 若只做只读校验(如判断用户是否已投票),LOCK IN SHARE MODE 可降低冲突概率,但注意:MySQL 8.0+ 对二级索引加 S 锁时,仍可能触发聚簇索引上的隐式锁升级
  • 不带 WHERE 条件的 SELECT ... FOR UPDATE 会锁全表,生产环境务必避免

死锁日志里怎么看谁抢了哪把锁

MySQL 的 SHOW ENGINE INNODB STATUS 输出中,<strong><em> (1) WAITING FOR this LOCK TO BE GRANTED:</em></strong> 下面那行就是当前事务卡住的锁请求; (2) HOLDS THE LOCK(S): 后面列出的是另一个事务持有的锁。

  • 注意锁对象格式:record lock, heap no 123 表示某行记录锁,gap before rec 是间隙锁,supremum pseudo-record 意味着锁住了索引区间上限
  • 死锁通常发生在两个事务以不同顺序访问相同索引行,比如事务 A 先锁 id=10 再锁 id=20,事务 B 反过来——修复方式不是加重试,而是统一 SQL 访问顺序(如总是按 id ASC 排序后处理)
  • innodb_print_all_deadlocks = ON 必须开,否则只记录最近一次,线上排查基本靠猜

长事务为什么让主从延迟飙升

长事务本身不直接导致延迟,但它会让 binlog 事件迟迟不落盘,同时阻塞 purge 线程清理 undo log,进而拖慢从库的 relay log 回放速度。更隐蔽的问题是:它让 MVCC 版本链拉得极长,SELECT 扫描时要跳过大量已删除但未 purge 的版本。

  • information_schema.INNODB_TRX 里查 TRX_STARTEDTRX_ROWS_LOCKED,能快速定位“睡着还占着锁”的事务
  • 应用层设置 wait_timeoutinteractive_timeout 防连接空闲挂起,但更重要的是业务代码里别用一个事务包着 http 请求全过程
  • 使用 SELECT ... FOR UPDATE NOWAIT 可避免无限等待,捕获 Lock wait timeout exceeded 后主动降级或重试,而不是让事务僵死几分钟

事务的复杂点不在语法,而在锁如何随索引结构、隔离级别、执行路径动态变化。同一个 UPDATE 语句,在有无索引、是否走范围条件、是否涉及外键约束时,锁的类型和范围可能完全不同——查执行计划不够,得看 INNODB_STATUS 里的真实锁信息。

text=ZqhQzanResources