浅谈Mysql之InnoDB中的锁

浅谈Mysql之InnoDB中的锁

Tans 1,671 2022-04-20

Mysql中的锁

锁通常是为了满足互斥性的才得以诞生,无论是在操作系统,应用程序,或者是在本篇文章所叙的Mysql数据库中都有着许多应用。

Mysql加入锁机制主要是为了满足下面两个要求:

  1. 实现多客户端场景下最大程度地实现数据库的并发访问。
  2. 确保每个用户可以以一致的方式去读取和修改数据。

由于 Mysql 插件式的设计,数据库引擎的内部对锁的支持和实现不尽相同;例如MyISAMMEMORY 只支持表锁,而5.6版本后的默认引擎 InnoDB 不仅支持表锁,也支持行锁。Innodb在市场的占有率越来越高,并且有着支持行锁的优越特性。因此本篇文章着重探讨 InnoDB 中的锁机制。

image-1651419109090

图1.Innodb中锁的分类

1. 锁的分类

从锁的粒度来说,在InnoDB引擎中,主要分为行锁、表锁以及间隙锁。

1.1 行锁

  1. 共享锁(Shared Lock):允许拥有锁的事务读一行数据。
  2. 排他锁(Exclusive Lock):允许拥有锁的事务删除或者更新一行数据。

当一个事务获得了行r的共享锁,那么另外的事务可以立即获得行r的共享锁,因为其他行并没有更改行的内容,称作锁兼容。

当一个事务获得了行r的排他锁,那么就需要等待其他事务释放行rS锁和X锁。

优点:粒度比较小,发生锁冲突的概率比较小, 并发度较高。适用于并发量比较高的场景。

缺点:由于粒度比较小,并且最大程度的支持并发,所以开销比较大。

1.2 表锁

先来谈谈什么是意向锁?意向锁是指当前事务想快速查看要操作的表是否有其他事务正在操作(读独或者写),那么他就是通过意向锁来查看的。

为什么会存在意向锁或者表锁?

因为当我们需要获取到一个表的表锁(当此事务需要更新的数据量太大,加行锁会造成效率的降低,所以需要锁表写入的 情况)如果没有意向锁的支持,需要检查每一行记录是否存在行锁,才能判断此表正在处于什么状态(此时有无被其他事务读写)。而这样会大大减少效率,因此引入意向锁锁可以更快得知表的锁的总体情况。

Innodb存储引擎支持多粒度锁定,这种锁定允许在行级上的锁和表级上的锁同时存在,为了支持在不同力度上进行加锁操作,提供了意向锁。InnoDB为我们提供了两种意向锁:

  1. 意向共享锁(IS):事务想要获得一个表中某几行的共享锁,事务在给一个数据行加共享锁之前首先需要获取该表的IS锁。
  2. 意向排他锁(IX):事务想要获得一个表中某几行的排他锁,事务在给一个数据行加排他锁之前首先需要获得该表的IX锁。

【锁兼容情况】

image-1651419015883

1.3 间隙锁

间隙锁是主要针对的是记录之间的间隙,Gap锁是用来在RR级别下解决当前读的幻读问题的。而快照读可以依靠MVCC来解决。

同时,在RR隔离级别下间隙锁才有效,RC隔离级别下没有间隙锁;
间隙锁和行锁合称next-key lock,每个next-key lock是前开后闭区间;

1.4 锁步骤

  1. 意向锁是InnoDB自动生成的,无需进行干预
  2. 对于修改数据集的操作例如Update、Delete和Insesrt语句,Innodb会自动给涉及到的数据集自动加锁。
  3. 对于普通select,InnoDB不会加任何锁,而是会通过MVCC来进行访问(一致性非锁定读)。这种普通的select也成为快照读
  4. 对应特殊的select:可以显式或隐式地给数据集加上共享锁和排他锁
    1. 共享锁:select * from .... lock in shared mode。为读取的行记录加上S锁
    2. 排他锁:select * from .... for update,为读取的行记录加一个X锁

1.5 乐观锁与悲观锁

乐观锁:认为自己在使用数据的时候不会有其他线程进行修改数据,所以不会添加锁,因此会在更新数据的时候去判断之前有没有其他线程修改了这个数据,如果数据没有被修改,那么本次修改成功;如果数据已经被其他线程修改,那么会根据不同的方式执行不同的操作,比如说报错或者重试。一个主要的实现就是CAS

悲观锁:认为在使用数据的时候一定会有其他线程进行修改数据,因此在获得线程的时候先加上锁,保证数据不会被其他的线程修改。

两者适用场景:

  • 悲观锁适合写操作多的场景,先加锁可以保证写操作数据正确。
  • 乐观锁适合读操作多的场景,不加锁可以使得读操作的性能大大增加。

CAS也有一定的缺点:

  1. ABA问题:CAS需要在操作值的时候检查内存值是否发生变化,当时如果原来内存值为A, 后来变成了B,然后又变成了A,那么可能CAS的的到的结果是值没有发生变化。
  2. 循环时间开销大,如果长时间获取不到某一个值,那么可能会导致一直自旋,给CPU带来很大的开销。

