🧠 概念
事务是一组操作的集合,它是一个不可分割的工作单位。事务会把所有的操作作为一个整体一起向系统提交或撤销操作请求,即这些操作 要么同时成功,要么同时失败。
提示
MySQL会立即隐式的提交事务。默认MySQL的事务是自动提交的,
🔄 事务控制语句
开启事务
| |
提交事务
| |
回滚事务
| |
⚙️ 四大特性
- 原子性(Atomicity):事务是不可分割的最小单元,要么全部成功,要么全部失败
- 一致性(Consistency):事务完成时,必须使所有的数据都保持一致状态
- 隔离性(lsolation):数据库系统提供的隔离机制,保证事务在不受外部并发操作影响的独立环境下运行
- 持久性(Durability):事务一旦提交或回滚,它对数据库中的数据的改变就是永久的
📖 实现原理
- 持久性是通过 redo log (重做日志)来保证的
- 原子性是通过 undo log (回滚日志) 来保证的
- 隔离性是通过 MVCC(多版本并发控制) 或锁机制来保证的
- 一致性则是通过持久性+原子性+隔离性来保证
🧩 MVCC
MVCC(Multi-Version Concurrency Control,多版本并发控制)是一种并发控制机制,允许多个事务同时读取和写入数据库,而无需互相等待,从而提高数据库的并发性能。
在 MVCC 中,数据库为每个事务创建一个数据快照。每当数据被修改时,MySQL 不会立即覆盖原有数据,而是生成新版本的记录。每个记录都保留了对应的版本号或时间戳。
🔗 版本链
在 InnoDB 中,每一行数据都有两个隐藏的列:一个是 DB_TRX_ID ,另一个是 DB_ROLL_PTR 。
DB_TRX_ID:保存创建这个版本的事务 ID。DB_ROLL_PTR:指向 undo 日志记录的指针,这个记录包含了该行的前一个版本的信息。通过这个指针,可以访问到该行数据的历史版本。
当事务更新一行数据时,InnoDB 不会直接覆盖原有数据,而是创建一个新的数据版本,并更新 DB_TRX_ID 和 DB_ROLL_PTR,使得它们指向前一个版本和相关的 undo 日志。这样,老版本的数据不会丢失,可以通过版本链找到。
📊 ReadView
ReadView(读视图)是 InnoDB 为了实现一致性读而创建的数据结构,它用于确定在特定事务中哪些版本的行记录是可见的。可以理解为数据快照
Read View 有四个重要的字段:
m_ids:指的是在创建 Read View 时,当前数据库中 活跃事务 的 事务 id 列表,注意是一个列表,“活跃事务”指的就是,启动了但还没提交的事务。min_trx_id:指的是在创建 Read View 时,当前数据库中 活跃事务 中事务 id 最小的事务 ,也就是m_ids的最小值。max_trx_id:这个并不是m_ids的最大值,而是 创建 Read View 时当前数据库中应该给下一个事务的 id 值 ,也就是全局事务中最大的事务 id 值(m_ids的最大值) + 1;creator_trx_id:指的是 创建该 Read View 的事务的事务 id 。

