Mysql 锁
Mysql 锁
通过锁对共享资源的并发访问,提供数据的完整性和一致性。
1 锁的类型
1.1 共享锁
允许事务读一行数据。
1.2 排他锁
允许事务删除或更新一行数据。

只有共享锁与共享锁直接可以兼容。
1.3 意向锁
支持多粒度锁定,允许事务在行级锁和表级锁同时存在。意向锁将锁定的对象分为多个层次,以便于更细粒度的加锁。
如果需要对页上的记录加X锁,那么分别需要对数据库、表、页上意向锁 IX
,最后对记录加X锁,需要等待粗粒度的锁完成,最后才能加行锁。

- 意向共享锁,事务想获取一张表中某几行的共享锁。
- 意向排他锁,事务想获取一张表中某几行的排他锁。

1.4 锁情况
InnoDB 添加了三张表可以查询当前事务锁的状态。
1.4.1 INNODB_TRX
事务的记录。
1.4.2 INNODB_LOCKS
锁的信息记录。

1.4.3 INNODB_LOCK_WAITS
锁等待的情况。

2 一致性非锁定读
InnoDB 通过多版本控制的方式来读取当前执行时间数据库中的文件。如果读取的行正在执行 DELETE/UPDATE
操作,这时读取操作也不会去等待行上锁的释放,而是读取行的快照数据。

快照数据是该行的之前版本数据,是通过 undo
段来完成,undo
日志用来事务中回滚数据,所以快照数据没有额外的开销。并且读取快照是不需要上锁的。一致性非锁定读时 InnoDB
的默认设置,即读取不会占用和等待表上的锁。
2.1 多版本并发控制 MVCC
MVCC
是一种并发控制的方法。快照数据是当前行数据之前的历史版本,每行记录可能有多个版本,一个行记录可能不止一个快照数据。
在不同的事务隔离级别下,并不是都使用一致性快照读。
在 READ_COMMIT
和 REPEAT_READ
下,使用的是非锁定的一致性读。
READ_COMMIT
下,对于快照数据,总是读取被锁定行的最新的一份快照数据。REPEATABLE_READ
下,对于快照数据总是读取事务开始时的行数据版本。

对于上述流程,READ_COMMIT
下,第7行返回的是空,REPEATABLE_READ
下,返回的记录 id
是1。
3 一致性锁定读
在 RC 和 RR 隔离级别下,通过显示的对读取加锁,可以保证数据的一致性。
InnoDB 提供了两种一致性锁定读。
SELECT * FROM table WHERE * FOR UPDATE;
SELECT * FROM table WHERE * LOCK IN SHARE MODE;
FOR UPDATE
是为读取的行记录加一个 X
锁,其他的事务不能对已锁定的事务加上任何锁。
LOCK IN SHARE MODE
对读取的行记录加一个S锁 ,其他事务可以向被锁定的行加 S
锁,但是如果加X锁就会被锁定。
两条语句必须在事务中,当事务提交时,锁也就释放了。
对于已经执行了一致性锁定读的行,仍然可以用一致性非锁定读读取该行。
4 自增长
自增长插入的分类。

自增长的插入模式。

5 锁的算法
5.1 Record Lock
单个行记录上的锁,会锁住索引记录,如果建表时没有指定任何一个索引,就会使用隐式的主键进行锁定。
5.2 Gap Lock
间隙锁,锁定一个范围,不包括记录本身。
5.3 Next-Key Lock
Gap Lock+Record Lock 锁定一个范围并且锁定记录本身。
对于一个索引有 10, 20两个值,那next-key locking的区间为 (-∞,10], (10, 20], (20, +∞)
。
5.4 锁降级
当查询的索引含有唯一属性时,InnoDB 会对 Next-KeyLock
优化,降级为 Record Lock
,即仅锁住索引本身,而不是范围。
对于表 t
, a
为主键:

