MySQL 事务
概念
MySQL定义了事务的四大特性:ACID,分别是原子性、一致性、隔离性以及持久性。InnoDB引擎实现了事务的四大特性:
- 原子性:一组操作要么都执行成功,要么都失败
- 隔离性:InnoDB实现了事务的四个隔离级别(读未提交、读已提交、可重读、串行化)
- 持久性:通过redo log和binlog实现了持久化
- 一致性:以上三者都保证之后,就可以保证一致性
原子性
一个事务中的所有操作,要么全部完成,要么全部不完成,不可能停滞在中间某个环节。如果事务在执行过程中发生错误,会被回滚到事务开始前的状态。原子性主要依赖undo log实现,undo log在MySQL主要用于原子性和MVCC实现。
当一个事务在修改数据页中的某行数据之前,首先将这条数据的旧版本信息(修改前的值)复制到 Undo Log 中作为备份,然后再执行真正的写操作,同一个行记录会存在多个undo log,undo log之间通过链表维护一个按修改时间排序的版本链。
事务执行过程中,如果出现异常,会回溯版本链将数据恢复到原本的值。执行成功commit之后,也不会立即删除undo log,用于MVCC的多版本读,最后由 purge 线程清理。
当事务已提交,并且没有其他活跃事务访问该undo log时,由专用的后台处理线程purge线程负责清理undo log数据
隔离性
InnoDB实现了事务的四大隔离级别,事务隔离级别定义了一个事务中所做的修改,在多大程度上对其他并发执行的事务是“可见”的。不同的隔离级别本质上是在数据一致性和并发性能之间做的不同程度的权衡。隔离级别越高,数据一致性越强,并发性越差。
| 隔离级别 (Isolation Level) | 脏读 (Dirty Read) | 不可重复读 (Non-Repeatable Read) | 幻读 (Phantom Read) |
|---|---|---|---|
| READ UNCOMMITTED (读未提交) | √ (可能发生) | √ (可能发生) | √ (可能发生) |
| READ COMMITTED (读已提交) | × (避免) | √ (可能发生) | √ (可能发生) |
| REPEATABLE READ (可重复读) | × (避免) | × (避免) | √ (可能发生) |
| SERIALIZABLE (串行化) | × (避免) | × (避免) | × (避免) |
InnoDB实现的可重读隔离级别通过临键锁避免了幻读
在InnoDB为存储引擎的情况下,事务的隔离级别是通过MVCC+临键锁实现的:
- 读未提交:不需要施加约束条件
- 读已提交:MVCC
- 可重读:MVCC+临键锁
- 串行化:表级锁
MVCC
MVCC也就是多版本并发控制机制,主要基于三个组件实现:行记录的隐藏字段、Undo log以及读视图ReadView。
- 事务在读取行记录时,会先判断行记录的事务ID是否满足当前事务的可见性,如果不满足,通过回滚指针定位undo log版本链进行遍历,直到找到一个满足可见性的版本并返回该版本数据
- 判断一个版本的行记录是否可为当前事务所见,是通过ReadView来判断的,ReadView在可重读场景下大致分为三部分,在事务创建时生成ReadView,生成ReadView时活跃的事务,以及创建于当前事务之后的事务产生的数据都是不可见的,只有在事务开始之前已完成提交的数据是可见的。
在不同的隔离级别下,ReadView的生成时机是不同的,读提交隔离级别下,每一次SELECT命令都会重新生成READVIEW,保证读取最新提交的行记录。而在可重读隔离级别下,只在事务开始时创建READVIEW。
MVCC只能保证快照读场景下的可重读,对于当前读(delete、insert、update、select .. for update或select ... in share都属于当前读)依然存在幻读现象,InnoDB引入临键锁解决可重读场景下的幻读。
InnoDB的可重读隔离级别可以避免幻读,主要基于以下机制:
- 快照读 (Snapshot Read):普通的 SELECT 语句,通过 MVCC 机制实现。事务启动时创建一个数据快照,后续的快照读都读取这个版本的数据,从而避免了看到其他事务新插入的行(幻读)或修改的行(不可重复读)
- 当前读 (Current Read):像 SELECT ... FOR UPDATE, SELECT ... LOCK IN SHARE MODE, INSERT, UPDATE, DELETE 这些操作。InnoDB 使用 Next-Key Lock 来锁定扫描到的索引记录及其间的范围(间隙),防止其他事务在这个范围内插入新的记录,从而避免幻读。Next-Key Lock 是行锁(Record Lock)和间隙锁(Gap Lock)的组合
临键锁
临键锁是 InnoDB 存储引擎在可重复读隔离级别下默认使用的一种行锁算法。它是记录锁和间隙锁的组合,临键锁的主要目的是为了解决幻读问题。
- 对唯一索引进行等值查询:
- 当记录存在时,退化为记录锁。
- 当记录不存在时,退化为间隙锁
- 对普通索引进行等值查询:
- 命中记录时,除了给命中的索引记录加临键锁,还会对这些记录对应的主键索引加记录锁(因为回表需要)
- 还会对下一个值加间隙锁,以确保范围
- 对任何索引进行范围查询:
- 会加临键锁,锁住整个访问到的范围,直到遇到不满足条件的第一个记录为止
读已提交隔离级别允许幻读,因此不需要临键锁,只需要MVCC保证看到已提交的事务数据
持久性
InnoDB的持久性主要通过redo log和binlog共同实现,其中binlog是跨存储引擎的。MySQL 实例挂了或宕机了,重启时,InnoDB 存储引擎会使用 redo log 恢复数据,保证数据的持久性与完整性
| 持久化机制 |
|---|
![]() |
MySQL 中数据是以页为单位,你查询一条记录,会从硬盘把一页的数据加载出来,加载出来的数据叫数据页,会放入到 Buffer Pool 中,之后的读写都是面向Buffer Pool读写。InnoDB采用Write-Ahead Logging (WAL,预写式日志) 技术,即在数据页的修改被刷写到磁盘之前,必须先将对应的日志记录写到持久化的日志文件中。Redo log记录的是物理逻辑日志,即“在某个数据页上做了什么修改”,事务直接修改Buffer Pool中的数据,并记录日志到Redo log buffer,再提交事务时,将日志刷新到磁盘。
为什么不直接刷数据,而是要额外采用一个日志呢?
- 写数据属于随机写,而写日志是顺序写,性能更高
- 每次的写数据量可能只有几行,刷盘时却要刷整个页,性能很差,所以使用日志记录,当写的数据量累积之后一次性刷盘
Redo log
- 0:表示每次提交事务不进行刷盘操作,而是由后台线程每隔1秒定时刷盘,性能最差,如果MySQL宕机会丢失1s内的数据
- 1:表示每次提交事务都刷盘,最安全,但是性能最差
- 2:表示每次提交事务时只将数据write到page cache,由操作系统决定何时刷盘,这种如果是MySQL服务宕机不会有数据丢失,但是机器宕机会丢失
| redo log刷盘 |
|---|
![]() |
Redo Log Buffer的大小是有限的,采用日志文件组循环写的方式,日志文件组包含32个文件,循环写,当日志文件组被写满,会触发刷盘,将redo log对应的数据修改刷到磁盘。
| 日志文件组循环写 |
|---|
![]() |
binlog
binlog是逻辑日志,记录内容是语句的原始逻辑,属于MySQL Server层。主要用于数据备份、主从同步等场景。binlog的记录格式有三种:
- statement:记录原始的执行命令,但是对于某些函数比如NOW()获取当前时间的函数,主从同步后会产生数据不一致
- row:直接记录数据修改,但是空间占用会更高
- mixed:混合模式,对于可能造成数据不一致的语句,使用row,否则使用statement
binlog刷盘策略:
- 0:表示每次只写入page cache,OS自动判断何时刷盘
- 1:表示提交事务时同步刷盘,安全性最高
- 2:每N个事务进行一次刷盘,安全性最差
对于安全性要求高的场景,使用双1配置,能够保证最高级别的安全性
两阶段提交
为了保证数据库具有崩溃恢复能力,InnoDB采用两阶段提交,保证数据不会丢失:
- InnoDB 将事务的 Redo Log 写入,并将事务标记为 PREPARE 状态
- Server 层将事务的 Binlog 写入并刷盘
- InnoDB 将事务的 Redo Log 标记为 COMMIT 状态
崩溃恢复:在数据库重启后,会检查所有处于 PREPARE 状态的 Redo Log 事务,并去核对 Binlog
- 如果发现该事务的 Binlog 是完整的(已在步骤2刷盘),则提交(Commit)该事务。
- 如果发现该事务的 Binlog 不完整(步骤2失败),则回滚(Rollback)该事务。
这样就保证了两种日志的最终一致性
一个事务提交时的完整持久化流程(以最安全的双1配置为例)可以概括为:
- 数据修改:事务在 InnoDB Buffer Pool 中修改数据页,产生“脏页”。
- 写 Redo Log (Prepare):将事务的 Redo Log 写入 Redo Log Buffer,并刷盘,标记为 PREPARE 状态。
- 写 Binlog:将事务的 Binlog 写入 Binlog Cache,并刷盘到 Binlog 文件。
- 写 Redo Log (Commit):将事务的 Redo Log 标记为 COMMIT 状态
- 返回成功:向客户端返回事务提交成功。
- 后台刷脏页:由后台线程在合适的时机(如 Checkpoint)将 Buffer Pool 中的“脏页”刷回到磁盘的数据文件中
| 日志类型 | 所属层 | 作用 | 是否可选 |
|---|---|---|---|
| Redo Log(重做日志) | InnoDB 存储引擎层 | 保证事务的持久性(Durability)和崩溃恢复(Crash Recovery) | 必须开启(InnoDB 核心机制) |
| Binlog(二进制日志) | MySQL Server 层 | 用于主从复制(Replication)、数据备份与恢复(Point-in-Time Recovery) | 可关闭(但生产环境通常开启) |
为什么要通过两个日志实现数据的持久化,一个不行吗?
如果只有Redo Log,通过 WAL 机制可以保证数据的崩溃恢复,但MySQL在生产环境会有从库,从库的数据同步通过binlog完成,redo log只能保证自身数据的完整性,binlog保证从库数据的完整性。需要通过两阶段提交保证两个日志的数据一致性
注意!!!! binlog只有在事务被提交后才能进行刷盘,否则会出现主从库数据不一致的现象,已同步给从库的事务数据在主库发生了回滚,导致主从数据不一致。而redo log可以随时写入,即使事务回滚,事务在回滚时会根据undo log执行反向操作,这个反向操作也会被记录到redo log来保证数据回到之前的版本。
那为什么不能通过undo log的反向操作同步给从库,让从库同步删除来保证binlog即使在事务未被提交写入从库的情况下导致的数据不一致呢?
redo log和undo log都是InnoDB实现的,而binlog和主从复制机制是Server层实现的,undo log不能够直接回滚binlog同步的数据。




浙公网安备 33010602011771号