ICode9

精准搜索请尝试: 精确搜索
首页 > 数据库> 文章详细

MySQL的MVCC

2020-08-16 10:02:40  阅读:433  来源: 互联网

标签:事务 log MySQL MVCC 版本 ReadView ID 隔离


MySQL的MVCC

转载地址

一、什么是MVCC

MVCC是Multi-Version Concurrency Control的简称,即多版本并发控制。MVCC是现代数据库引擎实现中常用的处理读写冲突的手段,目的在于提高数据库高并发场景下的吞吐性能。如此一来不同的事务在并发过程中,select操作可以不加锁而是通过MVCC机制读取指定的版本历史记录,并通过一些手段保证读取的记录值符合事务所处的隔离级别,从而解决并发场景下的读写冲突。

下面举一个多版本读的例子,例如两个事务A和B按照如下顺序进行更新和读取操作

transaction A transaction B
select x from table; return 10
begin transaction
update table set x=20
begin transaction
select x from table ; return rs1
commit
select x from table ; return rs2
commit

在事务A提交前后,事务B读取到的X的值是什么呢?答案是:事务B在不同的隔离级别下,读取到的值不一样。

  • 如果事务B的隔离级别是读未提交,那么两次读取均读取到X的最新值,即20。
  • 如果事务B的隔离级别是读已提交,那么第一次读取到的是旧值10,第二次因为事务A已经提交,则读取到新值20。
  • 如果事务B的隔离级别是可重复读或者串行,则两次均读到旧值10,不论事务A是否已经提交

二、为什么需要MVCC

InnoDB相比MyISAM有两大特点,一是支持事务二是支持行级锁,事务的引入带来了一些新的挑战。相对于串行处理来说,并发事务处理能大大增加数据库资源的利用率,提高数据库系统的事务吞吐量,从而可以支持更多的用户。但并发事务处理也会带来一些问题,主要包括一下几种情况:

  1. 更新丢失:当两个或多个事务选择同一行,然后基于最初选定的值更新该行时,由于每个事务都不知道其他事务的存在,就会发生丢失更新的问题-- 最后的更新覆盖了其他事务所做的更新。如何避免这个问题呢?最好在一个事务对数据进行更改但还未提交时,其他事务不能访问修改同一个数据。
  2. 脏读:一个事务正在对一条记录做修改,在这个事务提交前,这时,另一个事务夜来读取同一条记录并且读到了修改后尚未提交的数据,并依据此做了进一步的处理。这种现象就被形象的叫做脏读
  3. 不可重复读:指在一个事务内,多次读同一数据,前后读取的结果不一致。
  4. 幻读:幻读是指当事务不是独立执行时发生的一种现象,例如事务A对表中的一个数据进行了修改,这种修改涉及到表中的全部数据行。同时事务B也修改了这个表中的数据,这种修改是向表中插入一行新数据。那么就会发生操作事务A的用户发现表中还存在没有修改的数据行,就好像发生了幻觉一样。

以上是并发事务过程中会存在的问题,解决更新丢失可以交给应用,但是后三者需要数据库提供事务间的隔离机制来解决。实现隔离机制的方法主要有两种:

  1. 加读写锁
  2. 一致性快照读,即MVCC

本质上,隔离级别是一种在并发性能和并发产生的副作用间的妥协,通常数据库均倾向于后者采用 weak isolation。

三、InnoDB MVCC实现原理

InnoDB中MVCC的实现方式为:每一行记录都有两个隐藏列:DATA_TRX_ID、DATA_ROLL_PTR(如果没有主键,则还会多一个隐藏的主键列)。

image.png
DATA_TRX_ID:记录最近更新这条记录的事务ID,大小为6字节
DATA_ROLL_PTR:表示指向该行回滚段(rollback segment)的指针,大小为7个字节,InnoDB便是通过这个指针找到之前版本的数据。该行记录上所有旧版本,在undo log中通过链表的形式组织。
DB_ROW_ID:行标识(隐藏自增ID),大小为6字节,如果表没有主键,InnoDB会自动生成一个隐藏主键,因此会出现这个列。另外,每条记录的头信息(record header)里都有一个专门的bit(deleted_flag)来表示当前记录是否已经被删除。

