天天看点

MVCC与事务隔离级别

文章目录

    • (一)事务隔离级别
      • 1.并发产生的问题
      • 2.事务隔离级别
    • (二)MVCC
      • 1.隐藏字段和undo log
      • 2.Read View
      • 3.Read Committed的实现
      • 4.Repeatable-Read的实现
    • (三)next_key lock解决幻读问题
      • 1.record lock
      • 2.gap lock

(一)事务隔离级别

众所周知,MySQL中的INNODB引擎是支持事务的并发操作,但是并发就会带来各种的问题—例如:脏读,幻读等 所以我们首先来看一下MySQL中并发可能会产生的问题

1.并发产生的问题

  • 脏读:两个事务同时在执行,A修改了一个数据但是还没有提交,B读取并使用了A修改后的数据,但是A事务之后又回滚,那么B读到的这个数据就是不合法的,是脏的.这种情况就被称为脏读
  • 不可重复读:两个事务在执行,A在一开始读取到一个数据为x,然后B将这个数据更改为y,更改之后A又去读取该数据,结果却发现数据为y.和最开始的x不同.也就是说一个事务在过程中多次读取同一个数据,结果发现前后结果不同.这就被称为不可重复读.
  • 幻读: 两个事务在执行,A执行了一个语句比如说 select id from student where id > 1.查询出结果有4条,B这个时候插入了一条数据 **insert into student values(100) **.插入完成之后,A再执行之前相同的语句,却发现一个有5条记录! 平白无故多了一条记录.就像出现了幻觉.这个情况就被称为是幻读.

tip:不可重复读和幻读的小区别:不可重复读强调的是对原有的数据进行了修改.而幻读则强调增加或减少数据

2.事务隔离级别

隔离级别 描述 解决问题 实现方案
读未提交(read uncommitted) 当前事务可以读取其他事务没有提交的数据 什么都没解决 什么都没做
读已提交(read committed) 当前事务只能读取其他事务已提交的数据 脏读 MVCC(undo log + read view)
可重复读(repeatable-read) 可以保证事务执行期间对一个数据的多次读取结果是相同的 脏读,不可重复读 MVCC(undo log + read view)
可串行化(serializable) 最高的隔离级别,事务之间是一个一个的顺序执行,事务不能并发,自然也就没有并发问题. 什么都解决了 加锁

需要注意的是:在MySQL中,Repeatable Read 是Mysql的默认隔离级别,但是它这个Repeatable Read有点特殊,MySQL通过next_key lock解决了幻读问题.

(二)MVCC

MVCC,全称Muti-Version Concucurrency Control,翻译过来就是多版本并发控制.根据它的名字我们就可以知道: 它的目的是解决并发问题,它的方式是通过多版本.

1.隐藏字段和undo log

大家都知道数据库中有一行行的数据,但是大家可能不知道每一列除了我们插入的数据,还有一些隐式定义的字段,例如: trx_id,roll_ptr等.而且每一个事务INNODB都会给它分配一个事务id,并且按照事务执行的顺序递增,也就是说先执行的事务的id小,后面的大.
  • trx_id:每次当一个事务对该行数据进行一次更改之后,就会记录下它的事务id(trx_id).并且会生成一个undo log.这个undo log 会记录数据在更改之前的模样.
  • roll_ptr:roll_ptr相当于一个指针,它会指向该数据执行之前的undo log.并且每一条undo log也有一个roll_ptr.

下面我们以实际例子来展示一下真实的场景:

假设我们现在有一张表:

mysql> desc mvcc_test;
+-------+-------------+------+-----+---------+----------------+
| Field | Type        | Null | Key | Default | Extra          |
+-------+-------------+------+-----+---------+----------------+
| id    | int(11)     | NO   | PRI | NULL    | auto_increment |
| name  | varchar(20) | YES  |     | NULL    |                |
+-------+-------------+------+-----+---------+----------------+
2 rows in set (0.00 sec)
           

其中的数据为

mysql> select * from mvcc_test;
+----+--------+
| id | name   |
+----+--------+
|  1 | JackMa |
+----+--------+
1 row in set (0.00 sec)
           

那么此时的情况应该是这样的,

MVCC与事务隔离级别

现在我们对其进行更改

//这是A事务,假设事务id为 20
update mvcc_test set name = 'PonyMa' 

//这是B事务,假设事务id为30
update mvcc_test set name = 'BillGates' 
           

更改完之后,情况就会变成这样,这样一个通过roll_ptr连接起来的链表我们把它称作为 版本链

MVCC与事务隔离级别

2.Read View

上面我们看到了undo log的作用,通过roll_ptr我们就可以访问到之前的数据,但是我们需要对这些数据进行一个限制,要不然如果所有事务都可以随便访问之前的数据,那么undo log就没有了作用.Read View包含了系统当前包括了哪些活跃的事务,并且将它们的事务id放在一个列表中,并且命名为m_ids.这样当一个事务在访问某条记录的时候,就可以通过一定的规则的规则去查看记录的某个版本是否可见:

  • 如果被访问版本的trx_id属性值小于m_ids列表中最小的事务id,表明生成该版本的事务在生成ReadView前已经提交,所以该版本可以被当前事务访问。
  • 如果被访问版本的trx_id属性值大于m_ids列表中最大的事务id,表明生成该版本的事务在生成ReadView后才生成,所以该版本不可以被当前事务访问。
  • 如果被访问版本的trx_id属性值在m_ids列表中最大的事务id和最小事务id之间,那就需要判断一下trx_id属性值是不是在m_ids列表中,如果在,说明创建ReadView时生成该版本的事务还是活跃的,该版本不可以被访问;如果不在,说明创建ReadView时生成该版本的事务已经被提交,该版本可以被访问。

