MySQL事务隔离级别详解及MVCC实现原理

首先,初始化一张表,通过例子来讲解今天的内容

CREATE TABLE hero (
    number INT,
    name VARCHAR(100),
    country varchar(100),
    PRIMARY KEY (number)
) Engine=InnoDB CHARSET=utf8;

事务是什么

事务是一组操作,要么全部执行,要么全部都不执行。

事务的隔离级别

提起事务,首先想到的是事务的四个特性(ACID,详见//todo),关于事务的隔离性,主要讨论的还是多个事务并发处理同一个数据针对效率和事务隔离做的权衡。

事务并发存在的问题

  • 脏写:一个事务修改了另一个未提交事务修改过的数据
  • 脏读:一个事务读到了另一个未提交事务修改过的数据
  • 不可重复读:一个事务只能读到另一个已经提交的事务修改过的数据,并且其他事务每对该数据进行一次修改并提交后,该事务都能查询得到最新值
  • 幻读:一个事务先根据某些条件查询出一些记录,之后另一个事务又向表中插入了符合这些条件的记录,原先的事务再次按照该条件查询时,能把另一个事务插入的记录也读出来

对于先前已经读到的记录,之后又读取不到这种情况,其实这相当于对每一条记录都发生了不可重复读的现象。

幻读只是重点强调了读取到了之前读取没有获取到的记录。

按问题严重性排序

脏写 > 脏读 > 不可重复读 > 幻读

四种隔离级别

MySQL事务隔离级别详解及MVCC实现原理_第1张图片

不论是哪种隔离级别,都不允许脏写的情况发生,因为脏写问题太严重了。

隔离级别下,如何避免脏写呢?使用排他锁

假定A先更新数据,会对更新的数据行记录加上排他锁(也叫写锁,悲观锁),除非事务A提交或终止从而释放排他锁,否则事务B都是无法更新数据的。

读未提交下事务B的更新操作也是需要等待事务A的排他锁释放.

MVCC原理

InnoDB在实现MVCC时用到的一致性视图,用于支持RC和RR两种隔离级别的实现。
InnoDB里面每个事务都有一个唯一的事务ID,叫做transaction id,它是事务开始的时候向innoDB的事务系统申请的,顺序递增。

版本链

对于使用InnoDB存储引擎的表来说,它的聚簇索引记录中都包含两个必要的隐藏列:

  • trx_id:当某条记录被修改的时候,会将该事务的id赋值给trx_id隐藏列
  • roll_pointer:当记录被修改的时候,旧版本会写在undo log中,这个隐藏列就会记录指向旧版本的指针,通过它来找到之前的信息。

所有的版本都会被roll_pointer属性连接成一个链表,这个链表叫做版本链,头节点代表的当前记录最新值。

假设两个事务id分别为100、200的事务对这条记录进行UPDATE操作,操作流程如下:
MySQL事务隔离级别详解及MVCC实现原理_第2张图片
版本链如下
MySQL事务隔离级别详解及MVCC实现原理_第3张图片

ReadView

  • 对于读未提交隔离级别来说,因为可以读到未提交的数据,那么读最新就好了
  • 对于串行化隔离级别来说,使用加锁方式串行走。
  • 读已提交和可重复读必须保证读到的已经提交的事务修改的记录,核心问题在于判断版本链中的哪个版本,对于当前事务是可见的。

InnoDB提出ReadView,ReadView中主要包含4个比较重要的内容:

  • m_ids:表示在生成ReadView时当前系统中活跃的读写事务的事务id列表。
  • min_trx_id:表示在生成ReadView时当前系统中活跃的读写事务中最小的事务id,也就是m_ids中的最小值。
  • max_trx_id:表示生成ReadView时系统中应该分配给下一个事务的id值。max_trx_id并不是m_ids中的最大值。
  • creator_trx_id:表示生成该ReadView的事务的事务id。

我们前边说过,只有在对表中的记录做改动时(执行INSERT、DELETE、UPDATE这些语句时)才会为事务分配事务id,否则在一个只读事务中的事务id值都默认为0。

MySQL事务隔离级别详解及MVCC实现原理_第4张图片
通过下面步骤可以判断版本链的某个版本对当前是否可见

  • 访问版本的trx_id属性值==ReadView中的creator_trx_id值,当前事务在访问它自己的记录,所以该版本可被访问。
  • 访问版本的trx_id属性值
  • 访问版本的trx_id属性值>ReadView中的creator_trx_id值,说明事务在开启这个ReadView后才开启,所以不可见
  • trx_id属性值在ReadView的min_trx_id和max_trx_id之间,那就需要判断下是否在m_ids列表中,在的话,说明还没提交,不可见,不在的话,说明已经提交,可见。

顺着版本链寻找,如果找不到的话,那就说明这条记录对当前事务不可见。

READ COMMITTED和REPEATABLE READ

READ COMMITTED和REPEATABLE READ隔离级别的的一个非常大的区别就是它们生成ReadView的时机不同。

使用READ COMMITTED隔离级别的事务在每次查询开始时都会生成一个独立的ReadView。

READ COMMITTED

比方说现在系统里有两个事务id分别为100、200的事务在执行:

#Transaction 100
BEGIN;

UPDATE hero SET name = '关羽' WHERE number = 1;

UPDATE hero SET name = '张飞' WHERE number = 1;
# Transaction 200
BEGIN;

# 更新了一些别的表的记录
...

这个时候版本链如下
MySQL事务隔离级别详解及MVCC实现原理_第5张图片
假设现在有一个使用READ COMMITTED隔离级别的事务开始执行:

# 使用READ COMMITTED隔离级别的事务
BEGIN;

# SELECT1:Transaction 100、200未提交
SELECT * FROM hero WHERE number = 1; # 得到的列name的值为'刘备'

执行过程如下

  • SELECT生成一个ReadView,ReadView的m_ids是[100,200],min_trx_id是100,max_trx_id是201,creator_trx_id是0.
  • 从版本链头节点开始遍历,记录值为张飞的trx_id是100,在m_ids列表内,不可见
  • 继续遍历,记录值为关羽的trx_id也是100 不可见
  • 继续遍历,记录值为刘备的trx_id的是80,小于ReadView的min_trx_id值100,可见

之后,我们把事务id为100的事务提交一下,就像这样:

# Transaction 100
BEGIN;

UPDATE hero SET name = '关羽' WHERE number = 1;

UPDATE hero SET name = '张飞' WHERE number = 1;

COMMIT;

然后再到事务id为200的事务中更新一下表hero中number为1的记录:

# Transaction 200
BEGIN;

# 更新了一些别的表的记录
...

UPDATE hero SET name = '赵云' WHERE number = 1;

UPDATE hero SET name = '诸葛亮' WHERE number = 1;

版本链如下
MySQL事务隔离级别详解及MVCC实现原理_第6张图片
然后再到刚才使用READ COMMITTED隔离级别的事务中继续查找这个number为1的记录,如下:

# 使用READ COMMITTED隔离级别的事务
BEGIN;

# SELECT1:Transaction 100、200均未提交
SELECT * FROM hero WHERE number = 1; # 得到的列name的值为'刘备'

# SELECT2:Transaction 100提交,Transaction 200未提交
SELECT * FROM hero WHERE number = 1; # 得到的列name的值为'张飞'

这个SELECT2的执行过程如下:

  • SELECT会重新生成一个ReadView,ReadView的m_ids是[200],min_trx_id是200,max_trx_id是201,creator_trx_id是0
  • 从版本链头节点开始遍历判断
REPEATABLE READ

REPEATABLE READ —— 在第一次读取数据时生成一个ReadView

按以上流程分析,Read View是不变的。

最终的核心问题是:形成的版本链是不变的,变得是不同时刻的readView,然后通过顺着版本链的头节点遍历,拿出节点中的事务ID,在当前事务的readView中判断当前版本对当前事务是否可见。

更新时的事务逻辑

总结

MVCC(多版本并发控制)指的就是在使用READ COMMITTD、REPEATABLE READ,在执行SELECT操作时访问记录的版本链的过程,这样子可以使不同的事务读-写、写-读操作并发执行,从而提升索引性能。
READ COMMITTD、REPEATABLE READ隔离级别最大的不同就是生成ReadView的时机不同,READ COMMITTD会每次执行普通SELECT 操作前都生成一个ReadView,而REPEATABLE READ只在第一次进行SELECT之后生成一个ReadView,之后的查询操作都重复使用这个ReadView。

你可能感兴趣的:(每天一道面试题,MySQL)