侧边栏壁纸
博主头像
再见理想博主等级

只争朝夕,不负韶华

  • 累计撰写 112 篇文章
  • 累计创建 64 个标签
  • 累计收到 4 条评论

目 录CONTENT

文章目录

MySQL MVCC 和锁机制

再见理想
2022-05-26 / 0 评论 / 1 点赞 / 381 阅读 / 4,054 字

一,MVCC(Multiversion Concurrency Control)

1,MVCC 概念与实现

MVCC,即多版本并发控制,在并发读写数据库时,可以做到在读操作时不用阻塞写操作,写操作也不用阻塞读操作,提高了数据库并发读写的性能, 同时还可以解决脏读,幻读,不可重复读等事务隔离问题,但不能解决更新丢失问题。

在内部实现中,InnoDB 通过 undo log 保存每条数据的多个版本,并且能够找回数据历史版本提供给用户读,每个事务读到的数据版本可能是不一样的。在同一个事务中,用户只能看到该事务创建快照之前已经提交的修改和该事务本身做的修改。

当前读与快照读:

快照读(snapshot read):普通的 select 语句;
当前读(current read) :`select … lock in share mode,select … for update,insert,update,delete 语句;

MySQL 的 InnoDB 存储引擎默认事务隔离级别是RR(可重复读),是通过 行级锁+MVCC。而 MCVV 的实现依赖隐藏字段、Read View、Undo log

a、隐藏字段

InnoDB存储引擎在每行数据的后面添加了三个隐藏字段:

事务ID(DB_TRX_ID):表示最近一次对本记录行作修改的事务ID。
回滚指针(DB_ROLL_PTR):回滚指针,指向当前记录行的 undo log 信息,也就是记录行的历史版本。
行号(DB_ROW_ID):随着新行插入而单调递增的行 ID。这个 DB_ROW_ID 跟 MVCC 关系不大。当表没有主键或唯一非空索引时,innodb 就会使用这个行 ID 自动产生聚簇索引。如果表有主键或唯一非空索引,聚簇索引就不会包含这个行 ID 了。

b、Read View 结构(重点)

其实 Read View(读视图),跟快照、snapshot是一个概念。Read View 里面保存了对本事务不可见的其他活跃事务,主要是用来做可见性判断的。

Read View 比较重要的3个字段是low_limit_id,up_limit_id 以及一个数组 trx_ids:

① trx_ids:Read View创建时其他未提交的活跃事务ID列表。意思就是创建Read View时,将当前未提交事务ID记录下来,后续即使它们修改了记录行的值,对于当前事务也是不可见的。

②** low_limit_id:目前出现过的最大的事务ID+1,即下一个将被分配的事务ID**。

③ up_limit_id:活跃事务列表 trx_ids 中最小的事务ID,如果 trx_ids 为空,则 up_limit_id 等于low_limit_id。

一旦一个Read View被创建,这三个参数将不再发生变化,理解这点很重要,其中 low_limit_id 和 up_limit_id 分别是 trx_Ids 数组的上下界。

c、Undo log

Undo log 中存储的是老版本数据,当一个事务需要读取记录行时,如果当前记录行不可见,可以顺着undo log 链找到满足其可见性条件的记录行版本。大多数对数据的变更操作包括 insert/update/delete,在InnoDB里,undo log分为如下两类:

insert undo log : 事务对 insert 新记录时产生的 undo log, 因为不存在正在对这行数据进行读的事务,所以这个日志只在事务回滚时需要, 所以在事务提交后就可以立即丢弃。

update undo log : 事务对记录进行 delete 和 update 操作时产生的 undo log,不仅在事务回滚时需要,快照读也需要,因为可能存在正在对这行数据进行读的事务,只有当数据库所使用的快照中不涉及该日志记录,对应的回滚日志才会被 purge 线程删除。

2,记录行修改的具体流程

  1. 首先当前事务对记录行加排他锁
  2. 然后把改行数据拷贝到 undo log 中,作为旧版本;
  3. 拷贝完毕后,修改该行的数据;
  4. 事务提交,提交前用 CAS 机制判断记录行当前最新修改的事务id 是否发生了变化,如果没变,则修改记录行最新的修改事务id ,也就是 DB_TRX_ID 为当前事务id,并提交,如果变了,说明存在其他事务修改了这个记录行,那么就应该回滚这个事务。也就是当前事务没有生效。

3,记录行查询时的可见性判断算法

在innodb中,创建一个新事务后,执行第一个select语句的时候,innodb会创建一个快照(read view),快照中会保存系统当前不应该被本事务看到的其他活跃事务id列表(即 trx_ids)

当用户在这个事务中要读取某个记录行的时候,innodb 会将该记录行的 DB_TRX_ID 与该 Read View 中的一些变量进行比较,判断是否满足可见性条件。

假设当前事务要读取某一个记录行,该记录行的 DB_TRX_ID(即最新修改该行的事务ID)为 trx_id,Read View 的活跃事务列表 trx_ids 的上下界分别为 low_limit_id 和 up_limit_id.

具体的比较算法如下:

1,如果 trx_id < up_limit_id, 那么表明“最新修改该行的事务”在“当前事务”创建快照之前就提交了,所以该记录行的值对当前事务是可见的。直接标识为可见,返回true;

2,如果 trx_id >= low_limit_id, 那么表明“最新修改该行的事务”在“当前事务”创建快照之后才被创建且修改该行的,所以该记录行的值对当前事务不可见。应该通过回滚指针找到上个记录行版本,判断是否可见。循环往复,直到可见;

3,如果 up_limit_id <= trx_id < low_limit_id, 那就得通过二分查找判断trx_id 是否在trx_ids列表出现过;
① 如果出现过,说明是当前read view 中某个活跃的事务提交了,那当然是不可见的,应该通过回滚指针找到上个记录行版本,判断是否可见,循环往复,直到可见;
② 如果没有出现过,说明这个事务是已经提交了的,标识为可见,返回 true;

4,RR 和 RC 的 Read View 区别

提交读和可重复读都是使用 MVCC 机制来实现的,但是实现过程略微有一些不同,因为这些差别可以达到不同的隔离级别:

①在 innodb 中的 RR 级别, 只有事务在 begin 之后,执行第一条 select 时, 才会创建一个快照(read view),将当前系统中活跃的其他事务记录起来;并且事务结束前都是使用的这个快照,不会重新创建,直到事务结束。

②在 innodb 中的 RC 级别, 事务在 begin 之后,执行每条 select 语句时,都会创建一个快照


二,锁机制

锁(Locking)是数据库在并发访问时保证数据一致性和完整性的主要机制。在 MySQL 中,不同存储引擎使用不同的加锁方式;

我们以 InnoDB 存储引擎为例介绍 MySQL 中的锁机制,其他存储引擎中的锁相对简单一些。

1,表级锁与行级锁

MySQL 中的锁可以按照粒度分为表级锁和行级锁(Innodb默认):

表级锁:

具有开销小、加锁快的特性;
锁定粒度较大,发生锁冲突的概率高,支持的并发度低;

行级锁:

具有开销大,加锁慢的特性;
行级锁的锁定粒度较小,发生锁冲突的概率低,支持的并发度高。

2,共享锁与排他锁

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

共享锁(S):

允许多个事务同时获取这个锁,获得该锁的事务可以读取数据行(读锁)

排他锁(X):

同一时刻只允许一个事务获取到排他锁,获得该锁的事务可以更新或删除数据行(写锁)

共享锁和共享锁可以兼容,排他锁和其它锁都不兼容,也就是说只允许读读并发,读写,写读和写写操作都必须阻塞。

Innodb 中使用了 MVCC,解决了读写、写读操作阻塞的问题,提高了并发性能。

获取共享锁的方式:

select ... for share

#MySQL8.0 后可以使用:
select ... lock in share mode

获取排他锁的方式:

select ... for update

3,意向锁

意向锁是表级锁,主要针对用户多粒度的锁并存的情况,目的是表明事务稍后需要对表中的行使用那种类型的锁(s或x)。

如果要获取表锁,按一般法的方法,首先需要看该表是否已经被其他事务加上了表级锁,然后依次查看该表中的每一行是否已经被其他事务加上了行级锁。这种方式需要遍历整个表中的记录,效率很低。为此,InnoDB 引入了另外一种锁:意向锁(Intention Lock)

意向锁属于表级锁,由 InnoDB 自动添加,不需要用户干预。意向锁也分为共享和排他两种方式:

意向共享锁(IS)

事务在给数据行加行级共享锁之前,自动获取到该表的 IS 锁。

意向排他锁(IX)

事务在给数据行加行级排他锁之前,自动获取到该表的 IX 锁。

意向共享锁和意向排他锁都是表级锁,所以事务给表添加表锁之前先判断这两个锁是否被获取,如果都没有被获取,那么可以添加表锁,省去了遍历所有数据行的开销。
由于对每个数据行加锁是互不干扰的,所以意向共享锁和意向排他锁是可以同时被获取的。

4,行级锁实现

InnoDB 通过给索引上的索引记录加锁的方式实现行级锁。具体来说,InnoDB 实现了三种行锁的算法:

  1. 记录锁(Record Lock)
  2. 间隙锁(Gap Lock)
  3. Next-key 锁(Next-key Lock)

5,记录锁

记录锁(Record Lock)是针对索引记录(index record)的锁定。例如:

SELECT * FROM t WHERE id = 1 FOR UPDATE; 

会阻止其他事务对表 t 中 id = 1 的数据执行插入、更新,以及删除操作。

记录锁永远都是锁定索引记录,锁定非聚集索引会先锁定聚集索引,再锁定非聚簇索引。如果表中没有定义索引,InnoDB 默认为表创建一个隐藏的聚簇索引,并且使用该索引锁定记录。

6,间隙锁

间隙锁(Gap Lock)锁定的是索引记录之间的间隙、不对索引本身上锁。、

根据检索条件向左寻找最靠近检索条件的记录值A,作为左边界,向右寻找最靠近检索条件的记录值B作为右边界,即锁定的间隙为(A,B)

**间隙锁的目的是为了防止幻读其主要通过两个方面实现这个目的:

1,防止间隙内有新数据被插入。
2,防止已存在的数据,更新成间隙内的数

需要注意的是,不同事务可以获取一个间隙上互相冲突的锁。

SELECT * FROM t WHERE c1= 1 FOR UPDATE; 

只会对 id = 1 的索引记录加上记录锁,而不关心其他事务是否会在前面的间隙中插入数据。但是,如果 id 列上没有索引或者创建的是非唯一索引,则该语句会锁定前面的间隙。

InnoDB 间隙锁的唯一目的是阻止其他事务在间隙中插入数据。间隙锁可以共存,一个事务的间隙锁不会阻止另一个事务在同一个间隙上获取间隙锁。共享间隙锁和排他间隙锁之间没有区别,彼此不冲突,它们的作用相同。

7,Next-key 锁

Next-key 锁(Next-key Lock)锁其实是记录锁+间隙锁,即锁定一个范围,并且锁定记录本身。

默认隔离级别(REPEATABLE READ )下,InnoDB 通过 next-key 锁进行查找和索引扫描,用于防止幻读;因为它会锁定范围值,不会导致两次查询结果的数量不同。

如果我们将语句修改为

SELECT * FROM t WHERE c3 between 1 and 10 FOR UPDATE;

通过无索引的字段操作范围值,也会锁定主键的所有范围。这也就是为什么 MySQL 推荐通过索引操作数据,最好是主键。

8,插入意向锁

插入意向锁(Insert Intention Lock)是在插入数据行之前,由 INSERT 操作设置的一种间隙锁。插入意向锁表示一种插入的意图,如果插入到相同间隙中的多个事务没有插入相同位置,则不需要互相等待。

假设存在索引记录 4 和 7。两个事务分别尝试插入 5 和 6,它们在获取行排他锁之前,分别使用插入意向锁来锁定 4 到 7 之间的间隙;但是不会相互阻塞,因为插入的是不同的行。

插入意向锁的作用是为了提高并发插入的性能。间隙锁不允许多个事务同时插入同一个索引间隙,但是插入意向锁允许多个事务同时插入同一个索引间隙内的不同数据值。

9,MySQL 对锁的选择

1、如果更新条件没有走索引,例如执行

update from t1 set v2=0 where v2=5; 

此时会进行全表扫描,扫表的时候,要阻止其他任何的更新操作,所以上升为表锁。

2、如果更新条件为索引字段,但是并非唯一索引(包括主键索引),例如执行

update from t1 set v2=0 where v1=9; 

那么此时更新会使用 Next-Key Lock。使用 Next-Key Lock的原因:

a、首先要保证在符合条件的记录上加上排他锁,会锁定当前非唯一索引和对应的主键索引的值;
b、还要保证锁定的区间不能插入新的数据。

3、如果更新条件为唯一索引,则使用 Record Lock(记录锁)。InnoDB 根据唯一索引,找到相应记录,将主键索引值和唯一索引值加上记录锁。但不使用间隙锁。

1

评论区