MySQL MVCC 和 锁机制

MVCC(Multiversion Concurrency Control)

1、MVCC概念

        多版本控制(Multiversion Concurrency Control): 指的是一种提高并发的技术。最早的数据库系统,只有读读之间可以并发,读写,写读,写写都要阻塞。引入多版本之后,只有写写之间相互阻塞,其他三种操作都可以并行,这样大幅度提高了InnoDB的并发度。在内部实现中,InnoDB通过undo log保存每条数据的多个版本,并且能够找回数据历史版本提供给用户读,每个事务读到的数据版本可能是不一样的。在同一个事务中,用户只能看到该事务创建快照之前已经提交的修改和该事务本身做的修改。
        MVCC只在 Read Committed 和 Repeatable Read两个隔离级别下工作。其他两个隔离级别和MVCC不兼容,Read Uncommitted总是读取最新的记录行,不需要MVCC的支持;Serializable 则会对所有读取的记录行都加锁,单靠MVCC无法完成。
        MySQL的InnoDB存储引擎默认事务隔离级别是RR(可重复读),是通过 "行级锁+MVCC"一起实现的,正常读的时候不加锁,写的时候加锁。而 MCVV 的实现依赖:隐藏字段、Read View、Undo log。

1.1、隐藏字段

        InnoDB存储引擎在每行数据的后面添加了三个隐藏字段:
        1. 6字节的事务ID(DB_TRX_ID):表示最近一次对本记录行作修改(insert | update)的事务ID。至于delete操作,InnoDB认为是一个update操作,不过会更新一个另外的删除位,将行表示为deleted。并非真正删除。
        2. 7字节的回滚指针(DB_ROLL_PTR):回滚指针,指向当前记录行的undo log信息,也就是记录行的历史版本
        3. 6字节的行号(DB_ROW_ID):随着新行插入而单调递增的行ID。这个DB_ROW_ID跟MVCC关系不大。当表没有主键或唯一非空索引时,innodb就会使用这个行ID自动产生聚簇索引。如果表有主键或唯一非空索引,聚簇索引就不会包含这个行ID了。

1.2、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。源码 350行:
        ③ up_limit_id活跃事务列表trx_ids中最小的事务ID,如果trx_ids为空,则up_limit_id 等于low_limit_id。(也就是让下界等于上界)源码 358行
一旦一个Read View被创建,这三个参数将不再发生变化,理解这点很重要,其中low_limit_id 和 up_limit_id分别是 trx_Ids数组的上下界。
1.3 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、记录行修改的具体流程

         ① 首先当前事务对记录行加排他锁           
        ② 然后把改行数据拷贝到undo log中,作为旧版本
        ③ 拷贝完毕后,修改该行的数据
        ④ 事务提交,提交前用 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中的Repeatable Read级别, 只有事务在begin之后,执行第一条select(读操作)时, 才会创建一个快照(read view),将当前系统中活跃的其他事务记录起来;并且事务结束前都是使用的这个快照,不会重新创建,直到事务结束。
        ②在innodb中的Read Committed级别, 事务在begin之后,执行每条select(读操作)语句时,快照会被重置,即会重新创建一个快照(read view)。
所以这个实现的差别可以达到不同的隔离级别,两个隔离级别都是快照读

补充:

        快照读(snapshot read):普通的 select 语句(不包括 select ... lock in share mode, select ... for update)
        当前读(current read) :select ... lock in share mode,select ... for update,insert,update,delete 语句(这些语句获取的是数据库中的最新数据,官方文档:14.7.2.4 Locking Reads )

锁机制

锁(Locking)是数据库在并发访问时保证数据一致性和完整性的主要机制。在 MySQL 中,不同存储引擎使用不同的加锁方式;我们以 InnoDB 存储引擎为例介绍 MySQL 中的锁机制,其他存储引擎中的锁相对简单一些。

表级锁与行级锁

MySQL 中的锁可以按照粒度分为锁定整个表的表级锁(table-level locking)和锁定数据行的行级锁(row-level locking):
    表级锁具有开销小、加锁快的特性;表级锁的锁定粒度较大,发生锁冲突的概率高,支持的并发度低;
    行级锁具有开销大,加锁慢的特性;行级锁的锁定粒度较小,发生锁冲突的概率低,支持的并发度高。
InnoDB 存储引擎同时支持行级锁(row-level locking)和表级锁(table-level locking),默认情况下采用行级锁。

共享锁与排他锁

InnoDB 实现了以下两种类型的行锁:
    共享锁(S):允许多个事务同时获取这个锁,获得该锁的事务可以读取数据行(读锁)
    排他锁(X):同一时刻只允许一个事务获取到排他锁,获得该锁的事务可以更新或删除数据行(写锁),
共享锁和共享锁可以兼容,排他锁和其它锁都不兼容,也就是说只允许读读并发,读写,写读和写写操作都必须阻塞。
获取共享锁的方式:
    select ... for share , 
    MySQL8.0后可以使用:select ... lock in share mode
