Loading

MySQL-事务

MySQL——事务

事务的特性

事务Transaction)是有mysql存储引擎来实现的,我们常见的InnoDB引擎就支持事务。

要实现事务,必须遵守以下4个特性:

  • 持久性(Durability:事务结束后,对数据的修改就是永久的,即使系统故障也不会丢失。
  • 原子性(Atomicity:一个事务中的所有操作,要么全部完成,要么全部不完成,不会在中间某一部结束。如果在中间某一步出现错误,数据会被回滚到事务开始前的状态。
  • 隔离性(Isolation:数据库允许多个并发事务同时对其数据进行读取和修改,隔离性可以防止多个事务并发执行时由于交叉执行而导致的数据不一致问题。多个事务执行相同的数据时,不会相互干扰,每个事务都有一个完整的数据空间,对其他并发事务是隔离的。
  • 一致性(Consistency:是指事务操作前和操作后,数据满足完整性约束,数据库保持一致性状态。

以下是 InnoDB 引擎保证事务的四个特性所使用的技术:

  • 持久性是通过 redo log(重做日志)来实现的
  • 原子性是通过 undo log(回滚日志)来实现的
  • 隔离性是通过 MVCC(多版本并发控制)锁机制来实现的
  • 一致性是通过持久性+原子性+隔离性来实现

为什么事务需要隔离性

要明白为什么事务需要隔离性,就要先搞明白如果事务之间没有隔离性,会出现什么问题,又是如何解决这些问题的。

并行事务引发的问题

MySQL 服务端是允许多个客户端连接的,这意味着 MySQL 会出现同时处理多个事务的情况。

那么在同时处理多个事务的时候,就可能出现脏读(dirty read)、不可重复读(non-repeatable read)、幻读(phantom read)的问题

脏读

  • 脏读:一个事务读取了另一个事务 未提交的修改。如果未提交的事务最终回滚,读取到的数据就是无效的“脏数据”。

对应两个事务A和B,当A读取数据100后,A修改数据为200,此时还未提交事务A,而B读取了数据200,但是A因为某些原因触发回滚,数据就变回100,那么B读取到的数据200就是过期数据,这种现象就被称为脏读。

不可重复读

  • 不可重复读:同一事务中多次读取同一数据,结果不一致,因为其他事务 修改并提交 了该数据。

假设有 A 和 B 这两个事务同时在处理数据,事务 A 先开始从数据库中读取数据100,然后继续执行代码逻辑处理(此时事务A未提交)。在这个过程中如果事务 B 更新了这条数据,将数据修改为200,并提交了事务,那么当事务 A 再次读取该数据时,就会发现前后两次读到的数据是不一致的,这种现象就被称为不可重复读。

幻读

  • 幻读:同一事务中多次执行 相同范围查询,结果集的行数不同,因为其他事务 插入或删除 了符合条件的数据。

假设有 A 和 B 这两个事务同时在处理数据,事务 A 开始从数据库查询某个范围内的数据有100条。在这个过程中如果事务 B 往数据库中插入了100条新的数据,并提交了事务,那么当事务 A 再次查询该范围内的数据时,就会查到有200条数据,发现和前一次查到的数据量不一样了,这种现象就被称为幻读。

事务的隔离级别

当多个事务并行执行时,会出现脏读、不可重复读、幻读的问题,这些问题会对事务的一致性产生影响。

  • 脏读:读到其他事务未提交的数据
  • 不可重复读:前后读取的数据不一致
  • 幻读:前后读取的数据数量不一致

这三个问题对事务一致性的影响程度如下:

因此 SQL 提出了4种隔离级别来规避这些问题,级别越高,开销越大,数据库的性能就越低:

  • 读未提交:一个事务还未提交时,该事务所做的变更就能被其他事务看到
  • 读提交:只有一个事务提交完成后,该事务所做的变更才能被其他事务看到
  • 可重复读:事务执行过程中多次读取的数据,跟这个事务启动时读取到的数据是一致的(MySQL InnoDB引擎默认使用的隔离机制
  • 串行化:对数据加上读写锁,在多个事务对数据进行读写操作时,如果发生了读写冲突时,后一个事务只能等前一个事务提交完成后,才能继续执行

按照隔离程度高低排序如下:

因为不同的隔离级别的隔离程度不同,因此所能规避的问题也有差异,针对不同的隔离级别,执行并发事务时可能发生的问题如下:

  • 如果是读未提交,可能会出现脏读、不可重复读、幻读问题
  • 如果是读提交,可能会出现不可重复读、幻读问题
  • 如果是可重复读,可能会出现幻读问题
  • 如果是串行化,则可以完美解决上述三个问题

MySQL 的 InnoDB 引擎虽然是默认隔离级别是“可重复读”,但是可以很大程度上避免“幻读”问题(并不能彻底避免)。

(1) 基于 MVCC 的快照读(Snapshot Read)

  • 快照机制:事务启动时生成一个全局一致的 Read View,所有普通 SELECT 操作基于该快照读取数据,确保事务内多次读取结果一致。
  • 避免幻读的局限性:
    • 仅对 快照读 有效(普通 SELECT)。
    • 若其他事务插入新数据并提交,当前事务的 快照读 不会看到这些新数据。

(2) 基于 Next-Key Locking 的当前读(Current Read)

  • 当前读:显式加锁的读操作(如 SELECT ... FOR UPDATEUPDATEDELETE)会访问最新数据并加锁。
  • Next-Key Lock 的组成:
    • 行锁(Record Lock):锁定索引记录。
    • 间隙锁(Gap Lock):锁定索引记录的间隙(防止其他事务插入)。
  • 作用:在范围查询时,锁定符合条件的 现有行间隙,阻止其他事务插入新数据。

Read View 在 MVCC 中的工作原理

首先需要理解两个概念:

  • 什么是 Read View
  • 聚簇索引记录中两个跟事务有关的隐藏列 DB_TRX_ID 和 DB_ROLL_PTR

什么是 Read View?

Read View 就像一个快照,记录事务开始时的活跃事务列表,用来判断哪些数据版本可见

Read View 中包含四个核心组成:

Read View组成结构

还记得我们之前提到的隔离级别中的“可重复读”吗?在解释“可重复读”时,我们说在该级别中,事务执行过程中多次读取的数据,跟这个事务启动时读取到的数据是一致的。如何保证后续读取的数据跟启动时读取到的数据一致?就是使用 Read View 来保证一致的。

后面我们再详细解释一下,InnoDB 引擎是怎么使用 Read View 的。

聚簇索引中的两个隐藏列

在 MySQL InnoDB 的聚簇索引中,每行记录(数据行)都包含两个与事务相关的 隐藏列,它们是实现 多版本并发控制(MVCC)事务隔离 的核心机制。

隐藏列

1. DB_TRX_ID(事务 ID 列)

  • 作用:记录最后一次 修改该行数据的事务 ID
  • 规则:
    • 当一个事务对某行数据执行 INSERT、UPDATE、DELETE 操作时,会将自己的事务 ID 写入该行的 DB_TRX_ID
    • 事务 ID 是全局递增的唯一标识,由 InnoDB 自动分配。

2. DB_ROLL_PTR(回滚指针列)

  • 作用:指向该行数据在 undo log(回滚日志) 中的历史版本链。
  • 规则:
    • 每次对数据行进行修改时,旧版本的数据会被写入 undo log,同时 DB_ROLL_PTR 会指向这个旧版本,于是就可以通过该指针找到修改前的记录。
    • 通过 DB_ROLL_PTR,可以追溯该行数据的所有历史版本。

到这里我们了解了数据库中每条记录都有两个隐藏列 DB_TRX_ID 和 DB_ROLL_PTR,分别记录最后一次修改改行数据的事务的ID指向该行数据在回滚日志中的历史版本链的指针

那么我们在创建一个 Read View 之后,可以将 DB_TRX_ID 分为三种情况:

  1. 如果 DB_TRX_ID 中记录的事务ID 小于 Read View 中的 min_trx_id,说明这个版本的记录是在该 Read View 生成之前就完成提交了。这是因为 Read View 中记录的是第一次读取时数据库中所有已开始且未提交的事务,因此如果 DB_TRX_ID 比 Read View 中最小的事务ID还小,说明 DB_TRX_ID 对应的事务在 Read View 生成之前就已经提交了。所以该版本的记录对当前事务可见
  2. 如果 DB_TRX_ID 中记录的事务ID 大于等于 Read View 中的 next_trx_id,说明这个版本的记录对应的事务是在该 Read View 生成之后才开始执行的。这是因为 Read View 中记录的是第一次读取时数据库中所有已开始且未提交的事务,因此如果 DB_TRX_ID 比 Read View 中最大的事务ID还大,说明 DB_TRX_ID 对应的事务在 Read View 生成之后才开始执行。所以该版本的记录对当前事务不可见
  3. 如果 DB_TRX_ID 中记录的事务ID 大于 min_trx_id 且 小于 next_trx_id,说明在该 Read View 生成时,这个版本的记录对应的事务就已经开始执行了,但还未提交。所以该版本的记录对当前事务不可见

可重复读工作原理

可重复读级别是启动事务时生成一个 Read View,然后整个事务期间都使用这个 Read View。

假设事务A(事务ID为51)启动后,事务B(事务ID为52)也启动了。那么这两个事务生成的 Read View 可以抽象为如下形式:

当事务A生成 Read View 时,数据库里只有 A 一个事务,因此m_ids也只记录A事务ID。当事务B生成 Read View 时,数据库中除了B事务,还有正在执行未提交的A事务,因此B的m_ids中有A和B事务的ID。

当B第一次读取数据时,此时数据库中记录的最新修改后的事务ID是50,B会判断50比 Read View 中的 min_trx_id(此时是51)还小,因此判断修改该条数据的事务(ID为50的事务)已提交,故可以读取数据为100。

B读取数据之后,A修改数据,但是修改后并未提交,此时 MySQL 会记录相应的 undo log,并以链表的方式串联起来,形成版本链

当B再次读取数据时,此时数据库中记录的最新修改后的事务ID是51,会判断51在事务B的min_trx_id和next_trx_id之间,并且在m_ids中,因此判断该事务已执行但未提交,故会沿着版本链找到上一次修改的数据记录,该记录中的事务ID是50,判断50比事务B中 min_trx_id(此时是51)还小,因此判断修改该条数据的事务(ID为50的事务)已提交,故读取的数据仍然为100。

当A提交事务后,由于隔离级别是“可重复读”,因此B再次读取数据时,还会基于上面的 Read View 来判断当前数据库的记录是否可见。当数据库事务ID为51时,根据事务B的 Read View 判断该修改该数据的事务还未提交,会根据版本链找到上一条旧版本的记录,并最终读取事务ID为50的记录的数据,因此读取的数据仍为100。

读提交的工作原理

读提交隔离级别是在每次读取数据时,都会生成一个新的 Read View

在事务A提交之前的分析过程都一样。

假设事务A(事务ID为51)启动后,事务B(事务ID为52)也启动了。那么这两个事务生成的 Read View 可以抽象为如下形式:

当事务A生成 Read View 时,数据库里只有 A 一个事务,因此m_ids也只记录A事务ID。当事务B生成 Read View 时,数据库中除了B事务,还有正在执行未提交的A事务,因此B的m_ids中有A和B事务的ID。

当B第一次读取数据时,此时数据库中记录的最新修改后的事务ID是50,B会判断50比 Read View 中的 min_trx_id(此时是51)还小,因此判断修改该条数据的事务(ID为50的事务)已提交,故可以读取数据为100。

B读取数据之后,A修改数据,但是修改后并未提交,此时 MySQL 会记录相应的 undo log,并以链表的方式串联起来,形成版本链

当B再次读取数据时,会生成一个新的 Read View,但是因为事务A没有提交,所以事务B新生成的 Read View 与第一次生成的是一样的,此时数据库中记录的最新修改后的事务ID是51,会判断51在事务B的min_trx_id和next_trx_id之间,并且在m_ids中,因此判断该事务已执行但未提交,故会沿着版本链找到上一次修改的数据记录,该记录中的事务ID是50,判断50比事务B的 min_trx_id(此时是51)还小,因此判断修改该条数据的事务(ID为50的事务)已提交,故读取的数据仍然为100。

当A提交事务后,事务B再次读取数据时,生成的Read View就与之前的不同了,此时事务A已提交,因此B的m_ids中不再记录事务A的ID:

因此,此时事务B读取数据,当数据库事务ID为51时,根据事务B的 Read View 判断该修改该数据的事务ID小于min_trx_id,判断该事务在创建该 Read View 之前就已提交,所以该版本的记录对事务 B 是可见的,故会读取该条数据200。

posted @ 2025-05-12 15:43  maoxianjia  阅读(56)  评论(0)    收藏  举报