undo log

MySQL中有六种日志文件。分别是重做日志(redo log)、回滚日志(undo log)、二进制日志(bin log)、错误日志(error log)、慢查询日志(slow query log)、一般查询日志(general log)。其中redo log、undo log、bin log与事务操作息息相关。

undo log是将用户上一步做的操作对程序造成的改动恢复到改动之前,和redo log的区别是redo log是指重新实现这种改动。

3.1版本链

在多个事务并行操作某行数据的情况下,不同事务对该行数据的update会产生多个版本,然后通过回滚指针组织成一条 undo log链,下面我们通过一个简单的例子来看一下 undo log 链是如何组织的,DATA_TRX_ID和DATA_ROLL_PTR两个参数在其中又起到什么样的作用。

还以上文MVCC的例子,事务A对值X进行更新之后,该行即产生一个新版本和旧版本。假设之前插入该行的事务ID为100,事务A的ID为200,该行的隐藏主键为1。
image.png

事务A的操作过程为:

  1. 对DB_ROW_ID=1的这行记录加排他锁
  2. 把该行原本的值拷贝到undo log中,DB_TRX_ID和DB_ROLL_PTR都不动
  3. 修改该行的值这时产生一个新版本,更新DATA_TRX_ID为修改记录的事务ID,将DATA_ROLL_PRT指向刚刚拷贝到undo log链中的旧版本记录,这样就能通过DATA_ROLL_PTR找到这条记录的历史版本。如果对同一行记录执行连续的update,undo log会组成一个链表,遍历这个链表可以看到这条记录的变迁
  4. 记录redo log,包括undo log中的修改

那么insert和delete会怎么做呢?其实相比update这两者很简单,insert会产生一条新记录,它的DATA_TRX_ID为当前插入记录的事务ID;delete某条记录时可看成是一种特殊的update,其实是软删,真正执行删除操作会在commit时,DATA_TRX_ID则记录下删除该记录的事务ID。

这里还有一个问题就是,当前事务在启动时看到的内容是哪个版本的?这里就需要视图 read-view了。而且不同时刻启动的事务会有不同的read-view。

3.2如何实现一致性读 ReadView

在读未提交隔离级别下,直接读取新版本的记录了;在串行化隔离级别下,通过加锁互斥来访问数据,因此不需要MVCC的帮助。因此MVCC运行在读提交、可重复读 这两个隔离级别下,当InnoDB隔离级别设置为二者其一时,就会用到版本链。

那现在的问题就是哪些版本是对当前启动的事务可见?为了解决这个问题,于是有了ReadView(可读视图)的概念

3.2.1可重复读隔离级别下ReadView的生成

在可重复读隔离级别下,如果是通过begin启动的事务,那么当该事务执行第一个sql语句时,生成ReadView,即会将当前系统中的所有活跃的事务拷贝到一个列表生成ReadView。

下图事务A第一条select语句在事务B更新数据前,因此生成的ReadView在事务A过程中不发生变化,即使事务B在事务A之前提交,但是事务A第二条查询语句依旧无法读到事务B的修改。

transaction A transaction B
可重复读隔离级别
set tx_isolation=repeatable-read 可重复读隔离级别
set tx_isolation=repeatable-read
begin transaction begin transaction
select x from table ; return 10
这个时候生成ReadView
update table set x=20
commit
select x from table; return 10
无法读到x=20
commit

下图中,事务A的第一条sql语句在事务B的修改提交之后,因此可以读到事务B的修改。但是注意,如果事务A的第一条select语句查询时,事务B还没有提交,那么事务A也查不到事务B的修改。

transaction A transaction B
可重复读隔离级别
set tx_isolation=repeatable-read 可重复读隔离级别
set tx_isolation=repeatable-read
begin transaction begin transaction
update table set x=20
commit
select x from table ; return 20
这个时候读到了事务B的修改x=20
commit

3.2.2读提交隔离级别下ReadView的生成

在读提交隔离级别下,每个sql语句(select查询语句)开始时,都会重新将当前系统中的活跃事务拷贝一个列表生成ReadView。二者的区别就在于生成ReadView的时间点不同,一个是事务之后第一个查询语句开始;一个是事务中每条select语句开始。