获取排他锁的方式:
    select ... for update

意向锁

innodb的意向锁主要用户多粒度的锁并存的情况。当这表锁和行锁同时存在时,可能导致冲突。例如,事务 A 获取了表中一行数据的读锁;然后事务 B 申请该表的写锁(例如修改表的结构)。如果事务 B 加锁成功,那么它就应该能修改表中的任意数据行,但是 A 持有的行锁不允许修改锁定的数据行。显然数据库需要避免这种问题,B 的加锁申请需要等待 A 释放行锁。
如果要获取表锁,按一般法的方法,首先需要看该表是否已经被其他事务加上了表级锁,然后依次查看该表中的每一行是否已经被其他事务加上了行级锁。这种方式需要遍历整个表中的记录,效率很低。为此,InnoDB 引入了另外一种锁:意向锁(Intention Lock)
意向锁属于表级锁,由 InnoDB 自动添加,不需要用户干预。意向锁也分为共享和排他两种方式:
    意向共享锁(IS):事务在给数据行加行级共享锁之前,自动获取到该表的 IS 锁。
    意向排他锁(IX):事务在给数据行加行级排他锁之前,自动获取到该表的 IX 锁。
意向共享锁和意向排他锁都是表级锁,所以事务给表添加表锁之前先判断这两个锁是否被获取,如果都没有被获取,那么可以添加表锁,省去了遍历所有数据行的开销。
由于对每个数据行加锁是互不干扰的,所以意向共享锁和意向排他锁是可以同时被获取的。

行级锁实现

InnoDB 通过给索引上的索引记录加锁的方式实现行级锁具体来说,InnoDB 实现了三种行锁的算法:记录锁(Record Lock)、间隙锁(Gap Lock)和 Next-key 锁(Next-key Lock)。

记录锁

记录锁(Record Lock)是针对索引记录(index record)的锁定例如,SELECT * FROM t WHERE id = 1 FOR UPDATE;会阻止其他事务对表 t 中 id = 1 的数据执行插入、更新,以及删除操作。
记录锁永远都是锁定索引记录,锁定非聚集索引会先锁定聚集索引,再锁定非聚簇索引。如果表中没有定义索引,InnoDB 默认为表创建一个隐藏的聚簇索引,并且使用该索引锁定记录。

间隙锁

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

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

间隙锁的目的是为了防止幻读,其主要通过两个方面实现这个目的:
(1)防止间隙内有新数据被插入。
(2)防止已存在的数据,更新成间隙内的数

需要注意的是,不同事务可以获取一个间隙上互相冲突的锁。SELECT * FROM t WHERE c1= 1 FOR UPDATE;只会对 id = 1 的索引记录加上记录锁,而不关心其他事务是否会在前面的间隙中插入数据。但是,如果 id 列上没有索引或者创建的是非唯一索引,则该语句会锁定前面的间隙。
InnoDB 间隙锁的唯一目的是阻止其他事务在间隙中插入数据。间隙锁可以共存,一个事务的间隙锁不会阻止另一个事务在同一个间隙上获取间隙锁。共享间隙锁和排他间隙锁之间没有区别,彼此不冲突,它们的作用相同。

Next-key 锁

Next-key 锁(Next-key Lock)锁其实是记录锁加间隙锁,即锁定一个范围,并且锁定记录本身
默认隔离级别(REPEATABLE READ )下,InnoDB 通过 next-key 锁进行查找和索引扫描,用于防止幻读;因为它会锁定范围值,不会导致两次查询结果的数量不同。
如果我们将语句修改为SELECT * FROM t WHERE c3 between 1 and 10 FOR UPDATE;,通过无索引的字段操作范围值,也会锁定主键的所有范围。这也就是为什么 MySQL 推荐通过索引操作数据,最好是主键。

插入意向锁

插入意向锁(Insert Intention Lock)是在插入数据行之前,由 INSERT 操作设置的一种间隙锁。插入意向锁表示一种插入的意图,如果插入到相同间隙中的多个事务没有插入相同位置,则不需要互相等待。假设存在索引记录 4 和 7。两个事务分别尝试插入 5 和 6,它们在获取行排他锁之前,分别使用插入意向锁来锁定 4 到 7 之间的间隙;但是不会相互阻塞,因为插入的是不同的行。
插入意向锁的作用是为了提高并发插入的性能。间隙锁不允许多个事务同时插入同一个索引间隙,但是插入意向锁允许多个事务同时插入同一个索引间隙内的不同数据值。

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根据唯一索引,找到相应记录,将主键索引值和唯一索引值加上记录锁。但不使用Gap Lock(间隙锁)。

文章参考:


posted @ 2020-04-19 11:38  Lucky小黄人^_^  阅读(2893)  评论(0编辑  收藏  举报