本文旨在系统性地阐述MySQL中的事务机制、并发控制原理及其核心实现。内容涵盖事务的自动提交设置、并发读写可能引发的数据一致性问题、四种标准隔离级别的原理与差异,以及支撑事务四大特性(ACID)的关键技术,如MVCC、Undo Log、Redo Log和两阶段提交协议。
事务基础:连接与自动提交
在MySQL中,事务的生命周期与数据库连接(Connection)紧密绑定。
autoCommit = true:在此模式下,每一条SQL语句都会被视为一个独立的事务,并在执行后自动提交。
autoCommit = false:在此模式下,需要手动调用commit()来提交事务。在该连接上、手动提交之前执行的所有数据库操作,都将属于同一个事务。
并发事务的挑战与隔离级别
在并发环境下,写-写操作通过行锁、间隙锁等机制可以避免,但读-写并发则会引发经典的数据一致性问题:脏读、不可重复读和幻读。为解决这些问题,SQL标准定义了四种事务隔离级别。
需要明确的是,MVCC(多版本并发控制)是所有隔离级别都具备的底层机制。它使得“读”操作(通常是快照读)无需加锁,而是通过读取数据行的历史版本来实现非阻塞读。
- 读未提交 (Read Uncommitted):事务可以读取到其他未提交事务修改的MVCC版本数据。存在脏读、不可重复读、幻读问题。
- 读已提交 (Read Committed):事务只能读取到其他已提交事务的MVCC版本数据。解决了脏读,但存在不可重复读和幻读问题。
- 可重复读 (Repeatable Read,MySQL默认级别):事务只能读取到其事务ID之前已提交的MVCC版本数据。通过MVCC解决了快照读下的幻读问题;通过间隙锁 (Gap Lock) 实现了当前读下的读写互斥,从而解决了当前读的幻读问题。
- 串行化 (Serializable):通过加表级锁来实现最严格的隔离,所有读写操作均串行化。能解决所有并发问题,但性能开销最大,通常不推荐使用。
当前读 vs. 快照读
- 快照读:基于MVCC,读取符合事务ReadView规则的数据历史版本(通常是普通
SELECT语句)。
- 当前读:直接读取数据行的最新版本,需要加锁(如
SELECT ... FOR UPDATE, SELECT ... LOCK IN SHARE MODE, UPDATE, DELETE, INSERT语句),通过Next-Key Lock(临键锁)保证。
MVCC的核心:ReadView(读视图)
每个事务在启动时(具体时机因隔离级别而异)会生成一个自己的ReadView,用于判断数据版本的可见性。
ReadView的核心结构:
m_ids:生成ReadView时,系统中所有活跃(未提交) 事务ID的列表。
min_trx_id:m_ids中的最小值。
max_trx_id:生成ReadView时,系统应该分配给下一个事务的ID值。
creator_trx_id:生成该ReadView的事务自身的ID。
可见性判断规则:
- 若数据版本的
trx_id < min_trx_id,说明该版本在ReadView创建前就已提交,可见。
- 若数据版本的
trx_id >= max_trx_id,说明该版本是由在ReadView创建之后才启动的事务修改的,不可见。
- 若
min_trx_id <= trx_id < max_trx_id,则进一步判断:
- 若
trx_id在m_ids列表中,说明修改该版本的事务在生成ReadView时仍活跃,不可见。
- 若
trx_id不在m_ids列表中,说明该事务已提交,可见。
- 若数据版本的
trx_id等于creator_trx_id,说明是自己事务修改的,可见。
事务的基石:ACID特性实现原理
- 原子性 (Atomicity):通过Undo Log(回滚日志) 实现。事务中的任何操作,在Undo Log中都有相反操作的记录。若事务失败回滚,InnoDB会根据Undo Log执行反向操作,恢复到事务开始前的状态。
- 一致性 (Consistency):由应用层、数据库约束和事务机制共同保证。在主从复制场景下,Binlog(二进制日志) 用于保证主从数据一致性。而Binlog与Redo Log之间的一致性,则通过下文介绍的两阶段提交来保障。
- 隔离性 (Isolation):
- 快照读的隔离:由MVCC和Undo Log版本链共同实现,允许多版本并发读写。
- 当前读的隔离:由行锁和间隙锁(合称Next-Key Lock) 实现,通过锁机制达到读写互斥,实现更严格的隔离。
- 持久性 (Durability):主要由Redo Log(重做日志) 和两阶段提交保证。InnoDB的缓冲池(Buffer Pool)机制将数据页(通常16KB)缓存在内存中,并非每次修改都直接写盘。Redo Log记录了数据页的物理变化,可以在数据库异常重启后用于重放操作,确保已提交事务的持久性。关于网络和系统的底层交互,可以参考网络/系统相关内容。
两阶段提交:Redo Log与Binlog的协作
一条SQL语句的执行,特别是涉及事务提交时,其日志落盘流程至关重要。下图展示了在典型“双1”配置下,Redo Log和Binlog的协作过程:

