Skip to content

MySQL 锁

锁加在索引记录上

并发事务访问同一资源时,锁用来排队,避免写写互相覆盖。InnoDB 的行锁挂在索引记录上,不是挂在物理行号上。WHERE 没有走索引,可能锁住大量记录,看起来就像锁表。

快照读(普通 SELECT)几乎不加行锁;当前读才加记录锁、间隙锁或 Next-Key。不要背「RR = 共享锁拿到事务结束」——那不是 InnoDB 默认一致性读的行为。

隔离级别决定锁的范围和持有时间:RR 的间隙锁更强,用来堵幻读;RC 对间隙更宽松,并发插入更好,幻读风险也更高。

粒度、模式和意向锁

表锁行锁页锁(少见)
开销
加锁速度
死锁基本不会可能
并发

InnoDB 默认行锁。DDL、显式 LOCK TABLES 仍会用表锁。MyISAM 基本是表锁。

按模式:

  • 共享锁 S:读。兼容其他 S,不兼容 X
  • 排他锁 X:写。和其他 S / X 都不兼容

给整张表加 X 锁时,如果要扫每一行有没有行锁,代价过高。InnoDB 用表级意向锁先打标:

  • IS:打算给某些行加 S,先拿表 IS
  • IX:打算给某些行加 X,先拿表 IX

要锁整张表时,看表上有没有冲突的意向锁即可,不必遍历行。

Record、Gap 与 Next-Key

三种行锁:

  • Record Lock:锁住已有索引记录
  • Gap Lock:锁住记录之间的间隙,阻止别人往这个间隙插入
  • Next-Key Lock:Record + 前面的 Gap,RR 当前读的默认形态

插入意向锁让多个事务往不同间隙插入可以并行;往同一被 gap 锁住的间隙插入则等待。

RR 下:

  • 快照读靠 MVCC,不靠间隙锁
  • 当前读靠 Next-Key 减少幻读
  • 等值查询走唯一索引时,会优化成 Record Lock,不必锁间隙

排查用 SHOW ENGINE INNODB STATUSperformance_schema.data_locks。等锁超时由 innodb_lock_wait_timeout 控制,默认 50 秒。

乐观锁和悲观锁

悲观锁假定会发生冲突:SELECT ... FOR UPDATE,或把隔离级别提到 SERIALIZABLE。冲突多、不能接受覆盖写时用。

乐观锁先改,提交时再检测。常见是表上加 version

sql
UPDATE product
SET stock = stock - 1, version = version + 1
WHERE id = 100 AND version = 3;

影响行数为 0,说明被别人改过,业务重试。CAS 是同一思路。读多写少、冲突少时更合适。

死锁:检测后回滚,不是把超时调大

两个以上事务互相等对方持有的锁,就是死锁。InnoDB 用等待图检测,牺牲其中一个(通常 undo 更少的)回滚,错误号 1213。MyISAM 一次性拿齐表锁,几乎没有死锁。

减少死锁:

  • 多表、多行都按固定顺序访问
  • 事务尽量短,不要在事务里做 RPC
  • 要改就直接加 X 锁,不要先 S 再升级成 X
  • 条件走索引,锁范围更准
  • 能接受的话降低隔离级别,减少 gap lock
  • 批量更新先在应用侧按主键排好序

死锁发生后看最新死锁日志,调整 SQL 顺序。把 innodb_lock_wait_timeout 调大只是让会话等得更久,解决不了循环等待。