MySQL 事务隔离级别与MVCC | 8lovelife's life
0%

MySQL 事务隔离级别与MVCC

事物的隔离主要是为了控制多个事物并发执行时相互间的影响,多个事物可能会在同一时刻访问和修改同一组数据,这会产生各种并发问题,导致数据不一致。通过设置不同的事物隔离级别,数据库可以在并发性能与并发问题之间进行权衡,以适应不同应用场景的需求。

MySQL 事物隔离级别

SQL标准中定义了四种事物隔离级别,分别为 Read Uncommitted、Read Committed、Repeatable Read、Serializable。MySQL InnoDB 引擎实现了这四种隔离级别,我们先来回顾一下这四种事物隔离级别的特点。

Transaction Isolation Common Concurrency Issues Concurrency Performance
Read Uncommitted Dirty Reads, Non-repeatable Reads, Phantom Reads Highest
Read Committed Non-repeatable Reads, Phantom Reads High
Repeatable Read Phantom Reads Good
Serializable No concurrency issues, fully serialized execution Lowest

隔离级别选择

从以上图表可以发现,Read Uncommitted 的并发性能是最高的,导致的并发问题也是最多的,尤其是会产生脏读,这会导致更多的数据不一致,对于一般系统来说是无法接受的,虽然 Serializable不会产生并发问题,但并发性能是最低的,对于一般系统来说同样是无法接受的,大部分的应用会根据不同的使用场景,在 Read Committed 和 Repeatable Read 两个隔离级别之间作出选择。不同的数据库厂商也有不同的默认隔离级别选择,比如 MySQL InnoDB 默认的隔离级别是 Repeatable Read,而 Oracle 默认的隔离级别是 Read Committed。

MVCC

MVCC 全称 Multiversion Concurrency Control(多版本并发控制),是一种无锁的并发访问技术,最早的 MVCC 阐述可以追溯到 1978 年,一般被用在数据库管理系统中。MySQL InnoDB 提供了 MVCC 的实现,大大提高了数据库并发访问的性能。Read Committed 和 Repeatable Read 能够拥有如此良好的性能,离不开 MVCC 的支持。需要注意的是 Read Uncommitted 和 Serializable 隔离级别不支持 MVCC。

版本快照

MVCC 通过维护每一行数据的变更版本来实现并发控制,每当有事物更改某一行数据的时候,MySQL InnoDB 并不会直接覆盖原始值,而是生成一个新的快照,内容存储在 undo log 文件中。假设现在有 T1、T2、T3 这个三事物尝试更新账户 balance 的值,则会创建如下图所示的版本快照。

Operation balance DB_TRX_ID DB_ROLL_PTR
T3 updates balance = 400 (uncommitted latest record) 400 3
T2 updates balance = 300 (uncommitted) 300 2
T1 updates balance = 200 (uncommitted) 200 1
Initial state 100 0 NULL

版本链

DB_TRX_ID 和 DB_ROLL_PTR 是隐藏的行记录字段,存储在 Record Header(聚簇索引)中,分别表示最近修改该行记录的事物ID和指向该行记录的 undo log(上一个版本数据),下图展示了 T1、T2、T3 事物形成的版本链。

Transaction T3 (DB_TRX_ID = 3, balance = 400, uncommitted) Transaction T2 (DB_TRX_ID = 2, balance = 300, uncommitted) Transaction T1 (DB_TRX_ID = 1, balance = 200, uncommitted) Initial state (DB_TRX_ID = 0, balance = 100)

针对某一行数据修改的并发事物越多,产生的 undo log 也越多,这会占用更多的存储空间,典型的通过空间来换取时间。

Read View

Read View 规定了当前事物在读取数据时,哪些版本可见,哪些版本不可见,这些可见性规则是 MVCC 实现的一部分,Read View 主要有以下几个部分组成:

  1. ACTIVE_TRX_IDS:记录当前系统中所有尚未提交的事务ID。
  2. LOW_LIMIT_ID:系统中下一个还未开始的事务的ID。
  3. UP_LIMIT_ID:ACTIVE_TRX_IDS 中的最小值。
  4. CREATOR_TRX_ID:当前正在执行的事务ID。创建或修改数据的事务会分配事务ID,并将该ID记录为 CREATOR_TRX_ID,如果事物只是查询则事物ID默认为 0。

之前我们有提到 Read Committed 和 Repeatable Read 都支持 MVCC,但他们创建 Read View 的时机是不同的。

  • Read Committed:事物中的每一次查询,都会创建一个 Read View,这意味着多次查询同一条记录可能会看到不同的结果,存在不可重复读的问题。
  • Repeatable Read:整个事物只会创建一个 Read View,这意味着多次查询同一条记录,看到的结果是相同的,解决了不可重复读的问题。

可见性规则

当事务第一次执行 SELECT … FROM table 时,MySQL InnoDB 会创建一个 Read View,按照如下几条检查规则,可以确定当前查询可以看到哪些版本的数据:

  1. 如果被访问的版本中 DB_TRX_ID = CREATOR_TRX_ID,则表明当前事物正在访问自己修改的版本,所以该版本对当前事物可见。
  2. 如果被访问的版本中 DB_TRX_ID < UP_LIMIT_ID,则表明当前访问的版本事物在此 Read View 创建前已经提交,所以该版本对当前事物可见。
  3. 如果被访问的版本中 DB_TRX_ID >= LOW_LIMIT_ID,则表明当前访问的事物在此 Read View 创建后开启,所以该版本对当前事物不可见。
  4. 如果被访问的版本中 UP_LIMIT_ID <= DB_TRX_ID < LOW_LIMIT_ID,则可见性需要按照如下情况决定,如果 DB_TRX_ID 不在 ACTIVE_TRX_IDS 列表中,则说明当前访问的版本已经提交,该版本对当前事物可见,若在 ACTIVE_TRX_IDS 列表中,则说明当前访问的版本未提交,对当前事物不可见。

若经过以上规则判断,版本 DB_TRX_ID 对当前事物不可见,则沿着 DB_TRX_ID 的下一个版本继续进行规则判断,一直到找到当前事物可见的版本。值得注意的是,规则 1 的存在是 Repeatable Read 隔离级别存在幻读问题的一个原因。

Timeline Transaction A Transaction B
T1 START TRANSACTION;
T2 SELECT balance FROM accounts WHERE id = 1; (No record found)
T3 START TRANSACTION;
T4 INSERT INTO accounts (id, balance) VALUES (1, 100);
T5 SELECT balance FROM accounts WHERE id = 1; (No record found, due to Repeatable Read isolation)
T6 COMMIT; (Transaction B commits the new record)
T7 UPDATE accounts SET balance = balance + 50 WHERE id = 1; (1 row affected, current read behavior)
T8 SELECT balance FROM accounts WHERE id = 1; (Reads updated balance of 150)
T9 COMMIT;

事物 A 在 T2 和 T8 时间的两次查询,返回了不同的结果集,产生了幻读问题。原因是事物 A 在 T7 时间更新时发生了当前读,更新到了最新已经被提交的数据,紧接着 T8 又进行了快照读,根据可见性规则 1 的描述,查询到了可见的版本数据。