MySQL事务

MySQL事务的使用

🧠 概念

事务是一组操作的集合,它是一个不可分割的工作单位。事务会把所有的操作作为一个整体一起向系统提交或撤销操作请求,即这些操作 要么同时成功,要么同时失败。

提示

MySQL会立即隐式的提交事务。默认MySQL的事务是自动提交的,


🔄 事务控制语句

开启事务

1
start transaction

提交事务

1
commit;

回滚事务

1
rollback;

⚙️ 四大特性

  • 原子性(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_IDDB_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
ReadView结构

🔍 可见性实现

  • 如果记录的 trx_id 值小于 Read View 中的 min_trx_id 值,表示这个版本的记录是在创建 Read View 已经提交的事务生成的,所以该版本的记录对当前事务 可见
  • 如果记录的 trx_id 值大于等于 Read View 中的 max_trx_id 值,表示这个版本的记录是在创建 Read View 才启动的事务生成的,所以该版本的记录对当前事务 不可见
  • 如果记录的 trx_id 值在 Read View 的 min_trx_idmax_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语句

🔍 查看事务隔离级别

1
select @@transaction isolation;

⚙️ 设置事务隔离级别

1
set [ session | global] transaction isolation level { read uncommitted | read committed | repeatable read | serializable }

📝 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_logfile0ib_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 中,然后推送给从数据库,从数据库重放对应的日志即可完成复制。

”MySQL
  • 写入 Binlog:主库写 binlog 日志,提交事务,并更新本地存储数据。
    • MySQL 主库在收到客户端提交事务的请求之后,会先写入 binlog ,再提交事务,更新存储引擎中的数据,事务提交完成后,返回给客户端”操作成功”的响应。
  • 同步 Binlog:把 binlog 复制到所有从库上,每个从库把 binlog 写到暂存日志中。
    • 从库会创建一个专门的 I/O 线程,连接主库的 log dump 线程,来接收主库的 binlog 日志,再把 binlog 信息写入 relay log 的中继日志里,再返回给主库”复制成功”的响应。
  • 回放 Binlog:回放 binlog,并更新存储引擎中的数据。
    • 从库会创建一个用于回放 binlog 的线程,去读 relay log 中继日志,然后回放 binlog 更新存储引擎中的数据,最终实现主从的数据一致性。

🔐 二阶段提交

事务提交后, redo logbinlog 都要持久化到磁盘,但是这两个是独立的逻辑,可能出现半成功的状态,这样就造成主库和从库数据不一致。这是因为 redo log 影响主库的数据, binlog 影响从库的数据,所以 redo logbinlog 必须保持一致才能保证主从数据一致

📋 二阶段提交的过程

  • 准备阶段(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 中的数据是都一致即可。如果一致,则提交事务。不一致,则回滚事务。
그 경기 끝나고 좀 멍하기 있었는데 여러분 이제 살면서 여러가
使用 Hugo 构建
主题 StackJimmy 设计