🔍 可见性实现
- 如果记录的
trx_id值小于 Read View 中的min_trx_id值,表示这个版本的记录是在创建 Read View 前 已经提交的事务生成的,所以该版本的记录对当前事务 可见 。 - 如果记录的
trx_id值大于等于 Read View 中的max_trx_id值,表示这个版本的记录是在创建 Read View 后才启动的事务生成的,所以该版本的记录对当前事务 不可见 。 - 如果记录的
trx_id值在 Read View 的min_trx_id和max_trx_id之间,需要判断trx_id是否在m_ids列表中:- 如果记录的
trx_id在m_ids列表中,表示生成该版本记录的活跃事务依然活跃着(还没提交事务),所以该版本的记录对当前事务不可见。 - 如果记录的
trx_id不在m_ids列表中,表示生成该版本记录的活跃事务已经被提交,所以该版本的记录对当前事务 可见 。
- 如果记录的
⚡ 快照读和当前读
💡 快照读
快照读是指事务在执行查询时,不直接读取当前最新的数据,而是读取数据的历史版本(快照)。MySQL InnoDB 通过多版本并发控制(MVCC)来实现快照读。快照读只会返回在事务开始时可见的数据,即使其他事务在之后修改了这些数据,快照读也不会受影响。
🎯 当前读
MySQL 里除了普通查询是快照读,其他都是 当前读,比如 update、insert、delete,这些语句执行前都会查询最新版本的数据,然后再做进一步的操作。
当前读是指读取数据的最新版本,并且会加锁以确保数据的一致性。即使其他事务对这个数据的修改在当前读的事务之后,这个修改也会反映在当前读的结果中。(如果事务 2 开始时间发生在事务 1 之后,那么正常情况下事务 2 的修改对事务 1 不可见,但是事务 1 的当前读可见这个修改)。
🚨 并发问题
⚠️ 脏读
定义:一个事务读取数据并且对数据进行了修改,这个修改对其他事务来说是可见的,即使当前事务没有提交。这时另外一个事务读取了这个还未提交的数据,但第一个事务突然回滚,导致数据并没有被提交到数据库,那第二个事务读取到的就是脏数据
示例:假设有 A 和 B 这两个事务同时在处理,事务 A 先开始从数据库中读取余额数据,然后再执行更新操作,如果此时事务 A 还没有提交事务,而此时正好事务 B 也从数据库中读取余额数据,那么事务 B 读取到的余额数据是刚才事务 A 更新后的数据,即使没有提交事务。

📝 不可重复读
定义:在同一事务中,读取同一数据两次,但由于其他事务的提交,读取的结果不同。
示例:假设有 A 和 B 这两个事务同时在处理,事务 A 先开始从数据库中读取余额数据,然后继续执行代码逻辑处理,在这过程中如果事务 B 更新了这条数据,并提交了事务,那么当事务 A 再次读取该数据时,就会发现前后两次读到的数据是不一致的,这种现象就被称为不可重复读。

👻 幻读
定义:在一个事务内多次查询某个符合查询条件的记录数量,如果出现前后两次查询到的记录数量不一样的情况,就意味着发生了幻读现象。

