MySQL事务管理
一、 MySQL 事务生命周期概览
1.1、总体概览
1. **开始事务**:通过 `BEGIN`、`START TRANSACTION` 或 `SET autocommit = 0` 开启事务 2. **执行 DML 操作**:执行多条数据修改语句(如 `INSERT`、`UPDATE`、`DELETE`) 3. **日志记录**:InnoDB 会分别写入 Undo Log(用于回滚)与 Redo Log(用于崩溃恢复) 4. **提交或回滚**: - `COMMIT`:写入 Redo Log 并刷新磁盘,正式提交 - `ROLLBACK`:读取 Undo Log,撤销所有操作 5. **崩溃恢复(异常场景)**:系统崩溃时,InnoDB 启动后通过 Redo Log 重做已提交事务、通过 Undo Log 回滚未提交事务1.2、事务隔离级别
1.2.1、什么是读未提交?
- 这是最低的事务隔离级别。在这个级别下,一个事务可以看到其他事务未提交的更改。例如,假设在银行系统中,用户 A 查看自己账户余额,此时用户 B 对账户进行转账操作但尚未提交。在读未提交的隔离级别下,用户 A 可能会看到这个未提交的转账后的余额,这可能会导致数据的不准确和混乱。 - **怎么实现的??** 读未提交通过行锁共享锁确保一个事务在更新行数据但没有提交的情况下,其他事务不能更新该行数据,但不会阻止脏读,意味着事务2 可以在事务1 提交之前读取到事务1 修改的数据。1.2.2、什么是读已提交?
- 这是最低的事务隔离级别。在这个隔离级别下,一个事务只能看到其他事务已经提交的更改。它避免了读未提交中的脏读问题。例如,在图书馆借阅系统中,读者 A 查看某本书的状态为可借阅。此时读者 B 借出了这本书并且事务已经提交,那么读者 A 再次查看时会看到这本书的状态为已借出,不会读取到未提交的中间状态。 - **怎么实现的??** 读已提交会在更新数据前加行级排他锁,不允许其他事务写入或者读取未提交的数据,也就意味着事务2 不能在事务 1 提交之前读取到事务1 修改的数据,从而解决脏读的问题。另外,读已提交会在每次读取数据前都生成一个新的 ReadView,所以会出现不可重复读的问题。1.2.3、什么是可重复读?
- 可重复读是比读已提交更严格的隔离级别。它确保在一个事务中,多次读取同一数据的结果是一致的,避免了不可重复读问题。例如,在学生选课系统中,事务 1 在同一个事务内多次查询某门课程的剩余名额,其他事务在这个事务期间不能修改该课程的剩余名额。如果初始剩余名额是 10,事务 1 内不管查询多少次都是 10,直到事务 1 提交后,其他事务才可以进行修改。 - **怎么实现的??** 可重复读只在第一次读操作时生成 ReadView,后续读操作都会使用这个 ReadView,从而避免不可重复读的问题。另外,对于当前读操作,可重复读会通过临键锁来锁住当前行和前间隙,防止其他事务在这个范围内插入数据,从而避免幻读的问题。1.2.4、什么是串行化?
- 这是最高的事务隔离级别。它通过强制事务串行执行,避免了所有并发事务产生的问题,如脏读、不可重复读和幻读。这意味着事务必须一个接一个地执行,不能同时进行。例如,在一个在线售票系统中,对于一张热门车票的购买,系统会将多个购买请求事务串行处理,确保每个事务在独立的环境中进行,保证数据的完整性和准确性。1.3、数据不一致问题
1.3.1、什么是脏读?
当一个事务读取了另一个事务未提交的更改,如果这个未提交的更改被回滚,那么第一个事务所读取到的数据是无效的、不一致的。例如,在库存管理系统中,事务 1 将产品数量从 100 改为 50 但尚未提交,事务 2 读取到了这个 50 的数量。如果事务 1 因为某些原因回滚,将数量改回 100,事务 2 所读取的 50 就是一个脏数据。
1.3.2、什么是不可重复读?
在一个事务中,对同一数据多次读取得到不同的结果。这通常是由于在两次读取之间,其他事务对该数据进行了修改并提交。例如,在一个在线购物系统中,用户在购物车页面查看商品价格,此时商品价格是 100 元。在用户结算过程中,其他商家修改了该商品价格为 120 元并且提交事务。如果购物车页面没有及时更新价格,用户可能会按照 100 元结算,这就是不可重复读。
1.3.3、什么是幻读?
幻读是指在同一个事务中,多次查询相同条件的数据范围,但返回的结果集不同。这是因为在两次查询之间,其他事务在该范围内插入了新的数据。例如,在一个数据库表中存储文章信息,事务 1 首先查询所有标题为 “技术” 的文章,共有 10 篇。在事务 1 还未完成的情况下,事务 2 插入了一篇标题为 “技术” 的新文章并提交。当事务 1 再次查询标题为 “技术” 的文章时,会得到 11 篇,就好像出现了幻影一样。
快照读通过MVCC解决幻读,当前读通过临键锁解决幻读。在可重复读的情况下,一定程度上解决了幻读问题,但没完全解决。
**不同隔离级别的对比**
| **隔离级别** | **脏读** | **不可重复读** | **幻读** | **实现机制** | | --- | --- | --- | --- | --- | | 读未提交 (RU) | ✅ | ✅ | ✅ | 无隔离措施 | | 读已提交 (RC) | ❌ | ✅ | ✅ | MVCC(每次读新快照) | | 可重复读 (RR) | ❌ | ❌ | ⚠️ | MVCC(首次快照)+ 间隙锁 | | 串行化 (Serial) | ❌ | ❌ | ❌ | 所有操作加锁 |⚠️ 注:MySQL的RR级别在当前读(如**<font style="color:rgb(64, 64, 64);background-color:rgb(236, 236, 236);">SELECT ... FOR UPDATE</font>**
)时可能幻读,但快照读通过MVCC避免。
1.3、考考你呢
+ MySQL 事务是什么,默认隔离级别,什么是可重复读? + 说一下事务的四大隔离级别,分别解决什么问题? + MySQL 默认隔离级别? + 说说 MySQL 事务的隔离级别,如何实现? + Mysql隔离机制有哪些?怎么实现的?可串行化是怎么避免的三个事务问题?二、数据库四大特性
数据库事务的四大特性(ACID)分别是:原子性、一致性、隔离性、持久性。- 原子性:事务是不可分割的最小单元,要么全部成功,要么全部失败。
- 一致性:事务的执行必须使数据库从一个满足预定规则(完整性约束、业务逻辑)的一致性状态,转换到另一个同样满足这些规则的一致性状态。无论事务成功还是失败,数据库都不能违反这些规则(如主键唯一、外键约束、数据类型、账户总额不变等)。
- 隔离性:数据库提供的隔离机制,保证事务在不受外部并发操作影响的独立环境下运行。
- 持久性:事务一旦提交或回滚,对数据库中数据的影响是永久的。
总体的四大特性都说了,下面分开逐个击破。
2.1、原子性
原子性即事务是不可分割的最小单元,要么全部成功,要么全部失败。MySQL 使用 Undo Log 实现事务的原子性。Undo Log 是逻辑日志,记录每一条 DML 操作的反操作。
INSERT
操作记录对应的DELETE
UPDATE
操作记录更新前的旧值DELETE
操作记录原始完整记录以便恢复
如何保证原子性
MySQL通过undo log来保证事务的原子性,当你对数据库的数据进行修改时,首先会记录一条undo日志。当你的事务执行失败的要回滚的时候,就会逆序读取undo中的数据,然后回退到之前的数据,
事务回滚的基本流程:
2.2、持久性
事务一旦提交或回滚,对数据库中数据的影响是永久的。如何保证持久性?
- 写入 Redo Log Buffer: 当事务执行数据修改操作(如 INSERT/UPDATE/DELETE)时,这些修改首先会作用于内存中的数据页副本(位于 Buffer Pool),并同步生成描述物理变更的 Redo Log Record。这些记录被暂存到内存的 Redo Log Buffer 中。
- Redo Log 刷盘(关键步骤):
- 后台线程定期刷盘: 有专门的 InnoDB 后台线程会周期性地将 Redo Log Buffer 中的内容刷新到磁盘上的 Redo Log Files(如
**<font style="color:rgb(64, 64, 64);background-color:rgb(236, 236, 236);">ib_logfile0</font>**
,**<font style="color:rgb(64, 64, 64);background-color:rgb(236, 236, 236);">ib_logfile1</font>**
)。 - 事务提交时强制刷盘(Force-Log-at-Commit): 最关键的是,当一个事务执行
**<font style="color:rgb(64, 64, 64);background-color:rgb(236, 236, 236);">COMMIT</font>**
时,InnoDB 会立即将该事务产生的所有 Redo Log Records 从 Redo Log Buffer 强制刷新(Flush)到磁盘上的 Redo Log Files 中。只有这次磁盘写入确认完成,**<font style="color:rgb(64, 64, 64);background-color:rgb(236, 236, 236);">COMMIT</font>**
操作才会向客户端返回成功。 这一步是持久性的核心保证,确保了即使系统随后崩溃,已提交事务的修改也不会丢失。
- 后台线程定期刷盘: 有专门的 InnoDB 后台线程会周期性地将 Redo Log Buffer 中的内容刷新到磁盘上的 Redo Log Files(如
- 脏页与异步刷盘: 被修改的内存数据页称为 脏页(Dirty Page)。这些脏页由独立的 InnoDB 后台线程在后续某个时间点异步地刷新到磁盘的数据文件(.ibd 文件)中。事务提交成功并不需要等待脏页落盘。
- Checkpoint 机制: Checkpoint 是一个关键的协调点。它定期执行,主要做两件事:
- 标记一个 日志序列号(LSN),表示在此 LSN 之前生成的所有 Redo Log 所对应的数据修改,其脏页已经被刷新到了磁盘数据文件中。
- 允许安全覆盖 Checkpoint LSN 之前的 Redo Log 文件空间(因为对应的修改已持久化到数据文件)。
- 崩溃恢复: 当 MySQL 因崩溃重启时,InnoDB 会自动进行恢复:
- 重做(Redo): 首先,引擎会读取 Redo Log Files。对于所有在崩溃前已经成功提交的事务(其 Redo Log 已落盘),InnoDB 会重放(Apply) 这些 Redo Log Records,将数据页恢复到崩溃前提交完成的状态。这确保了已提交事务的持久性。
- 回滚(Undo): 然后,对于崩溃时尚未提交的事务(即使它们的部分修改可能已写入 Redo Log 或脏页),InnoDB 会利用 Undo Log 来回滚这些未完成事务所做的修改。这确保了数据库最终恢复到只包含已提交修改的一致性状态。
- Write-Ahead Logging (WAL): 指预写日志,先写日志再写数据,即在对数据进行任何修改之前,必须先将修改的日志记录(redo log)持久化到磁盘
需要用到redo log,你对事务的修改操作,内存中的redo log buffer之中,然后会有后台进程定期的将redolog buffer中的数据刷到磁盘当中,让然,当任务提交的时候,redologbuffer中的数据也会强制刷到磁盘中,内存中的数据修改后称之为脏页,脏页由后台线程异步刷入到磁盘中,Checkpoint 定期标记哪些修改对应的脏页已刷盘,并允许覆盖其之前的 Redo Log。redolog日志写入磁盘,这个事务就算是提交成功了。当 MySQL 崩溃重启时,会先检查 Redo Log。对于已提交的事务,MySQL 会重放 Redo Log 中的记录。对于未提交的事务,MySQL 会通过 Undo Log 回滚这些修改,确保数据恢复到崩溃前的一致性状态。
———————————————————————————————————–
“因为事务提交时只保证了 Redo Log 落盘,而内存中脏页(包含最新数据)可能还没来得及刷到磁盘数据文件(.ibd)中。所以数据库崩溃重启后,磁盘数据文件里的数据是旧的(未包含那些只存在于脏页或 Checkpoint 之后提交的修改)。
MySQL 重启恢复时,会读取 Redo Log 文件。Redo Log 里记录的不是完整的新数据,而是描述如何物理修改数据页的指令(比如‘在A页的B位置写入C字节’)。
恢复引擎会根据 Checkpoint 找到起点,然后按顺序重放 (Redo) 后续所有的 Redo Log 记录。对于每条记录,它加载对应的磁盘数据页到内存,检查是否需要应用(根据 LSN),如果需要,就严格按照记录里的指令在内存页的指定位置修改指定的字节。
这个过程把磁盘上的旧数据页,在内存中一步步‘重做’(Redo)成了崩溃前应该有的最新状态(包含已提交和部分未提交事务的修改)。最后,再通过 Undo 回滚未提交事务的修改,并将最终正确的脏页刷盘,从而保证数据一致性和持久性。”
2.3、隔离性
数据库系统提供的隔离机制,保证事务在不受外部并发操作影响的独立环 境下运行。如何保证隔离性?
隔离性指:数据库系统提供的隔离机制,保证事务在不受外部并发操作影响的独立环境下运行。
通过MVCC机制(多版本并发控制)+锁机制来保证吧。
一些基本概念:
1、当前读:读取的是记录的最新记录(一行数据就是一条记录),读取时还要保证其它并发事务不能修改当前记录,会对读取的记录加锁。
例如:select ……. lock in share mode(共享锁), select …… for update ,update,insert,delete(这些上的都是排他锁),这些都是当前读。
2、快照读:简单的select(不加锁)就是快照读,读取的是记录数据的可见版本,可能是历史数据,不会加锁,是非阻塞读。
- 读已提交:每次select都会生成一个快照
- 可重复读:开始事务后,第一个select语句才是快照读的地方(后续在查询的时候,查的就是前面那个快照的数据)
- 串行化:快照读变成当前读
2.3.1、MVCC
多版本并发控制。就是说维护一个数据的多个版本,**让读操作可以访问旧版本,而写操作创建新版本,从而实现读写操作互不阻塞(无冲突)**。2.3.2、三个隐藏字段
1. **DB_TRX_TD:最近修改事务id,记录插入这条记录或最后一次修改改记录的事务id(存储引擎会自动赋值)** 2. **DB_ROLL_PTR:回滚指针,指向这条记录的上一个版本,用于配合undo log,指向上一个版本** 3. **DB_ROW_ID:隐藏主键,如果表结构没有指定主键,将还会生成该隐藏字段,并非每张表都有。**前连个隐藏字段都会加上,最后一个字段加不加取决于当前表有没有主键,有主键就不会加。
假设有一张表原始数据是:
同时有四个并发事务同时访问这张表
第二步
第三步:
可以发现,不同事务或相同事务对同一条记录进行修改,会导致该记录的undolog生成一条记录版本链表,链表的头部是最新的旧记录,链表尾部是最早的旧记录。
查询的时候,到底返回哪个版本呢?由readview决定。
2.3.3、ReadView
ReadView(读视图)是 快照读 SQL执行时MVCC提取数据的依据,记录并维护系统当前活跃的事务(未提交的)id。readview有四个核心字段:
2.3.4、版本链数据的访问规则
不同的隔离级别,生成ReadView的时机不同:
- READ COMMITTED :在事务中每一次执行快照读时生成ReadView。
- REPEATABLE READ:仅在事务中第一次执行快照读时生成ReadView,后续复用该ReadView。
原理分析
RC(读已提交)隔离级别–每次select都会生成一个快照
以上图事务5为例,两次查询,就对应两个快照。
以下是第一次读的过程:
在进行匹配时,会从undo log的版本链,从上到下进行挨个匹配:
- 先匹配
这条记录,这条记录对应的trx_id为4,也就是将4带入右侧的匹配规则中。①不满足②不满足③不满足④也不满足,都不满足,则继续匹配undo log版本链的下一条。
- 再匹配第二条
这条记录对应的trx_id为3,也就是将3带入右侧的匹配规则中。①不满足②不满足③不满足④也不满足,都不满足,则继续匹配undo log版本链的下一条。
- 再匹配第三条
这条记录对应的trx_id为2,也就是将2带入右侧的匹配规则中。①不满足②满足终止匹配,此次快照读,返回的数据就是版本链中记录的这条数据。
第二次读的过程:
在进行匹配时,会从undo log的版本链,从上到下进行挨个匹配:
- 先匹配
这条记录,这条记录对应的trx_id为4,也就是将4带入右侧的匹配规则中。①不满足②不满足③不满足④也不满足,都不满足,则继续匹配undo log版本链的下一条。
- 再匹配第二条
这条记录对应的trx_id为3,也就是将3带入右侧的匹配规则中。①不满足②满足。终止匹配,此次快照读,返回的数据就是版本链中记录的这条数据。
RR(可重复读)隔离级别–仅在事务中第一次执行快照读时生成快照后续复用该快照
我们看到,在RR隔离级别下,只是在事务中第一次快照读时生成ReadView,后续都是复用该
ReadView,那么既然ReadView都一样,ReadView的版本链匹配规则也一样,那么最终快照读返
回的结果也是一样的。所以呢,MVCC的实现原理就是通过InnoDB表的隐藏字段、UndoLog 版本链、ReadView来实现的。
而MVCC + 锁,则实现了事务的隔离性。而一致性则是由redolog 与undolog保证。
两种总结对比一下
特性 | 当前读 | 快照读 |
---|---|---|
是否加锁 | 是(会加行锁) | 否(非阻塞读) |
是否使用 ReadView | 否 | 是 |
典型 SQL | <font style="color:#34495e;">SELECT ... FOR UPDATE</font> |
<font style="color:#34495e;">SELECT ...</font> (无锁) |
适用场景 | 一致性强,保证更新数据 | 高并发读,性能好 |
版本链是指不同事物或同一事物对同一记录进行修改。