MYSQL事务的底层原理( 二 )

MySQL 在 REPEATABLE READ 隔离级别下,是可以很大程度避免幻读问题的发生的 。
版本链
对于使用 InnoDB 存储引擎的表来说,它的聚簇索引记录中都包含两个必要的隐藏列:

  • trx_id:每次一个事务对某条聚簇索引记录进行改动时,都会把该事务的事务 id 赋值给 trx_id 隐藏列;
  • roll_pointer:每次对某条聚簇索引记录进行改动时,都会把旧的版本写入到 undo 日志中,然后这个隐藏列就相当于一个指针 , 可以通过它来找到该记录修 改前的信息;
演示
-- 创建表
CREATETABLEmvcc_test (
idINT,
nameVARCHAR(100),
domAInvarchar(100),
PRIMARYKEY(id)
)Engine=InnoDBCHARSET=utf8;
-- 添加数据
INSERTINTOmvcc_test VALUES(1,'habit','演示mvcc');
假设插入该记录的事务 id=50 , 那么该条记录的展示如图:
?假设之后两个事务 id 分别为 70、90 的事务对这条记录进行 UPDATE 操作 。
trx_id=70trx_id=90begin  begin  update mvcc_test set name='habit_trx_id_70_01' where id=1 update mvcc_test set name='habit_trx_id_70_02' where id=1 commit  update mvcc_test set name='habit_trx_id_90_01' where id=1 update mvcc_test set name='habit_trx_id_90_02' where id=1 commit 每次对记录进行改动,都会记录一条 undo 日志,每条 undo 日志也都有一个 roll_pointer 属性,可以将这些 undo 日志都连起来,串成一个链表 。
MYSQL事务的底层原理

文章插图
对该记录每次更新后,都会将旧值放到一条 undo 日志中,就算是该记录的一个旧版本,随着更新次数的增多,所有的版本都会被 roll_pointer 属性连接成一个链表,把这个链表称之为版本链 , 版本链的头节点就是当前记录最新的值 。另外,每个版本中还包含生成该版本时对应的事务 id 。于是可以利用这个记录的版本链来控制并发事务访问相同记录的行为,那么这种机制就被称之为:多版本并发控制,即 MVCC 。
ReadView
对于使用 READ UNCOMMITTED 隔离级别的事务来说,由于可以读到未提交事务修改过的记录,所以直接读取记录的最新版本就好了 。
对于使用 SERIALIZABLE 隔离级别的事务来说,InnoDB 使用加锁的方式来访问记录 。
对于使用 READ COMMITTED 和 REPEATABLE READ 隔离级别的事务来说 , 都必须保证读到已经提交了的事务修改过的记录 , 也就是说假如另一个事务已经修改了记录但是尚未提交 , 是不能直接读取最新版本的记录的,核心问题就是:READ COMMITTED 和 REPEATABLE READ 隔离级别在不可重复读和幻读上的区别是从哪里来的,其实结合前面的知识,这两种隔离级别关键是需要判断一下版本链中的哪个版本是当前事务可见的 。
为此,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 中的最大值,事务 id 是递增分配的 。比方说现在有 id 为 1 , 2,3 这三个事务,之后 id 为 3 的事务提交了 。那么一个新的读事务在生成 ReadView 时 , m_ids 就包括 1 和 2,min_trx_id 的值就是 1,max_trx_id 的值就是 4;
  • creator_trx_id:表示生成该 ReadView 的事务的事务 id;
有了这个 ReadView,这样在访问某条记录时 , 只需要按照下边的步骤判断记录的某个版本是否可见:
  1. 如果被访问版本的 trx_id 属性值与 ReadView 中的 creator_trx_id 值相同,意味着当前事务在访问它自己修改过的记录,所以该版本可以被当前事务访问;
  2. 如果被访问版本的 trx_id 属性值小于 ReadView 中的 min_trx_id 值,表明生成该版本的事务在当前事务生成 ReadView 前已经提交,所以该版本可以被当前事务访问;
  3. 如果被访问版本的 trx_id 属性值大于或等于 ReadView 中的 max_trx_id 值,表明生成该版本的事务在当前事务生成 ReadView 后才开启,所以该版本不可以被当前事务访问;
  4. 如果被访问版本的 trx_id 属性值在 ReadView 的 min_trx_id 和 max_trx_id 之间 min_trx_id < trx_id < max_trx_id , 那就需要判断一下 trx_id 属性值是不是在 m_ids 列表中,如果在 , 说明创建 ReadView 时生成该版本的事务还是活跃的,该版本不可以被访问;如果不在,说明创建 ReadView 时生成该版本的事务已经被提交,该版本可以被访问;


    推荐阅读