2. 不同的读

2.1 什么是一致性非锁定读

InnoDB数据引擎通过多版本控制(MVCC)的方式来获取当前执行时间数据的行数据。如果该行数据正处在X锁的情况下,该读取操作不会因此而会等待行上的锁释放,而是会读取行的一个快照数据(Undo段)。大大提高了并发性。默认也是这种读的方式。要注意的是,在不同的隔离级别下,InooDB引擎对快照数据的定义也不相同:

  • RC级别下,非一致性读总是读取最新的并且对本事务可见的快照,也就是每次select语句都会生成ReadView
  • RR级别下,非一致性读总是读取此事务开始时的数据版本,只有事务开始时生成一次ReadView

这一部分详细解析可以查看

其中,MVCC依赖于以下三部分:

  • Undo日志链 (Record历史记录)
  • ReadView (包含了当前在该条记录操作的事务链)
  • 表结构的隐藏字段,TRX_ID等

ReadView包括

  • m_low_limit_id:目前出现过的最大的事务 ID+1,即下一个将被分配的事务 ID。大于等于这个 ID 的数据版本均不可见
  • m_up_limit_id:活跃事务列表 m_ids 中最小的事务 ID,如果 m_ids 为空,则 m_up_limit_idm_low_limit_id。小于这个 ID 的数据版本均可见
  • m_ids:Read View 创建时其他未提交的活跃事务 ID 列表。创建 Read View时,将当前未提交事务 ID 记录下来,后续即使它们修改了记录行的值,对于当前事务也是不可见的。m_ids 不包括当前事务自己和已提交的事务(正在内存中)
  • m_creator_trx_id:创建该 Read View 的事务 ID

image-1673761207841

2.2 ‘快照读’与‘当前读’

在RR级别中,通过MVCC机制,虽然让数据变得可重复读,但我们读到的数据可能是历史数据,是不及时的数据,不是数据库当前的数据!,这在一些对于数据的时效特别敏感的业务中,就很可能出问题。对于这种读取历史数据的方式,我们叫它快照读 (snapshot read),而读取数据库当前版本数据的方式,叫当前读 (current read)。很显然,在MVCC中:

  • 快照读:就是select
    • select * from table ….;
  • 当前读:特殊的读操作,插入/更新/删除操作,属于当前读,处理的都是当前的数据,需要加锁。也叫加锁读
    • select * from table where ? lock in share mode;
    • select * from table where ? for update;
    • insert;
    • update ;
    • delete;

上文可以知道,在快照读的情况下,InnoDB给我们提供了MVCC的方式来解决非锁定一致性读,但是对于例如updateinsert当前读,就需要引入其他模块来进行实现了。在RR级别下,为了解决当前读中的幻读问题,Mysql引入了Next-key机制来维护。

3. Next-Key Locks

Next-key锁是GAP锁行锁的结合。

  1. 行锁防止别的事务修改或删除
  2. GAP锁防止别的事务新增

行锁和GAP锁结合形成的的Next-Key锁共同解决了RR级别在写数据时的幻读问题。每个Next-key Lock的区间都是左开右闭的。

例如数据库开启RR隔离级别, 存在一个表 t , 表结构如下:

其中id字段是主键,name字段是非索引字段

id name
1 tan
3 cao
5 ma
10 niu

试着分析一下语句生成的Next-key Lock锁的区间范围:

  1. select * from t for update : 全局表锁, (,1],(1,3],(3,5],(5,10],(10,](-∞, 1], (1, 3], (3, 5], (5, 10], (10, -∞]
  2. select * from t where id = 1 for update : 由于是等值查询、并且是索引列,那么Next-key Lock退化为行锁, 因此锁的区间为 [1,1][1,1]
  3. select * from t where id = 7 for update : 由于是等值索引但是数据不存在,那么就会使用Gap锁来锁住区间, 因此被锁区间为 (5,10](5, 10]
  4. select * from t where id > 5 and id < 10 for update ,由于是区间当前读,锁的区间为 (5,10](5, 10]
  5. select * from t where name = 'li' for update , 由于使用了非索引字段,那么就算没有找到相应的记录,其也会加表锁

4.总结

I. MySQL InnoDB 支持三种行锁定方式:

  • 记录锁(Record Lock) :也被称为记录锁,属于单个行记录上的锁。
  • 间隙锁(Gap Lock) :锁定一个范围,不包括记录本身。
  • 临键锁(Next-key Lock) :Record Lock+Gap Lock,锁定一个范围,包含记录本身。记录锁只能锁住已经存在的记录,为了避免插入新记录,需要依赖间隙锁。

II. InnoDB 的默认隔离级别 REPEATABLE-READ(可重读)是可以解决幻读问题发生的,在RR级别下:

  • 快照读 :由 MVCC 机制来保证不出现幻读。
  • 当前读 : 使用 Next-Key Lock 进行加锁来保证不出现幻读。

5. 参考文档

美团技术团队-InnoDB
Mysql Lock
Mysql是如何解决幻读问题的
深入了解Next-keys
JavaGuide-Mysql锁剖析

6. 其他文章

  1. 分布式锁