提示
幻读和不可重复读的区别
- 幻读 :在事务期间,对于数据总量的突然增加或减少,将别的事务提交的读取到了(针对于数据的数量)
- 不可重复读 :对于同一条数据,在事务执行期间,里面的字段内容发生了变化,读取到了别的事务提交修改的数据。(针对于数据的内容)。
⚖️ 事务隔离级别
📊 四大隔离级别
| 隔离级别 | 脏读 | 不可重复读 | 幻读 |
|---|---|---|---|
| Read uncommitted (读取未提交) | √ | √ | √ |
| Read committed (读取已提交) | × | √ | √ |
| Repeatable Read(默认) (可重复读) | × | × | √ |
| Serializable (串行化) | × | × | × |
其中 Serializable 数据安全性更好但是性能最差,Read uncommitted反之
- 读未提交(READ UNCOMMITTED):
- 这是最低的隔离级别,在该级别下,一个事务可以看到另一个事务尚未提交的数据修改。这可能会导致 脏读 问题,即读取到其他事务未提交的数据。
- 读已提交(READ COMMITTED):
- 在这个级别下,一个事务只能看到已经提交的其他事务所做的修改。这可以避免脏读问题,但是可能会引发 不可重复读 问题,即在同一个事务中,相同的查询可能返回不同的结果。
- 可重复读(REPEATABLE READ):
- 指一个事务执行过程中看到的数据,一直跟这个事务启动时看到的数据是一致的。在这个级别下,确保在一个事务中的多个查询返回的结果是一致的。这可以避免不可重复读问题,但是可能会引发 幻读 问题,即在同一个事务中,多次查询可能返回不同数量的行(MySQL 默认的隔离级别)。
- 串行化(SERIALIZABLE):
- 可串行化执行被定义为:并发执行的 SQL 事务的操作,其效果与这些 SQL 事务按某种顺序串行执行的效果相同。串行执行是指每个 SQL 事务在下一个 SQL 事务开始之前完成其全部操作
- 这是最高的隔离级别,在这个级别下,保证事务间的操作结果相当于一个按顺序执行的单线程操作。这可以避免所有的并发问题,但是会大大降低并发性能。该级别会对记录加上读写锁,在多个事务对这条记录进行读写操作时,如果发生了读写冲突的时候,后访问的事务必须等前一个事务执行完成,才能继续执行
🔧 隔离级别的实现
🎯 读已提交
每次读取数据前都生成一个 Read_View,保证每次读操作都是最新的数据。 正是因为在读提交隔离级别下,事务每次读数据时都重新创建 Read View,那么在事务期间的多次读取同一条数据,前后两次读的数据可能会出现不一致,因为可能这期间另外一个事务修改了该记录,并提交了事务。
🔄 可重复读
可重复读隔离级别是启动事务时生成一个 Read View,然后整个事务期间都在用这个 Read View。
可重复读隔离级别下在事务期间读到的记录都是事务启动前的记录。具体过程如下
- 记录被另一个事务修改时会修改记录中的
trx_id,同时记录的roll_pointer指向该记录的上一个版本的版本 - 当事务发现记录的
trx_id被修改后根据Read_View的可见机制判断该记录是否可读 - 事务从头到尾都是使用事务启动时创建的Read_View
💻 相关SQL语句
🔍 查看事务隔离级别
| |
⚙️ 设置事务隔离级别
| |
📝 MySQL的日志级别
binlog :是 MySQL 中的二进制日志文件,用于记录 MySQL 服务器上的所有更新和修改操作。它可以记录所有的 DDL和 DML操作,包括对表结构的更改、数据的插入、修改、删除等等。 binlog 是在事务提交后生成的,因此可以用于恢复数据库。是位于 Server 层的日志
- STATEMENT:每一条修改数据的 SQL 都会被记录到 binlog 中(相当于记录了逻辑操作,所以针对这种格式, binlog 可以称为逻辑日志),主从复制中 slave 端再根据 SQL 语句重现。但 STATEMENT 有动态函数的问题,比如你用了 uuid 或者 now 这些函数,你在主库上执行的结果并不是你在从库执行的结果,这种随时在变的函数会导致复制的数据不一致;
- ROW:记录行数据最终被修改成什么样了(这种格式的日志,就不能称为逻辑日志了),不会出现 STATEMENT 下动态函数的问题。但 ROW 的缺点是每行数据的变化结果都会被记录,比如执行批量 update 语句,更新多少行数据就会产生多少条记录,使 binlog 文件过大,而在 STATEMENT 格式下只会记录一个 update 语句而已;
- MIXED:包含了 STATEMENT 和 ROW 模式,它会根据不同的情况自动使用 ROW 模式和 STATEMENT 模式;
🔧 redo log :用于恢复数据,保证数据的一致性和持久性。当 MySQL 发生修改时,redolog 会将这些操作记录下来,并写入磁盘。这样,当 MySQL 发生宕机或崩溃时,通过重放 redolog 就可以恢复数据。实现了事务中的 持久性 ,主要用于故障恢复;
🔙 undo log :用于回滚操作。当 MySQL 发生事务回滚时,undo log 会记录这些操作并将其写入磁盘。这样,当 MySQL 需要回滚时,通过重放 undo log 就可以回滚事务。实现了事务中的 原子性 ,主要 用于事务回滚和 MVCC 。
📄 undo log
undo log (回滚日志),它保证了事务的 ACID 特性 (opens new window)事务中的原子性(Atomicity)
📖 原理
undo log 是一种用于撤销回退的日志。在事务没提交之前,MySQL 会先记录更新前的数据到 undo log 日志文件里面,当事务回滚时,可以利用 undo log 来进行回滚。
在发生回滚时,就读取 undo log 里的数据,然后做原先相反操作。比如当 delete 一条记录时, undo log 中会把记录中的内容都记下来,然后执行回滚操作的时候,就读取 undo log 里的数据,然后进行 insert 操作。
🎯 作用
- 实现事务回滚,保障事务的原子性 :事务处理过程中,如果出现了错误或者用户执行了 ROLLBACK 语句,MySQL 可以利用 undo log 中的历史数据将数据恢复到事务开始之前的状态。
- 实现 MVCC(多版本并发控制)关键因素之一 :MVCC 是通过 ReadView + undo log 实现的。 undo log 为每条记录保存多份历史数据,MySQL 在执行快照读(普通 select 语句)的时候,会根据事务的 Read View 里的信息,顺着 undo log 的版本链找到满足其可见性的记录。
📜 redo log
redo log 是物理日志,记录了某个数据页做了什么修改,比如对 XXX 表空间中的 YYY 数据页 ZZZ 偏移量的地方做了AAA 更新,每当执行一个事务就会产生这样的一条或者多条物理日志。通常用于保存事务已经提交但未写入磁盘的数据。
🔍 原理
在事务提交时,先将 redo log 持久化到磁盘即可。
当系统崩溃时,接着 MySQL 重启后,可以根据 redo log 的内容,将所有数据恢复到最新的状态。
⚙️ 写入方式
写入 redo log 的方式使用了追加操作, 磁盘操作是顺序写,
写入数据需要先找到写入位置,然后才写到磁盘,所以磁盘操作是随机写。MySQL 的写操作并不是立刻更新到磁盘上,而是先记录在日志上,然后在合适的时间再更新到磁盘上 。
因此MySQL 的写操作从磁盘的随机写变成了顺序写,提升语句的执行性能。
默认情况下, InnoDB 存储引擎有 1 个重做日志文件组( redo log Group),重做日志文件组由有 2 个 redo log 文件组成,这两个 redo 日志的文件名叫 :ib_logfile0 和 ib_logfile1 。
重做日志文件组是以循环写的方式工作的,从头开始写,写到末尾就又回到开头,相当于一个环形。
所以 InnoDB 存储引擎会先写 ib_logfile0 文件,当 ib_logfile0 文件被写满的时候,会切换至 ib_logfile1 文件,当 ib_logfile1 文件也被写满时,会切换回 ib_logfile0 文件。
✅ 作用
- 实现事务的持久性:能够保证 MySQL 在任何时间段突然崩溃,重启后之前已提交的记录都不会丢失;
- 将写操作从随机写变成了顺序写 :提升 MySQL 写入磁盘的性能。
提示
WAL技术
WAL(Write-Ahead Logging)技术是一种数据库事务日志管理技术,它确保在修改真正的数据之前,先将修改记录写入日志。这使得即使系统崩溃,通过日志也能恢复数据。保证了数据的持久性和一致性。
其中redolog就是WAL技术的体现
当一个事务开始时,所有对数据库的修改首先记录到重做日志缓冲区中。
重做日志缓冲区的数据会周期性地刷新到磁盘上的重做日志文件。
当事务提交时,InnoDB 确保重做日志已写入磁盘,然后将数据页的修改写入数据文件。
如果系统崩溃,InnoDB 会在启动时通过重做日志重新应用所有未完成的事务,以恢复数据库到一致状态。
🔗 binlog
binlog 日志是一种逻辑日志,用于记录所有事务执行的数据库的修改操作(如插入、更新、删除等),以便在需要时进行数据恢复、数据复制和数据同步等操作。
📊 作用
- 数据恢复: binlog 日志可以用于回滚到之前的某个时间点,从而恢复数据。
- 数据复制: binlog 日志可以用于在主从数据库之间复制数据,从而实现数据的高可用和负载均衡等功能
提示
如果不小心整个数据库的数据被删除了,能使用 redo log 文件恢复数据吗?
- 因为 redo log 文件是循环写,是会边写边擦除日志的,只记录未被刷入磁盘的数据的物理日志,已经刷入磁盘的数据都会从 redo log 文件里擦除。
- binlog 文件保存的是全量的日志,也就是保存了所有数据变更的情况,理论上只要记录在 binlog 上的数据,都可以恢复,所以如果不小心整个数据库的数据被删除了,得用 binlog 文件恢复数据。
redo log和binlog的区别
- 写入方式
- binlog 是追加写,写满一个文件,就创建一个新的文件继续写,不会覆盖以前的日志,保存的是全量的日志。
- redo log 是循环写,日志空间大小是固定,全部写满就从头开始,保存未被刷入磁盘的脏页日志。
- 引擎类型
- binlog 是 MySQL 的 Server 层实现的日志,所有存储引擎都可以使用;
- redo log 是 Innodb 存储引擎实现的日志;
- 日志内容
- binlog 是逻辑日志,记录数据库的修改操作(如插入、更新、删除等)
- redo log 是物理日志,记录的是在某个数据页做了什么修改
🔄 主从复制
MySQL 的主从同步机制是一种数据复制技术,用于将主数据库(Master)上的数据同步到一个或多个从数据库(Slave)中。
主要是通过二进制日志(Binary Log,简称 binlog)实现数据的复制。主数据库在执行写操作时,会将这些操作记录到 binlog 中,然后推送给从数据库,从数据库重放对应的日志即可完成复制。

