MVCC
MVCC 全称 Multi-Version Concurrency Control,多版本并发控制,主要是为了提高数据库的并发性能
平时,在同一行数据上同时发生读写请求时,会上锁阻塞住,但 MVCC 提供更好的方式去处理读-写请求,可以做到在发生读-写请求冲突时不用加锁
这个读是快照读,不是当前读,当前读是一种加锁的操作,是悲观锁 > FOR UPDATE
前面在介绍 隔离级别
时,说到了 MySQL 在 REPEATABLE READ
隔离级别下,可以很大程度上避免幻读问题的发生,从以下几个概念及实操来说明是如何去避免的
版本链
当对一条数据并发执行多次操作时,对该条数据会形成版本链
对于使用 InnoDB 存储引擎的表来说,它的聚簇索引记录中包含了两个必要的隐藏列,不包括 row_id
row_id 并不是必要的,在创建的表中有主键或非 NULL 的 UNIQUE 键时,row_id 就是主键或唯一键,若没有主键或唯一键,会默认生成 row_id
必要的两个隐藏列,如下:
- trx_id:一个事务对某条聚簇索引记录进行改动时,都会把该事务的 id 赋值给 trx_id 隐藏列
- roll_pointer:在对某条聚簇索引记录进行改动时,都会把旧的版本写入到 undo 日志中,然后 roll_pointer 这个隐藏列就相当于一个指针,可以通过它来
溯源
该记录修改前的信息
undo 日志:为了实现事务的原子性操作,InnoDB 存储引擎在实际进行 DML 操作记录时,都先要把对应的 undo 日志记下来。
一般对一条记录进行一次改动,就对应一条 undo 日志
,但在某些更新操作中,也可能会对应两条 undo 日志;一个事务在执行过程中可能会新增、删除、更新若干条记录,也就是说会记录很多条 undo 日志,这些 undo 日志会从 0 开始编号,依此按顺序生成:1、2、…、N,此编号被称之为undo no
undo 日志是 MySQL 三大日志其中之一,还包含了 redo log、bin log,日志这方面的内容后续文章再详细分析
为了说明【MySQL 在 REPEATABLE READ 隔离级别下,可以很大程度上避免幻读问题的发生】此问题,创建一张演示表,如下:
create table technology_column( id BIGINT(10) not null primary key auto_increment COMMENT '主键', category_name varchar(30) not null COMMENT '专栏名称' ) Engine=InnoDB CHARSET=utf8 COMMENT '技术专栏表';
往这张表中插入一条数据,如下:
INSERT INTO technology_column VALUES(1, 'Spring');
假设插入该条数据的事务 id > trx_id = 80,那么此条记录的示意图如下:
假设之后有两个事务:trx_id 分别为 100、120,对这条记录进行了 UPDATE 操作,操作流程如下:
对记录每次进行改动,都会记录一条 undo 日志,每条 undo 日志都有一个 roll_pointer
属性
(INSERT INTO 操作对应的 undo 日志没有该属性,因为该记录没有更早的版本)可以将这些 undo 日志连起来,串成一个链表,如上图右侧所示~
对 INSERT INTO 操作后的记录,每次更新后,都会将旧值放到一条 undo 日志中,就当是该记录的一个旧版本,
便于作事务回滚、数据溯源
,随着更新的次数增多,所有的版本都会被 roll_pointer 属性连接成一个链表,将这个链表称为版本链
,版本链的头节点就是当前记录最新的值;另外,每个版本中还包含了生成该版本对应的事务 id
基于此,利用该记录的版本链来控制并发事务同时访问该记录的行为,那么这种机制就称之为多版本并发控制 MVCC
ReadView
读取视图 > 作用于 SQL 查询语句
对于使用 READ UNCOMMITTED 读未提交隔离级别的事务来说,由于可以
读取到未提交的事务修改过的信息,所以直接读取记录的最新版本即可,由此读未提交就会出现脏读、不可重复读、幻读
对于使用 SERIALIZABLE 可串行化读隔离级别的事务来说,InnoDB 采用加锁的方式来访问记录,
当事务正在执行时,其他事务就会阻塞住直到前面的事务提交或回滚后才会执行,所以不会出现脏读、不可重复读、幻读
引入版本链的机制主要是为了解决:已提交读、可重复读的事务隔离级别
对于使用 READ COMMITTED、REPEATABLE READ 隔离级别事务来说,都必须保证读取到的数据是已经事务已提交修改过的记录,也就是说:假如事务已经修改了记录但尚未提交,是不能直接读取到最新版本记录的
核心问题:READ COMMITTED、REPEATABLE READ 隔离级别在不可重复读、幻读上的区别是从何而来,基于前面所介绍的版本链,主要关键是需要判断这两种级别在版本链中哪个版本是当前事务可见的
,为此 InnoDB 提出了 ReadView 概念
ReadView 主要包含了四个比较重要的内容,如下:
- m_ids:表示在生成 ReadView 时当前系统正在活跃的读写事务的事务 id 集合
- min_trx_id:表示在生成 ReadView 时当前系统中获取的读写事务中的最小事务 id,也就是 m_ids 中的最小值
- max_trx_id:表示在生成 ReadView 时当前系统中应该分配给下一个事务的 id 值
max_trx_id 并不是 m_ids 集合中的最大值,事务 id 是递增分配的;比如:现在有 id > 1、2、3 三个活跃事务,之后 id=3 的事务提交了,那么新的读事务在生成 ReadView 时, m_ids 集合中还有 1、2,min_trx_id 值就为 1,max_trx_id 值就为 4
- creator_trx_id:表示生成该 ReadView 读取视图的事务 id
下面来具体介绍,READ COMMITTED、REPEATABLE READ 隔离级别是如何分别处理脏读、不可重复读、幻读问题的.
在 MySQL 中,READ COMMITTED、REPEATABLE READ 隔离级别非常大的一个区别就是它们生成 ReadView 的时机不同
READ COMMITTED
READ COMMITTED 隔离级别的事务在每次查询开始时都会生成一个 ReadView
以上面的 technology_column
表为例,现在只有一条事务 id 为 80 插入的一条记录
比如:现有系统中有两个事务 > Trx_id 100、120 在执行,事务 > Trx_id 100、120 SQL 语句如下:
Trx_id-100、120 Begin; Trx_id-100、120:select * from technology_column where id = 1; Trx_id-100:update technology_column set category_name ='MySQL' where id = 1; Trx_id-100:update technology_column set category_name ='Redis' where id = 1; Trx_id-100:Commit; Trx_id-120:update technology_column set category_name ='分布式' where id = 1; Trx_id-120:update technology_column set category_name ='Linux' where id = 1; Trx_id-120:Commit;
脏读问题
在 Trx_id-100:Commit;
语句执行之前,technology_column.id =1 记录得到的版本链表,如下所示:
# 查询语句 select * from technology_column where id = 1;
以上使用 READ COMMITTED 隔离级别的事务,Trx_id 100、120 的事务均未提交,所以此时查询的数据仍然为 Spring!在 Trx_id-100:Commit;
语句执行之前,整个的执行过程如下:
- 在执行语句时,会先生成一个 ReadView
- ReadView 活跃集合 m_ids 内容为 100,120,min_trx_id 为 100,max_trx_id 为 121,creator_id 为 0
- 从版本链中挑选可见的记录,从上图中可以看出,最新版本的 category_name 值内容为 Redis;该版本的 trx_id 为 100,在 m_ids 集合内,所以不符合可见性要求
trx_id 属性值在 ReadView 中 min_trx_id、max_trx_id 之间,说明创建 ReadView 时生成该版本的事务还是活跃的,该版本不可以被访问;若不在 min_trx_id、max_trx_id 之间,说明创建 ReadView 时生成该版本的事务已经被提交,该版本才可以被访问
- 通过 Roll_ptr 指针跳到下一版本继续访问,下一个版本 category_name 值内容为 MySQL,该版本的 trx_id 为 100,仍然在 m_ids 集合内,也不符合可见性要求
- 通过 Roll_ptr 指针跳到下一版本继续访问,下一个版本 category_name 值内容为 Spring,该版本的 trx_id 为 80,小于 ReadView 中 min_trx_id 值,所以这个版本是符合要求的,最后返回给客户端的版本就是这条 category_name 值内容为 Spring 的记录
所以有这种机制存在,就不会发生脏读问题!因为会去判断活跃的事务版本,必须是不在活跃中的事务版本才能使用,也就不可能读到没有 commit 提交的记录