跳至主要內容

Mysql 锁

blacklad大约 7 分钟MysqlMysqlMysql 技术内幕

Mysql 锁

通过锁对共享资源的并发访问,提供数据的完整性和一致性。

1 锁的类型

1.1 共享锁

允许事务读一行数据。

1.2 排他锁

允许事务删除或更新一行数据。

image-20210722224233826
image-20210722224233826

只有共享锁与共享锁直接可以兼容。

1.3 意向锁

支持多粒度锁定,允许事务在行级锁和表级锁同时存在。意向锁将锁定的对象分为多个层次,以便于更细粒度的加锁。

如果需要对页上的记录加X锁,那么分别需要对数据库、表、页上意向锁 IX,最后对记录加X锁,需要等待粗粒度的锁完成,最后才能加行锁。

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

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_COMMITREPEAT_READ 下,使用的是非锁定的一致性读。

  1. READ_COMMIT 下,对于快照数据,总是读取被锁定行的最新的一份快照数据。

  2. 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 为辅助索引

表数据如下

ab
11
31
53
76
108

执行如下操作

时间会话A会话B
1BEGIN
2SELECT * FROM t where b = 3 FOR UPDATE;
3BEGIN
4SELECT * FROM t WHERE a = 5 LOCK IN SHARE MODE
5INSERT INTO t SELECT 4,2
6INSERT INTO t SELECT 6,5
7
8INSERT INTO t SELECT 8,6
9INSERT 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的方式进行主动的死锁检测:

  1. 锁的信息链表。
  2. 事务等待链表。

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

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

上次编辑于:
贡献者: blacklad