事物的隔离主要是为了控制多个事物并发执行时相互间的影响,多个事物可能会在同一时刻访问和修改同一组数据,这会产生各种并发问题,导致数据不一致。通过设置不同的事物隔离级别,数据库可以在并发性能与并发问题之间进行权衡,以适应不同应用场景的需求。
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 主要有以下几个部分组成:
- ACTIVE_TRX_IDS:记录当前系统中所有尚未提交的事务ID。
- LOW_LIMIT_ID:系统中下一个还未开始的事务的ID。
- UP_LIMIT_ID:ACTIVE_TRX_IDS 中的最小值。
- 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,按照如下几条检查规则,可以确定当前查询可以看到哪些版本的数据:
- 如果被访问的版本中 DB_TRX_ID = CREATOR_TRX_ID,则表明当前事物正在访问自己修改的版本,所以该版本对当前事物可见。
- 如果被访问的版本中 DB_TRX_ID < UP_LIMIT_ID,则表明当前访问的版本事物在此 Read View 创建前已经提交,所以该版本对当前事物可见。
- 如果被访问的版本中 DB_TRX_ID >= LOW_LIMIT_ID,则表明当前访问的事物在此 Read View 创建后开启,所以该版本对当前事物不可见。
- 如果被访问的版本中 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 的描述,查询到了可见的版本数据。