多版本并发控制(mvcc )中文全版本控制是指现代数据库(包括MySQL、Oracle、PostgreSQL等)引擎实现中常用的读写
这样,在同时执行不同事务的同时,SELECT操作不加锁,通过读取由MVCC机制指定的版本历史记录,并确保读取的记录值满足事务所的独立性水平的手段,实现同时方案中的读取和
举出读取多个版本的例子。 例如,两个事务a和b按以下顺序进行更新和读取
在提交事务a之前和之后,事务b读取的x值是什么? 答案是,事务b读取的值因隔离级别而异。
如果事务b的隔离级别为ru(readuncommit ),则两次读取将读取x的最新值,即20。
如果事务b的隔离级别是提交的读取(RC ),则首先读取旧值10,第二次读取新值20,因为事务a已提交。
如果事务b的隔离级别是可重复的或串行的(RR,s ),则无论是否提交了事务a,两次都会读取旧值10。
在不同的隔离级别下,可以看到数据库通过MVCC和隔离级别按规则执行事务之间的并行处理,以确保单个事务中前后数据的一致性。
为什么MVCC InnoDB比MyISAM有两大特征? 一种是支持行级锁而不是事务,事务的引入带来了新的挑战。 与串行处理相比,并发事务处理可以大大提高数据库资源的利用率,提高数据库系统的事务吞吐量,支持更多的用户。 然而,并发事务存在一些问题,主要是:
丢失更新如果两个或更多事务选择同一行,并根据第一个选定的值更新该行,则丢失更新问题——最后一次更新将由其他事务更新,因为每个事务不知道其他事务的存在要避免此问题,最好在一个事务更改了数据但尚未提交时,阻止其他事务访问和更改同一数据。
脏读(Dirty Reads )某个事务对一个记录进行了更改,在提交该事务之前,该记录中的数据处于不一致状态。 此时,其他事务也来读取同一条记录。 如果不加以控制,第二个事务将读取这些未提交的脏数据,并对其进行进一步处理,从而导致未提交的数据相关性。 这种现象被形象地称为“污渍读取”。
非可重复读取(Non-Repeatable Reads ) :事务读取期间某些数据已更改或某些记录已被删除。 这种现象叫做“不能重复阅读”。
幻读(Phantom Reads )一个事务在相同的查询条件下重新读取了以前检索到的数据,但另一个事务插入了满足查询条件的新数据,这种情况称为幻读。
上述是同时事务中存在的问题,为了解决更新的丢失,可以委托APP,但后者3者需要提供事务间的隔离机制。 实现隔离机制的方法主要有两种。
打开读写锁
一致的快照读取,即MVCC
但本质上隔离水平是同时性和并发不良反应之间的妥协,数据库通常倾向于采用Weak Isolation。
InnoDB的MVCC正文聚焦于MySQL的MVCC实现,在《高性能 MySQL》一书中从MVCC的介绍中可以看出:
MySQL中InnoDB引擎支持MVCC
为了应对高并发事务,MVCC比简单的行锁更有效,开销小
MVCC在“读已提交”和“可读取”隔离级别上工作
MVCC可以基于乐观锁定和悲观锁定两者来实现
InnoDB MVCC实现原理InnoDB中MVCC的实现方法是每行记录都有两个隐藏列: DATA_TRX_ID和DATA_ROLL_PTR。 如果没有主键,隐藏的主键列也会增加一个。
DATA_TRX_ID记录上次更新此行记录的事务ID。 大小为6字节
DATA_ROLL_PTR表示指向该行的回退段(rollback segment )的指针,大小为7个字节,InnoDB将从此指针中查找以前版本的数据。 该行记录上的所有旧版本都在还原中以链表的形式组织。
DB_ROW_ID行ID (隐藏单调递增ID )的大小为6个字节,如果表中没有主键,InnoDB会自动生成隐藏主键,因此会显示此列。 此外,每个记录的标头信息record header都有一个特殊的bit(deleted_flag ),用于指示当前记录是否已被删除。
如何组织还原日志链关于重做日志和还原日志的概念可以看到以前的报道InnoDB的重做和还原日志
如上所述,如果多个事务并行处理一个行数据,则不同的事务会为该行数据的UPDATE生成多个版本,并通过回滚指针将其组织到一个还原日志链中。 在这个简单的例子中,我们来看看DATA_TRX_ID和DATA_ROLL_PTR这两个链接是如何组织的
参数在其中又起到什么样的作用。还是以上文 MVCC 的例子,事务 A 对值 x 进行更新之后,该行即产生一个新版本和旧版本。假设之前插入该行的事务 ID 为 100,事务 A 的 ID 为 200,该行的隐藏主键为 1。
事务 A 的操作过程为:
对 DB_ROW_ID = 1 的这行记录加排他锁
把该行原本的值拷贝到 undo log 中,DB_TRX_ID 和 DB_ROLL_PTR 都不动
修改该行的值这时产生一个新版本,更新 DATA_TRX_ID 为修改记录的事务 ID,将 DATA_ROLL_PTR 指向刚刚拷贝到 undo log 链中的旧版本记录,这样就能通过 DB_ROLL_PTR 找到这条记录的历史版本。如果对同一行记录执行连续的 UPDATE,Undo Log 会组成一个链表,遍历这个链表可以看到这条记录的变迁
记录 redo log,包括 undo log 中的修改
那么 INSERT 和 DELETE 会怎么做呢?其实相比 UPDATE 这二者很简单,INSERT 会产生一条新纪录,它的 DATA_TRX_ID 为当前插入记录的事务 ID;DELETE 某条记录时可看成是一种特殊的 UPDATE,其实是软删,真正执行删除操作会在 commit 时,DATA_TRX_ID 则记录下删除该记录的事务 ID。
在 RU 隔离级别下,直接读取版本的最新记录就 OK,对于 SERIALIZABLE 隔离级别,则是通过加锁互斥来访问数据,因此不需要 MVCC 的帮助。因此 MVCC 运行在 RC 和 RR 这两个隔离级别下,当 InnoDB 隔离级别设置为二者其一时,在 SELECT 数据时就会用到版本链
核心问题是版本链中哪些版本对当前事务可见?
InnoDB 为了解决这个问题,设计了 ReadView(可读视图)的概念。
RR 下的 ReadView 生成在 RR 隔离级别下,每个事务 touch first read 时(本质上就是执行第一个 SELECT 语句时,后续所有的 SELECT 都是复用这个 ReadView,其它 update, delete, insert 语句和一致性读 snapshot 的建立没有关系),会将当前系统中的所有的活跃事务拷贝到一个列表生成 ReadView。
下图中事务 A 第一条 SELECT 语句在事务 B 更新数据前,因此生成的 ReadView 在事务 A 过程中不发生变化,即使事务 B 在事务 A 之前提交,但是事务 A 第二条查询语句依旧无法读到事务 B 的修改。
下图中,事务 A 的第一条 SELECT 语句在事务 B 的修改提交之后,因此可以读到事务 B 的修改。但是注意,如果事务 A 的第一条 SELECT 语句查询时,事务 B 还未提交,那么事务 A 也查不到事务 B 的修改。
在 RC 隔离级别下,每个 SELECT 语句开始时,都会重新将当前系统中的所有的活跃事务拷贝到一个列表生成 ReadView。二者的区别就在于生成 ReadView 的时间点不同,一个是事务之后第一个 SELECT 语句开始、一个是事务中每条 SELECT 语句开始。
ReadView 中是当前活跃的事务 ID 列表,称之为 m_ids,其中最小值为 up_limit_id,最大值为 low_limit_id,事务 ID 是事务开启时 InnoDB 分配的,其大小决定了事务开启的先后顺序,因此我们可以通过 ID 的大小关系来决定版本记录的可见性,具体判断流程如下:
如果被访问版本的 trx_id 小于 m_ids 中的最小值 up_limit_id,说明生成该版本的事务在 ReadView 生成前就已经提交了,所以该版本可以被当前事务访问。
如果被访问版本的 trx_id 大于 m_ids 列表中的最大值 low_limit_id,说明生成该版本的事务在生成 ReadView 后才生成,所以该版本不可以被当前事务访问。需要根据 Undo Log 链找到前一个版本,然后根据该版本的 DB_TRX_ID 重新判断可见性。
如果被访问版本的 trx_id 属性值在 m_ids 列表中最大值和最小值之间(包含),那就需要判断一下 trx_id 的值是不是在 m_ids 列表中。如果在,说明创建 ReadView 时生成该版本所属事务还是活跃的,因此该版本不可以被访问,需要查找 Undo Log 链得到上一个版本,然后根据该版本的 DB_TRX_ID 再从头计算一次可见性;如果不在,说明创建 ReadView 时生成该版本的事务已经被提交,该版本可以被访问。
此时经过一系列判断我们已经得到了这条记录相对 ReadView 来说的可见结果。此时,如果这条记录的 delete_flag 为 true,说明这条记录已被删除,不返回。否则说明此记录可以安全返回给客户端。
举个例子 RC 下的 MVCC 判断流程我们现在回看刚刚的查询过程,为什么事务 B 在 RC 隔离级别下,两次查询的 x 值不同。RC 下 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。
RR 下的 MVCC 判断流程如果在 RR 隔离级别下,为什么事务 B 前后两次均查询到 10 呢?RR 下生成 ReadView 是在事务开始时,m_ids 为 [200,300],后面不发生变化,因此即使事务 A 提交了,trx_id 为 200 的记录依旧处于 m_ids 中,不能被访问,只能访问版本链中的记录 10
一个争论点其实并非所有的情况都能套用 MVCC 读的判断流程,特别是针对在事务进行过程中,另一个事务已经提交修改的情况下,这时不论是 RC 还是 RR,直接套用 MVCC 判断都会有问题,例如 RC 下:
事务 A 的 trx_id = 200,事务 B 的 trx_id = 300,且事务 B 修改了数据之后在事务 A 之前提交,此时 RC 下事务 A 读到的数据为事务 B 修改后的值,这是很显然的。下面我们套用下 MVCC 的判断流程,考虑到事务 A 第二次 SELECT 时,m_ids 应该为 [200],此时该行数据最新的版本 DATA_TRX_ID = 300 比 200 大,照理应该不能被访问,但实际上事务 A 选取了这条记录返回。
这里其实应该结合 RC 的本质来看,RC 的本质就是事务中每一条 SELECT 语句均可以看到其他已提交事务对数据的修改,那么只要该事物已经提交其结果就是可见的,与这两个事务开始的先后顺序无关,不完全适用于 MVCC 读。
RR 级别下还是用之前那张图:
这张图的流程中,事务 B 的 trx_id = 300 比事务 A 200 小,且事务 B 先于事务 A 提交,按照 MVCC 的判断流程,事务 A 生成的 ReadView 为 [200],最新版本的行记录 DATA_TRX_ID = 300 比 200 大,照理不能访问到,但是事务 A 实际上读到了事务 B 已经提交的修改。这里还是结合 RR 本质进行解释,RR 的本质是从第一个 SELECT 语句生成 ReadView 开始,任何已经提交过的事务的修改均可见。
RC、RR 两种隔离级别的事务在执行普通的读操作时,通过访问版本链的方法,使得事务间的读写操作得以并发执行,从而提升系统性能。RC、RR 这两个隔离级别的一个很大不同就是生成 ReadView 的时间点不同,RC 在每一次 SELECT 语句前都会生成一个 ReadView,事务期间会更新,因此在其他事务提交前后所得到的 m_ids 列表可能发生变化,使得先前不可见的版本后续又突然可见了。而 RR 只在事务的第一个 SELECT 语句时生成一个 ReadView,事务操作期间不更新。