MySQL事务与MVCC机制
# MySQL 事务与 MVCC 机制
事务 (Transaction) 是数据库管理系统执行过程中的一个逻辑单位,由一个有限的数据库操作序列构成。它主要用于保证数据的一致性。
# 1. 事务的 ACID 特性
事务具有四个标准属性,通常称为 ACID 特性:
- 原子性 (Atomicity): 事务是一个不可分割的工作单位,事务中的操作要么都发生,要么都不发生。如果事务失败,所有已执行的操作都必须回滚到事务开始前的状态。InnoDB 通过
undo log
来实现原子性。 - 一致性 (Consistency): 事务必须使数据库从一个一致性状态变换到另一个一致性状态。事务执行的结果必须是使数据满足所有的约束(如主键约束、外键约束、业务规则等)。一致性是事务追求的最终目标,原子性、隔离性、持久性都是为了保证一致性。
- 隔离性 (Isolation): 事务的执行不受其他并发事务的干扰。事务隔离分为不同的级别,用于平衡隔离程度和并发性能。InnoDB 通过锁机制和 MVCC 来实现隔离性。
- 持久性 (Durability): 一个事务一旦提交,它对数据库中数据的改变就是永久性的,接下来的其他操作或故障不应对其有任何影响。InnoDB 通过
redo log
来保证持久性。
注意
事务支持是在存储引擎层实现的。并非所有 MySQL 存储引擎都支持事务,例如 MyISAM 就不支持。InnoDB 是支持事务的常用引擎。
# 2. 并发事务带来的问题
在多个事务同时执行时,如果没有有效的隔离机制,可能会出现以下问题:
- 脏读 (Dirty Read): 一个事务读取到了另一个未提交事务修改的数据。如果那个事务最终回滚,那么读取到的数据就是无效的“脏”数据。
- 不可重复读 (Non-Repeatable Read): 在同一个事务内,多次读取同一行数据,结果却不一样。通常是因为在事务执行期间,有其他事务提交了对该行数据的修改。侧重于修改。
- 幻读 (Phantom Read): 在同一个事务内,多次执行相同的范围查询,结果集中的行数不一样。通常是因为在事务执行期间,有其他事务插入或删除了满足查询条件的新行。侧重于新增或删除。
# 3. SQL 标准的事务隔离级别
SQL 标准定义了四种隔离级别,隔离级别越高,数据一致性越好,但并发性能越差:
- 读未提交 (Read Uncommitted):
- 最低级别,允许读取未提交的数据变更(脏读)。
- 基本不使用。
- 读提交 (Read Committed):
- 一个事务只能读取到其他事务已经提交的数据变更。
- 解决了脏读问题。
- 每次
SELECT
都会创建新的快照 (Read View),可能导致不可重复读和幻读。 - Oracle、SQL Server 等数据库的默认隔离级别。
- 可重复读 (Repeatable Read):
- 保证在同一个事务中多次读取相同记录的结果是一致的。
- 解决了脏读和不可重复读问题。
- 事务启动时创建快照 (Read View),整个事务期间都使用这个快照。
- MySQL (InnoDB) 的默认隔离级别。
- 理论上仍可能出现幻读,但 InnoDB 通过间隙锁 (Gap Lock) 机制在一定程度上解决了幻读问题(特别是在当前读场景下)。
- 串行化 (Serializable):
- 最高级别,强制事务串行执行。
- 通过对所有读取的行都加读锁,对写入的行加写锁来实现。
- 解决了脏读、不可重复读和幻读问题。
- 并发性能极差,一般不使用。
隔离级别 | 脏读 | 不可重复读 | 幻读 |
---|---|---|---|
读未提交 | √ | √ | √ |
读提交 | × | √ | √ |
可重复读 (默认) | × | × | △ |
串行化 | × | × | × |
(√: 可能出现, ×: 不会出现, △: InnoDB 通过间隙锁部分解决)
# 4. MySQL 事务启动方式
- 显式启动:
START TRANSACTION
或BEGIN
。- 提交:
COMMIT
。 - 回滚:
ROLLBACK
。
- 自动提交:
- MySQL 默认开启
autocommit
(SELECT @@autocommit;
查看,值为 1 表示开启)。每条 SQL 语句都被当作一个独立的事务自动提交。 - 关闭自动提交:
SET autocommit = 0;
。之后需要手动执行COMMIT
或ROLLBACK
。执行SET autocommit = 1;
可恢复默认行为。
- MySQL 默认开启
# 5. 多版本并发控制 (MVCC)
MVCC (Multi-Version Concurrency Control) 是 InnoDB 引擎用来实现在读提交 (RC) 和可重复读 (RR) 这两种隔离级别下,处理读写冲突时避免加锁,从而提高并发性能的一种机制。它使得在同一时刻,不同事务看到的可能是同一份数据的不同版本。
核心组件:
- 隐藏字段: InnoDB 会为每行数据添加几个隐藏字段:
DB_TRX_ID
: 创建或最后修改该行数据的事务 ID。DB_ROLL_PTR
: 回滚指针,指向该行上一个版本的undo log
记录。DB_ROW_ID
: 如果表没有主键且没有唯一非空索引,InnoDB 会自动生成一个隐藏的行 ID。
- Undo Log:
- 记录数据被修改前的值。用于事务回滚和 MVCC。
- 当事务修改数据时,会将旧版本数据存入
undo log
,并通过DB_ROLL_PTR
指针形成一个版本链。
- Read View (一致性视图):
- 事务进行快照读(普通的
SELECT
语句)时,会基于当前数据库状态生成一个 Read View。它决定了事务能看到哪些版本的数据。 - Read View 主要包含:
m_ids
: 创建 Read View 时,当前活跃(未提交)的事务 ID 列表。min_trx_id
:m_ids
中的最小事务 ID。max_trx_id
: 创建 Read View 时,系统应该分配给下一个事务的 ID (即当前最大事务 ID + 1)。creator_trx_id
: 创建该 Read View 的事务 ID。
- 事务进行快照读(普通的
- 可见性判断规则: 当事务访问某行数据时,会根据该行记录的
DB_TRX_ID
和 Read View 进行比较,判断该版本是否可见:- 如果
DB_TRX_ID
<min_trx_id
:表明该版本是在当前所有活跃事务之前就已经提交的,可见。 - 如果
DB_TRX_ID
>=max_trx_id
:表明该版本是在 Read View 创建之后才开启的事务生成的,不可见。需要沿着undo log
链找到上一个版本,再进行判断。 - 如果
min_trx_id
<=DB_TRX_ID
<max_trx_id
:- 若
DB_TRX_ID
在m_ids
列表中:表明该版本是由 Read View 创建时还活跃的事务生成的,不可见。需要沿着undo log
链找上一个版本。 - 若
DB_TRX_ID
不在m_ids
列表中:表明该版本是由 Read View 创建时已经提交的事务生成的,可见。
- 若
- 如果
DB_TRX_ID
等于creator_trx_id
:表明是当前事务自己修改的,可见。
- 如果
# MVCC 下的隔离级别实现
- 读提交 (RC): 每次
SELECT
语句执行时都会重新生成一个新的 Read View。这导致在同一个事务中,后续的SELECT
可能会看到其他事务在此期间提交的修改,从而产生不可重复读。 - 可重复读 (RR): 事务启动后的第一个
SELECT
语句执行时(或者START TRANSACTION WITH CONSISTENT SNAPSHOT
时)创建 Read View,并且整个事务期间都复用这个 Read View。这保证了事务期间读取到的数据版本总是一致的,避免了不可重复读。
快照读 vs 当前读
- 快照读 (Snapshot Read): 普通的
SELECT
语句(不加锁),读取的是 Read View 决定的数据版本,不加锁。 - 当前读 (Current Read): 读取数据库中最新的、已提交的版本,并对读取的记录加锁,保证其他并发事务不能修改这些记录。常见的当前读语句:
SELECT ... LOCK IN SHARE MODE
(共享锁, S锁)SELECT ... FOR UPDATE
(排他锁, X锁)INSERT
,UPDATE
,DELETE
(排他锁, X锁) 在 RR 级别下,当前读需要使用记录锁 (Record Lock)、间隙锁 (Gap Lock) 或 Next-Key Lock (记录锁 + 间隙锁) 来防止幻读。
上次更新: 6/21/2025