5.5 辅助索引锁
对于表 t
, a
为主键索引,b
为辅助索引
表数据如下
a | b |
---|---|
1 | 1 |
3 | 1 |
5 | 3 |
7 | 6 |
10 | 8 |
执行如下操作
时间 | 会话A | 会话B |
---|---|---|
1 | BEGIN | |
2 | SELECT * FROM t where b = 3 FOR UPDATE; | |
3 | BEGIN | |
4 | SELECT * FROM t WHERE a = 5 LOCK IN SHARE MODE | |
5 | INSERT INTO t SELECT 4,2 | |
6 | INSERT INTO t SELECT 6,5 | |
7 | ||
8 | INSERT INTO t SELECT 8,6 | |
9 | INSERT INTO t SELECT 2,0 |
对于回话A
,执行语句2时,会使用 Next-key Lock
加锁,由于有两个索引,需要分别加锁。对于列a
为主键索引,只需要加上 Record Lock 5
,对于辅助索引,需要加上 Next-Key Lock
,锁住的范围是(1, 3]
, 同时InnoDB 还会对辅助索引的下一个键加上 gap lock
,即 (3, 6)
。
所以对于会话B
,第4行由于已对 a=5
这行加X锁,第5,6行由于辅助索引的 Next-Key Lock
也会被阻塞。第8,9行不在两个锁的范围内,所以不会被阻塞可以直接运行。
Gap Lock
可以防止多个事务插入到同一范围内,避免幻读的发生。对于 RC
模式下的事务不会加 Gap Lock
锁(处理唯一性约束)。
5.6 通过 Next-Key Lock 进行唯一性检查
SELECT * FROM table WHERE * LOCK IN SHARE MODE;
# IF NOT FOUND ROW
INSERT INTO table VALUES (*);
如果用户通过所有查询一个值,并对该行加一个 S LOCK
,即使查询的值不存在其锁定的也是一个范围,那么插入的值一定是唯一的。

6 锁问题
通过锁机制可以实现事务的隔离性要求,使事务可以并发的执行。
6.1 脏读
脏数据是指未提交的数据,即一个事务可以读取到另一个事务未提交的数据,违反了数据库的隔离性。在事务隔离级别 Read Uncommited
下,一个事务可以读取到另外一个事务未提交的数据,如果另外一个事务进行了回滚,则会出现问题。
6.2 不可重复读
是指在一个事务内多次读取同一数据集合,在这个事务还没有结束时,另外一个事物也访问该同一数据集合,并做了一些更新操作,第一个数据再次读取数据内容是不一样的。不可重复读读取到了其他事务已提交的数据。
InnoDB 使用 MVCC 解决不可重复读问题
6.3 幻读
在一个事务内相同的 SQL,第二次读取到了其他事务插入的行。
Mysql InnoDB 默认使用RR事务隔离级别,对于快照读使用MVCC解决幻读问题。对于当前读通过 next-key Lock
解决幻读。
6.4 丢失更新
应用程序中可能存在如下:
1. 事务T1读取数据 a = 100 b = 100
2. 事务T2读取数据 a = 100 b = 100
3. 事务T1修改数据 a = 100+10 b = 100-10 // b向a转10
4. 事务T2修改数据 a = 100+20 b = 100-20 // b向a转20
5. 最终结果 a = 120 b = 80
最终的结果与实际的意义不同。每个事务开始读取数据时使用 SELECT ... FOR UPDATE
对相应的行加写锁,这样相当于事务中的步骤串行话,保证逻辑上的正确。
7 阻塞
因为不同锁之间的兼容性关系,一个事务中的锁需要等待另一个事务中的锁释放它锁占用的资源。
InnoDB可以通过配置修改锁超时时间。默认配置下不会回滚超时导致的错误,所以需要由用户判断是否 commit 或者 rollback。
8 死锁
死锁是指两个或者两个以上的事务在事务过程中,因争夺锁资源而造成的一种互相等待的现象。
8.1 超时机制
通过超时机制可以简单的解决死锁问题,但超时过多造成大量事务回滚,影响Mysql性能。
8.2 死锁检测
也可以通过等待图 wait-for graph的方式进行主动的死锁检测:
- 锁的信息链表。
- 事务等待链表。

通过两个表可以得出事务等待图:

从图中可以得出事务t1和t2之间存在相互等待的情况,InnoDB检测到死锁后会选择回滚undo量最小的事务。