- 写入 Binlog:主库写 binlog 日志,提交事务,并更新本地存储数据。
- MySQL 主库在收到客户端提交事务的请求之后,会先写入 binlog ,再提交事务,更新存储引擎中的数据,事务提交完成后,返回给客户端”操作成功”的响应。
- 同步 Binlog:把 binlog 复制到所有从库上,每个从库把 binlog 写到暂存日志中。
- 从库会创建一个专门的 I/O 线程,连接主库的 log dump 线程,来接收主库的 binlog 日志,再把 binlog 信息写入 relay log 的中继日志里,再返回给主库”复制成功”的响应。
- 回放 Binlog:回放 binlog,并更新存储引擎中的数据。
- 从库会创建一个用于回放 binlog 的线程,去读 relay log 中继日志,然后回放 binlog 更新存储引擎中的数据,最终实现主从的数据一致性。
🔐 二阶段提交
事务提交后, redo log 和 binlog 都要持久化到磁盘,但是这两个是独立的逻辑,可能出现半成功的状态,这样就造成主库和从库数据不一致。这是因为 redo log 影响主库的数据, binlog 影响从库的数据,所以 redo log 和 binlog 必须保持一致才能保证主从数据一致
📋 二阶段提交的过程
- 准备阶段(Prepare Phase):在事务提交时,MySQL 的 InnoDB 引擎会先写入 redo log,并将其状态标记为prepare,表示事务已经准备提交但还未真正完成。此时的 redo log 是预提交状态,还未标记为完成提交。
- 提交阶段(Commit Phase):当 redo log 的状态变为 prepare 后,MySQL Server 会写入 binlog(记录用户的 DML 操作)。binlog 写入成功后,MySQL 会通知 InnoDB,将 redo log 状态改为commit,完成整个事务的提交过程。
提示
半成功状态的情况
- 先写完 redo log,再写 binlog
- 写完 redo log 后,MySQL 异常宕机,binlog 还未写入数据。重启后 redo log 记录了,因此可以从 redo log 恢复事务的修改,但是 binlog 并没有本次事务提交的数据。后续从库通过 binlog 恢复的时候,本次事务的修改就丢了。
- 先写完 binlog,再写 redo log
- 写完 binlog 后,MySQL 异常宕机, redo log 还未写入数据。重启后因为 redo log 中没有记录,所以无法恢复本次事务的修改,但是 binlog 记录了本次事务提交的数据。后续从库通过 binlog 恢复的时候,本次事务的修改可以复原,但是这和原库的数据又不一致了。
🔍 异常宕机过程
- redo log 处于 prepare 阶段,binlog 还未写入 ,此时 MySQL 异常宕机:由于 redo log 还未 commit ,所以异常恢复后, redo log 中记录的数据也不作数, binlog 内也没有记录数据,此时数据是一致的。
- redo log 处于 prepare 阶段,binlog 已写入,但 redo log 还未 commit ,此时 MySQL 异常宕机:此时对比 redo log 中 prepare 的数据和 binlog 中的数据是都一致即可。如果一致,则提交事务。不一致,则回滚事务。