3.3 ReadView列表

ReadView中当前活跃的事务ID列表,称为m_ids,其中最小值为up_limit_id,最大值为low_limit_id,事务ID是事务开启时InnoDB分配的,其大小决定了事务开启的先后顺序,因此我们可以通过ID的大小关系来决定版本记录的可见性,其判断流程如下:

  1. 如果被访问版本的trx_id小于m_ids中的最小值up_limit_id,说明生成该版本的事务在ReadView生成之间已经提交了,所以该版本可以被当前事务访问。
  2. 如果被访问版本的trx_id大于m_ids中的最大值low_limit_id,说明生成该版本的事务在ReadView生成之后才生成,所以该版本不可以被当前事务访问。
  3. 如果被访问版本的trx_id在m_ids列表中最大值和最小值之间,那就需要判断一下trx_id是不是在m_ids列表中。如果在,说明创建ReadView时生成该版本所属事务还是活跃的,因此该版本不可以被访问,需要查找undo log链找到上一个版本,然后根据该版本的data_trx_id在从头计算一次可见性;如果不在说明创建ReadView时生成该版本的事务已经提交,该版本可以被访问
  4. 此时经过一系列的判断我们已经得到了这条记录相对ReadView来说的可见结果。此时,如果这条记录的delete_flag为true,说明这条记录已经被删除,不返回。否则说明这条记录可以安全返回给客户端。

四、举个例子

4.1 读提交下的MVCC判断流程

我们现在回看刚刚的查询过程,为什么事务B在读提交隔离级别下,两次查询X值不同。读提交隔离级别下 ReadView是在语句颗粒度上生成的。

当事务A未提交时,事务B进行查询,假设事务B的事务ID为300,此时生成ReadView的m_ids为[200,300],而最新版本的trx_id为200,处于m_ids中,则该版本不可被访问,查询版本链得到上一条记录的trx_id为100,小于m_ids的最小值200,因此可以被访问,此时事务B就查询到值10而非20。

待事务A提交之后,事务B进行查询,此时生成的ReadView的m_ids为[300],而最新的版本记录中trx_id为200,小于m_ids的最小值300,因此可以被访问到,此时事务B就查询到20。

4.2 可重复读下的MVCC判断流程

如果在可重复读隔离级别下,为什么事务B前后两次均查询到10呢?可重复读下生成ReadView是事务开始时,m_ids为[200,300],后面不发生变化,因此即使事务A提交了,trx_id为200的记录依旧处于m_ids中,不能被访问,只能访问版本链中的记录10。

五、写在最后

读提交、可重复读两种隔离级别的事务在执行普通的读操作时,通过访问版本链的方法,使得事务间的读写操作得以并发执行,从而提升系统性能。读提交、可重复读这两个隔离级别的一个很大不同就是生成ReadView的时间点不同,读提交在每一次select语句前都会生成一个ReadView,事务期间会更新,因此在其他事务提交前后所得到的m_ids列表可能发生变化,使得先前不可见的版本后续又突然可见了;而可重复读只在事务的第一个select语句时生成一个ReadView,事务操作期间不更新。

标签:事务,log,MySQL,MVCC,版本,ReadView,ID,隔离
来源: https://www.cnblogs.com/lucky9322/p/13511601.html

本站声明: 1. iCode9 技术分享网(下文简称本站)提供的所有内容,仅供技术学习、探讨和分享;
2. 关于本站的所有留言、评论、转载及引用,纯属内容发起人的个人观点,与本站观点和立场无关;
3. 关于本站的所有言论和文字,纯属内容发起人的个人观点,与本站观点和立场无关;
4. 本站文章均是网友提供,不完全保证技术分享内容的完整性、准确性、时效性、风险性和版权归属;如您发现该文章侵犯了您的权益,可联系我们第一时间进行删除;
5. 本站为非盈利性的个人网站,所有内容不会用来进行牟利,也不会利用任何形式的广告来间接获益,纯粹是为了广大技术爱好者提供技术内容和技术思想的分享性交流网站。

专注分享技术,共同学习,共同进步。侵权联系[81616952@qq.com]

Copyright (C)ICode9.com, All Rights Reserved.

ICode9版权所有