# 系列目录

# 参考

# InnoDB 锁

数据库遵循的是两段锁协议,将事务分成两个阶段,加锁阶段和解锁阶段(所以叫两段锁)

  • 加锁阶段:在该阶段可以进行加锁操作。加锁不成功,则事务进入等待状态,直到加锁成功才继续执行。
  • 解锁阶段:当事务释放了一个封锁以后,事务进入解锁阶段,在该阶段只能进行解锁操作不能再进行加锁操作。

这种方式虽然无法避免死锁,但是两段锁协议可以保证事务的并发调度是串行化(串行化很重要,尤其是在数据恢复和备份的时候)的。

# 行锁

InnoDB 实现了两种类型的行级锁:

  • 共享锁 (S):在对任何数据进行读操作之前要申请并获得 S 锁。其它事务可以继续加共享锁,但不能加排它锁;
  • 独占锁 (X):在进行写操作之前要申请并获得 X 锁。其它事务不能再获得任何锁;

# 表锁

InnoDB 支持多粒度锁,因此 S 和 X 锁还可以锁表(如使用 ALTER TABLE 等语句会给表上 X 锁)。

另外还设计了两个意向锁,注意意向锁都是表级的

  • 意向共享锁 (IS):表明事务即将给表中的行设置 S 锁。事务给行加 S 锁前必须获得该表的 IS 锁。
  • 意向排它锁 (IX):表明事务即将给表中的行设置 X 锁。事务给行加 X 锁前必须获得该表的 IX 锁。

综上,MySQL 支持两种行锁和四种表锁。

四种表锁的兼容表如下:

锁类型 X IX S IS
X 冲突 冲突 冲突 冲突
IX 冲突 兼容 冲突 兼容
S 冲突 冲突 兼容 兼容
IS 冲突 兼容 兼容 兼容

总结一下就是:

  • 意向锁之间相互不冲突
  • 互斥锁和所有锁都冲突
  • 共享锁互斥意向锁冲突

# 行锁的算法

MySQL :: MySQL 8.0 Reference Manual :: 15.7.1 InnoDB Locking (opens new window)

  • Record Locks:锁(单条)记录
  • Gap Locks:间隙锁,锁定一个开区间范围,但不包括记录本身。间隙指的是索引键值之间的间隙,如下面例子的 (-∞, 0)(10, 15)
  • Next-Key Locks:锁定一个非开区间范围,包括记录本身

假设表中 id 为主键,且有值:0, 5, 10, 15, 20, 25

主键等值查询且数据存在时,会添加 Record Locks,锁住 10 这个记录:

SELECT * FROM t WHERE id = 10 FOR UPDATE;

主键等值查询且数据不存在时,会添加 Gap Locks,锁住 (10, 15) 的范围,防止幻读:

SELECT * FROM t WHERE id = 11 FOR UPDATE;

主键范围查询时,会添加 Next-Key Locks,锁住 (10, 15] 的范围,防止幻读:

SELECT * FROM t WHERE id >= 10 AND id < 11 FOR UPDATE;

MySQL 官网上有提到锁的是前开后闭区间,但是网上测试不同版本的 MySQL 的表现还不一样,所以就记非开区间吧。

# 实际演示

# 两个事务先后 update 但不提交 -> 后更新的被卡住

picture 1

说明可重复读时进行更新也会加锁

# 两个事务都 lock in share mode,然后一个事务 update -> 该事务被卡住

picture 2

也是说明可重复读时进行更新也会加锁,但是这次加独占锁失败了。

# InnoDB 通过加锁回避幻读

picture 3

如果在可重复读下进行了 SELECT ... FOR UPDATEUPDATEDELETE,InnoDB 会锁住当前最新已提交的数据(而不是当前事务开始前已提交的数据),并在查询、更新、删除过程中使用最新已提交的数据。

代价是,加锁后读的值和加锁前读的值不同,即不可重复读。

-- locking - How MVCC works with Lock in MySql? - Stack Overflow (opens new window)