MVCC 即多版本并发控制主要是为了解决数据库中并发事务读写的一致性问题, 那么多个事务并发执行的时候事务的隔离到底是怎么实现的呢? MySQL 默认的 RR 隔离级别是怎样避免不可重复读的问题呢? 我们好好来分析一下.
什么是 undo log 多版本链
MVCC 如何保证事务隔离
总结
** 一,****** 什么是 undo log 多版本链 ****
MySQL 的 MVCC 机制, 以 undo log 版本链为实现基础. 因此要理解 MVCC 机制, 我们得先分析下 undo log 版本链是个什么东东. Innodb 存储引擎给每个数据表都添加了三个隐藏字段: DB_TRX_ID,DB_ROLL_PTR,DB_ROW_ID.
(1)DB_TRX_ID: 标记更新当前数据记录的 transaction id, 每处理一个事务, 其值自动 + 1.
(2)DB_ROLL_PTR: 回滚指针, 记录了最新一次修改该条记录的 undo log, 回滚的时候就通过这个指针找到 undo log 回滚.
(3)DB_ROW_ID: 当数据表没有指定主键时, 数据库会自动以这个列来作为主键, 生成聚集索引.
假设有如下的一条数据, 此时这条数据对应的 DB_TRX_ID 为 10, 由于之前没有数据, 因此回滚指针对应指向空地址.
图片
每个事务都有自己的一个 id, 就像其身份证一样唯一标记该事务. 当事务启动的时候, 向 Innodb 存储引擎进行申请. 假设此时如果打南边来了个事务 A, 它的事务 id 为 12, 事务 A 对表中的数据字段 count 进行修改, 修改后该条数据对应的事务 id 为 12, 同时回滚指针指向实际的 undo log 回滚日志的地址.
图片
此时打北边又来了个事务 B, 它的事务 id 为 20, 事务 B 将表中的数据字段 count 修改为 21, 对应数据的事务 id 变为 20, 回滚指针指向上一条 undo log 信息. 如下图所示:
图片
如果一直有事务进行数据的修改, 那么就会形成一条由回滚指针串联的 undo log 多版本链条.
** 二,******MVCC 如何保证事务隔离 ****
阐述完了 undo log 多版本链条原理, 我们知道当一个事务 A(事务 id=12)更新数据后, 当前数据的事务 id 变为 12, 同时回滚指针指向 undo log 数据. 每次进行数据更新后, 事务 id 就是修改数据的事务 id, 同时回滚指针指向回滚数据, 最终形成 undo log 链. 但是仅仅依靠 undo log 多版本链好像并不能实现事务并发执行时的相互隔离, 因此我们需要一种机制可以利用 undo log 多版本链来实现事务隔离. 这个机制就是 ReadView.
MySQL 执行事务的时候, 会生成一个 ReadView, 其中会包含以下重要信息:
(1)m_ids:MySQL 中未提交的事务 id 集合;
(2)min_trx_id: 集合中最小的事务 id;
(3)max_trx_id,MySQL 下一个要生成的事务 id, 也就是事务 id 集合中最大的事务 id 加 1;
(4)当前需要执行的事务 id;
下面我们来说说 MySQL 的 RR(Repeatable Read )隔离级别即可重复读, 读取数据的事务, 无论读多少次都是和第一次读取数据获得的值时一样的. 我们一起来看下可重复读是如何通过 MVCC 实现的. 假设现在有事务 A 以及事务 B 两个事务, A 事务需要读取数据, B 事务需要修改数据.
图片
当事务 A 需要读取数据时, 开启 ReadView. 由于此时数据库活跃的事务为事务 A 以及事务 B, 那么对应的 ReadView 中 m_ids={12,34},min_trx_id=12,max_trx_id=35, 当前需要执行的事务 id 为 12. 此时事务 A 读取数据时, 先判断当前的事务 id 为 12, 而数据中的事务 id 为 11, 小于当前事务 A 的 id. 说明当前读取的数据是在事务 A 开启之前提交的, 因此可以正常进行数据读取.
图片
如果此时事务 B 进行了数据修改, 修改 count 为 29. 而事务 A 再次进行数据读取时, 继续进行判断, 发现当前数据对应的事务 id 为 34 比当前的查询事务要大, 但是小于 max_trx_id, 同时在 m_ids 中. 说明该事务 id 对应的事务和事务 A 属于并发执行事务, 因此不能进行数据读取. 则根据 undo log 版本链, 往上寻找 undo log 信息. 如果找到的事务 id 小于当前读取数据的 id 则证明此时的数据是在当前开启查询事务之前提交的, 因此可以进行数据的查询.
图片
那么另外一个问题又来了, RC 级别又是如何实现的呢? 所谓 RC 级别, 就是当别人的事务提交后, 你就可以读取到别人修改后的值. 因此会发生不可重复读问题. 当设置为事务级别为 RC 时, 它每次发起数据查询 (set session transaction isolation level read committed;) 后, 每次进行数据查询都会新开启一个新的 ReadView.
假设当前事务中活跃着两个事务, 他们的事务 id 分别是 12,34. 此时事务 id 为 34 的事务更新了数据. 此时数据更新为 29. 同时数据对应的事务 id 更新为 34, 同时回滚指针指向上一条数据. 若此时事务 id 为 15 的事务进行数据查询, 此时开启 readview, 进行检查, 发现此时的数据中对应的事务 id 在活跃事务 id 中, 说明是和查询事务差不多时机执行的, 但是此时的事务还未提交. 因此此时的数据不可以读, 所以顺着 undolog 版本链条读取上一次的数据. 同时进行判断.
如果事务 B 进行了提交, 那么事务 A 再次进行数据查询的时候, 会新开启一个 ReadView, 我们暂且称之为 ReadViewA1, 由于此时的事务 B 已经提交, 所以 ReadViewA1 中对应的活跃列表中只有事务 A 对应的事务 id 为 12. 此时发现事务已提交, 不再活跃事务列表中, 因此可以进行数据读取.
图片
综上分析, 这就是 MySQL 通过 ReadView 以及 undo log 多版本链条实现 RC 以及 RR 的秘密. RC 以及 RR 的区别就在于, RC 事务隔离级别下, 每次获取数据的时候, 都会重新生成新的 ReadView, 再根据 ReadView 中的信心进行数据读取的判断.
三, 总结
综合以上的分析我们大致理清楚了 MySQL 多事务并发的隔离原理, 其本质是通过 MVCC 机制来进行实现的. 而 MVCC 其实是基于 ReadView 以及 undo log 多版本链条实现. MySQL 的默认隔离级别是 RR, 避免了脏读, 不可重复读以及幻读的问题.
来源: http://www.jianshu.com/p/abc940938e29