3.Read Committed的实现

还是上面的那个例子,假设事务A(trx_id = 20)已经执行完成了,而事务B(trx_id = 30)还没有执行完成,现在又有一个事务隔离级别为Read Committed的事务C来执行下列操作

//事务C,假设事务id为60,事务A(20)已经提交,事务B(30)没有提交
	select * from mvcc_test where id = 1;	# 得到结果为PonyMa,满足ReadCommitted的要求
	// 一分钟后,事务B(30)也提交了,mysql系统中没有活跃的事务
	select * from mvcc_test where id = 1;   # 得到结果为BillGates

           

那么这个过程中,是怎样通过Read View + Undo Log来解决的呢?

Read_Committed中,每执行一条语句就会生成一个Read View

  • 事务C在执行第一次 select * from mvcc_test where id = 1 会生成一个Read View,其中的m_ids是[ 30 ],因为这个时候只有事务B(30)还是活跃的.
  • 然后我们从版本链中寻找,最新的数据 trx_id 为30 大于等于 m_ids中的最小值.所以我们不使用这个值
  • 通过roll_pointer跳转到版本链中的下一条记录中,下一条记录中的trx_id为20 小于 m_ids中的 最小值,所以符合要求.最终返回的结果为 PonyMa
  • 第二次执行 select * from mvcc_test where id = 1 会再生成一个 Read View,其中的m_ids为[],此时没有事务活跃
  • 然后我们从版本链中查找,最新的数据 trx_id为30,不在m_ids,我们读取到就是 BillGates

4.Repeatable-Read的实现

照样是上面的例子,不过事务的执行顺序变一下

//这是A事务,假设事务id为 20,A执行并提交
update mvcc_test set name = 'PonyMa' 

//这是C事务,最先执行,假设事务Id为 25
select * from mvcc_test where id = 1; # 得到结果为 PonyMa

//这是B事务,假设事务id为 30,B执行并提交
update mvcc_test set name = 'BillGates' 

//这是C事务,在A,B事务执行完成之后再次执行一条语句
select * from mvcc_test where id = 1; # 得到结果为 PonyMa,结果符合repeatable read的要求
           

与Read Committed不同,Repeatable Read只会在第一次执行查询语句的时候生成一个ReadView,之后会一直使用这个ReadView

  • 事务C在执行第一次 select * from mvcc_test where id = 1的时候,生成了一个ReadView,m_ids为[20],读取最新的数据,最新数据的trx_id为1,小于m_ids的最小值,所以返回的就是JackMa.
  • 上面的查询语句执行完成之后,事务A,B分别执行并提交,生成最新的版本链
  • 事务C再次执行 select * from mvcc_test where id = 1,但是这个时候不会再生成一个ReadView,而会使用第一次生成的Read View,m_ids为[20]. 此时最新的数据的trx_id为30,大于m_ids中的最小值,所以不满足要求
  • 通过roll_pointer转到下一条版本中,下一条版本的trx_id为20,存在于m_ids.所以返回 PonyMa.

(三)next_key lock解决幻读问题

MySQL底层通过next_key lock解决了幻读问题,其实next_key lock可以看成是`

next key lock = gap lock + record lock

1.record lock

首先我们先了解一下

record lock

,假设我们有这样一张表

mysql> select * from mvcc_test;
+----+---------+
| id | name    |
+----+---------+
|  1 | JackMa  |
|  5 | MarkLau |
+----+---------+
2 rows in set (0.00 sec)
           

现在我们执行一条语句

# 事务A
select * from mvcc_test where id > 1 and id < 5 for update
# 这样数据库就会对id = 5 的数据上record锁,别的事务在事务A的执行过程就不能对该条记录进行写操作
# 这就是record锁
           

2.gap lock

然后我们在了解一下

gap lock

,gap就是间隙的意思,顾名思义它会锁住间隙,假设还是上面的那个表,我们再执行一条语句

# 事务B,此时 数据库中的间隙有(-无穷,1),(1,5),(5,正无穷)
select * from mvcc_test where id = id > 2 and id < 6 for update
# 这个时候如果是gaplock,我们命中了id=5的记录,就会锁住5左右的两个区间,(1,5),(5,正无穷),这时候如果我们插入一条数据
insert into mvcc_test values(6,'ElonMask');
#此时该语句会被阻塞,因为(5,正无穷)被锁住了.
           

下面我们应该就了解,

next key lock

gap lock + record lock

的结合体,所以在命中数据的时候,不仅会锁住本行数据,还会锁住相邻区间.

候如果我们插入一条数据

insert into mvcc_test values(6,‘ElonMask’);

#此时该语句会被阻塞,因为(5,正无穷)被锁住了.

下面我们应该就了解,`next key lock`是`gap lock + record lock`的结合体,所以在命中数据的时候,不仅会锁住本行数据,还会锁住相邻区间.