mysql事务是怎么实现的
注意:这里忽略了binlog
binlog 的作用:
- 主从复制:在主从复制架构中,主库的 binlog 记录了所有引起数据变更的事务操作。从库通过读取主库的 binlog 并在自己的数据库上重放这些事务,实现与主库的数据同步。当主库宕机恢复后,从库可以继续从主库获取新的 binlog 事件,保持数据一致。
- 崩溃恢复和数据一致性:在 MySQL 的某些存储引擎(如 InnoDB)中,为了保证 binlog 和 redo log 的一致性,引入了两阶段提交(Two-Phase Commit,2PC)协议。在事务提交过程中,先将 redo log 写入并处于 prepare 状态,然后将 binlog 写入,最后将 redo log 状态改为 commit。如果在这个过程中数据库宕机,重启时会检查 binlog 和 redo log 的状态,根据两阶段提交协议来决定事务是否需要提交或回滚,以保证数据的一致性。
在MySQL中,事务的实现主要依赖于InnoDB引擎(唯一支持完整事务特性的存储引擎),通过日志系统(undo/redo日志)、锁机制、MVCC(多版本并发控制)等核心技术,实现ACID特性。以下是详细实现原理:
一、ACID特性的实现基础
1. 原子性(Atomicity):undo日志回滚
- undo日志:记录事务对数据的反向操作(如删除前的旧值、更新前的旧值),用于回滚事务。
- 每个事务开始时,InnoDB为其分配一个事务ID(DB_TRX_ID),并在修改数据前将旧值写入undo日志。
- 若事务回滚,InnoDB根据undo日志恢复数据到事务开始前的状态。
- 示例:
undo日志记录旧的UPDATE users SET balance = balance - 100 WHERE id = 1; -- 扣款操作
balance
值(如500),若事务回滚,直接将balance
恢复为500。
2. 持久性(Durability):redo日志持久化
- redo日志(重做日志):记录事务对数据的正向修改,用于故障恢复(即使数据库崩溃,重启后可通过redo日志重建未提交的数据)。
- WAL(Write-Ahead Logging)机制:
- 事务提交时,先将redo日志写入磁盘(通过
fsync
确保落盘),再更新数据页(可能延迟写入磁盘)。 - redo日志包含LSN(日志序列号),用于标记日志的写入位置,确保恢复时按顺序应用。
- 事务提交时,先将redo日志写入磁盘(通过
3. 隔离性(Isolation):锁与MVCC
- 锁机制:
- 写锁(X锁):独占数据修改,阻止其他事务读写(解决写-写、写-读冲突)。
- 读锁(S锁):共享读,阻止写锁(解决读-写冲突)。
- 间隙锁(Gap Lock):在可重复读隔离级别下,阻止幻读(锁定索引间隙,防止插入新数据)。
- MVCC(多版本并发控制):
- 为每行数据维护多个版本(通过隐藏字段
DB_TRX_ID
和DB_ROLL_PTR
),快照读(普通SELECT
)通过Read View访问历史版本,避免锁竞争。 Read View
包含当前活跃事务ID列表,判断数据版本是否可见(未提交的版本对当前事务不可见)。
- 为每行数据维护多个版本(通过隐藏字段
4. 一致性(Consistency):ACID协同保障
- 一致性是最终目标,依赖原子性(确保单事务正确)、隔离性(确保事务间不干扰)、持久性(确保修改持久化)共同实现,同时结合数据库约束(如唯一索引、外键)。
二、事务的生命周期与关键步骤
1. 事务启动
- 通过
START TRANSACTION
或BEGIN
显式启动,或隐式启动(如DML语句自动开启事务)。 - InnoDB分配唯一的事务ID(全局递增,保证顺序性)。
2. 事务执行(DML操作)
以UPDATE
操作为例:
- 生成操作日志:
- 记录
undo日志
(旧值)和redo日志
(新值)。 - 示例:更新
balance
从500到400,undo日志记录(id=1, balance=500)
,redo日志记录(id=1, balance=400)
。
- 记录
- 修改内存数据:
- 在内存中的数据页(Buffer Pool)中更新
balance
为400,标记为“脏页”。
- 在内存中的数据页(Buffer Pool)中更新
- 锁处理:
- 对更新的行加X锁(排他锁),阻止其他事务修改。
3. 事务提交(COMMIT)
- 写入redo日志:
- 将redo日志从缓存写入磁盘(
fsync
确保持久化),事务状态标记为“已提交”。
- 将redo日志从缓存写入磁盘(
- 释放锁:
- 释放事务持有的所有锁(X锁/S锁),允许其他事务访问数据。
- 异步刷脏页:
- 内存中的脏页由后台线程逐步刷入磁盘(不阻塞提交,提升性能)。
4. 事务回滚(ROLLBACK)
- 读取undo日志:
- 根据undo日志中的旧值,恢复数据页到事务开始前的状态(如
balance
从400恢复为500)。
- 根据undo日志中的旧值,恢复数据页到事务开始前的状态(如
- 释放锁:
- 释放所有锁,事务结束。
三、关键数据结构与日志格式
1. undo日志的两种类型
- INSERT Undo Log:记录插入操作的反向操作(仅在事务回滚时使用,提交后可删除)。
- UPDATE Undo Log:记录更新/删除操作的旧值(用于MVCC,保留一定时间供其他事务的快照读使用)。
2. redo日志的物理格式
- 包含操作类型(如更新、插入)、数据页地址、修改前后的值等,用于快速重做操作。
- 示例:
[LSN=1000] UPDATE RECORD (page=10, offset=50, old_val=500, new_val=400)
3. MVCC的隐藏字段
每行数据包含:
DB_TRX_ID
:最后一次修改数据的事务ID。DB_ROLL_PTR
:指向undo日志中该版本的前一个版本(形成版本链)。DB_ROW_ID
:隐藏自增主键(若无显式主键)。
四、隔离级别与实现差异
隔离级别 | 核心实现机制 | 锁与MVCC配合 |
---|---|---|
读未提交 | 直接读取最新数据(不使用MVCC) | 写操作加X锁,读操作无锁(允许脏读) |
读已提交 | 每次快照读生成新的Read View(MVCC) | 写操作加X锁,读操作无锁(通过版本控制避免脏读) |
可重复读 | 事务启动时生成Read View(MVCC) | 写操作加X锁,范围查询加间隙锁(防幻读) |
串行化 | 完全依赖锁(S/X锁),禁用MVCC | 读加S锁,写加X锁,读写互斥(串行执行) |
五、故障恢复机制
- 崩溃恢复(Crash Recovery):
- 数据库重启时,InnoDB通过redo日志重做未提交的事务(已写入redo日志但未刷脏页的操作)。
- 通过undo日志回滚未提交的事务(事务ID未在系统表中标记为提交)。
- ACID的最后保障:
- 即使发生断电等故障,redo日志确保已提交事务的数据持久化,undo日志确保未提交事务回滚。
六、总结:事务实现的核心逻辑
- 原子性:undo日志记录反向操作,回滚时恢复数据。
- 持久性:redo日志通过WAL机制先于数据落盘,确保不丢失。
- 隔离性:锁机制解决写冲突,MVCC实现无锁快照读,共同满足隔离级别。
- 一致性:通过上述机制及数据库约束,确保事务前后数据合法。
一句话核心:
MySQL事务通过undo日志实现回滚(原子性)、redo日志实现持久化(持久性)、锁与MVCC实现隔离性,三者协同工作,结合数据库约束,最终保障数据一致性。