事务隔离级别的实现原理

310 阅读5分钟

一起养成写作习惯!这是我参与「掘金日新计划 · 4 月更文挑战」的第2天,点击查看活动详情

我们都知道事务有四种隔离级别:读未提交(READ-UNCOMMITTED)、读已提交(READ-COMMITTED)、可重复读(REPEATABLE-READ)、串行化(SERIALIZABLE)。

每种隔离级别的效果如下:

  • 读未提交(READ-UNCOMMITTED):最低的隔离级别,允许读取尚未提交的数据变更
  • 读已提交(READ-COMMITTED):允许读取并发事务已经提交的数据
  • 可重复读(REPEATABLE-READ):对同一字段的多次读取结果都是一致的,除非数据是被本身事务自己所修改
  • 串行化(SERIALIZABLE):最高的隔离级别,完全服从ACID的隔离级别。所有的事务依次逐个执行,这样事务之间就完全不可能产生干扰。

(对于以上内容理解不是明白的,可参考:事务隔离级别及相关锁实践

那在MySQL中,每种隔离级别是如何实现的呢?今天就聊聊事务隔离级别的实现原理。

前置知识

MySQL中,表中每条记录都有两个隐藏的字段:DB_TRX_ID、DB_ROLL_PTR。

  • DB_TRX_ID为最后一次更新(插入)行的事务ID.

  • DB_ROLL_PTR 为回滚指针,指向修改记录之前的行记录,这样每行数据都有一个版本链,可以根据它找到每一个版本的数据。

ReadView

MySQL借助「ReadView」实现不同的隔离级别。那「ReadView」是一个什么东西呢?

可以把它理解为一种数据,其主要包含4个内容:

  1. m_ids :表示在生成 ReadView 时当前系统中活跃的读写事务的事务id列表。活跃的事务,即为当前正在执行当但未提交的事务。比如,当前系统有两个活跃的事务,其ID为18,27,则生成该ReadView的m_ids为[18, 27]

  2. min_trx_id :表示在生成 ReadView 时当前系统中活跃的读写事务中最小的事务id ,也就是 m_ids中的最小值。上面的例子,则min_trx_id 为 18

  3. max_trx_id :表示生成 ReadView 时系统中应该分配给下一个事务的id值。在生成此ReadView之后,系统创建的第一个事务id即为max_trx_id。它与m_ids并没有任何关系,因为在m_ids之后可能会有已提交的事务。

  4. creator_trx_id : 表示生成该ReadView的事务的事务id。PS: 只有在对表中的记录做改动时(执行INSERT、DELETE、UPDATE这些语句时)才会 为事务分配事务id,否则在一个只读事务中的事务id值都默认为0。

ReadView如何实现各种隔离级别

读未提交(READ-UNCOMMITTED)、串行化(SERIALIZABLE) 隔离级别并没有使用到ReadView。读未提交,直接读取最新数据即可;串行化通过锁的机制实现。

读已提交(READ-COMMITTED)

读已提交,能读取到事务已经提交的数据。换句话说,我们需要过滤未提交事务修改的数据。MySQL是这样实现的:在每次读取数据之前,生成一个ReadView,此时记录活跃的事务id列表(m_ids),最小的事务id(min_trx_id),最大的事务id(max_trx_id),当前事务id(creator_trx_id)。

当读取到每行数据时,根据隐藏的字段 DB_TRX_ID 来判断当前数据行是否可读。判断规则:

  1. 当 DB_TRX_ID 小于 min_trx_id,则说明该行数据是在生成ReadView之前已提交的,所以其是可读的。
  2. 当 DB_TRX_ID 等于 creator_trx_id,说明该行数据是同一个事务更新或插入的,所以其是可读的
  3. 当 DB_TRX_ID 在 m_ids列表中,说明其他活跃事务正在操作该行数据,属于未提交状态,所以其是不可读的。
  4. 当 DB_TRX_ID 大于 max_trx_id,说明其在该ReadView之后其他事务生成的,所以其是不可读的。
  5. 当 DB_TRX_ID 小于 max_trx_id 且不在 m_ids列表中,说明操作该数据的事务是已提交的(不是活跃的),所以其是可读的。

事务隔离,只是隔离其他正在进行中的事务操作的相关数据,在这之前的数据也应该正常读取。上面所说的不可读,只是说当前操作的数据不可读,MySQL还会根据每行的回滚指针DB_ROLL_PTR,找到之前版本的数据,再根据这个规则判断是否可读,以此类推,直接找到符合规则的数据或者不存在记录。

以上,就是读已提交隔离级别的实现原理。

可重复读(REPEATABLE-READ)

可重复读,对同一字段的多次读取结果都是一致的,除非数据是被本身事务自己所修改。其原理跟「读已提交」差不多,最大的区别就是ReadView只在第一次读取时生成,后续的每一次读都不再生成新的ReadView。比如第一次读取数据时,ReadView的m_ids为[18, 27],当读取到的行的事务ID为18,此时该数据不可读的。当事务18提交时,当前事务再次读取数据时,因为其不再生成新的ReadView,事务18还是当成活跃的事务,读取的结果还是不可读(如果是读已提交隔离级别,这次读取的结果变为可读)。这样就达到了可重复读的效果。

MySQL中的可重复读隔离级别解决了不可重复读的问题,同时也解决了幻读的问题(幻读指的是一个事务在前后两次查询同一个范围的时候,后一次查询看到了前一次查询没有看到的行)。原理很简单,前后读取的数据,如果在中间发生了修改,其行记录的事务ID为m_ids列表中或大于max_trx_id,其数据都是不可读的,所以前后读取的数据会保持一致。