(图示:MySQL事务提交过程中,Redo Log与Binlog的两阶段提交流程)
关键参数与落盘时机:
两阶段状态:
- Prepare阶段:事务提交时,先将Redo Log标记为
PREPARE状态并持久化。
- Commit阶段:接着,将Binlog写入并刷盘。最后,将Redo Log标记为
COMMIT状态。
崩溃恢复:基于日志的数据修复
数据库崩溃重启后,InnoDB会从Redo Log的最后一个检查点(Checkpoint)开始,恢复之后所有状态的记录(包括ACTIVE, PREPARE, COMMITTED)。
ACTIVE状态:事务未提交,使用Undo Log进行回滚。
COMMITTED状态:事务已提交,将对应的修改重放(前滚)到数据页。
PREPARE状态(两阶段提交的第一阶段):需检查Binlog。
- 若Binlog中存在该事务完整记录,则判定事务提交成功,进行重放。
- 若Binlog中无该事务记录,则判定事务提交失败,使用Undo Log回滚。
说明:Redo Log是物理日志,记录“在某个数据页的某个偏移量处,将数据A改为数据B”。Binlog是逻辑日志,记录原始SQL语句或行变化逻辑。
应对页损坏:Double Write机制
InnoDB数据页大小为16KB,而操作系统(如Linux)的页(Page Cache)大小为4KB,磁盘I/O的最小单位是操作系统页。在写入16KB数据页时,可能发生部分写(Partial Page Write) 问题:即只成功写入了其中几个4KB的OS页,导致该16KB的数据库页数据损坏且不可恢复(Redo Log重放需要基于一个完整的正确旧页)。
Double Write(双写缓冲) 机制用于解决此问题:
- 在将脏数据页写入磁盘实际位置前,先将它们连续地写入磁盘上一个名为
doublewrite buffer的公共区域。
- 然后再将数据页写入各自的表空间文件。
- 如果发生崩溃,恢复时可以先从
doublewrite buffer中找到该页的完好副本,用于恢复损坏的页,然后再应用Redo Log。
Binlog的格式选择
- STATEMENT:记录原始的SQL语句。可能导致主从不一致(如SQL中含
RAND(), NOW()等非确定性函数)。
- ROW(推荐):记录每一行数据的变化细节(修改前、修改后的值)。数据一致性最强,但日志量较大。
- MIXED:混合模式。通常记录SQL语句,但当SQL可能引起主从不一致时(如使用了不确定函数),自动切换为ROW格式记录。
MVCC的物理存储:版本链
每行数据记录(Record)后面都隐藏了几个系统字段,其中包括:
DB_TRX_ID:最近一次修改该行的事务ID。
DB_ROLL_PTR:指向该行数据在Undo Log中上一个历史版本记录的指针。
Undo Log中存储了数据行的版本链。当某个事务需要读取数据时,会根据自身ReadView的规则,沿着这个版本链寻找对其可见的、正确版本的数据。更多关于数据库内部机制的深入讨论,可以访问数据库/中间件板块。

(图示:通过DB_TRX_ID和DB_ROLL_PTR形成的MVCC数据版本链)
例如,在REPEATABLE READ级别下,执行DELETE操作并不会立即物理删除数据行,而是将其标记为删除(更新DB_TRX_ID等)。这是因为其他事务可能还需要通过版本链访问该行的旧版本数据。真正的物理清理由后台的Purge线程在合适的时机进行。
|