隔离性,还有一个说法就是 数据可见性。

隔离性、数据可见性是一个在并发事务下才需要考虑的问题,并发事务可以分3种情况考虑

  1. 读-读, 读操作不会对数据产生影响,所以不需要关注
  2. 读-写 or 写-读, 可能会出现脏读、不可重复读、幻读
  3. 写-写,可能会脏写的情况

并发事务下的数据的一致性写问题

  • 脏写:一个事务修改了另一个未提交事务修改过的数据。

并发事务下的数据的一致性读问题

  • 脏读:事务读取了未提交的数据,可能造成数据不一致。
  • 不可重复读:事务在内部的多次读取中看到了同一数据的不同版本,主要由于其他事务的更新操作。
  • 幻读:事务在两次查询同一个范围时看到了不一样的行,通常是因为其他事务添加或删除了行。

MySQL 的 4种 事务隔离级别

隔离级别 解决的问题 未解决的问题 原理描述
读未提交 脏读、不可重复读、幻读 允许事务读取其他事务未提交的修改,可能导致脏读。
读已提交 脏读 不可重复读、幻读 只能看到已经被其他事务提交的数据,避免了脏读,但不能防止在同一事务中看到不一致的数据。
可重复读 脏读、不可重复读 MySQL 在该隔离级别下加上gap 锁可部分解决幻读问题 在事务开始后所有SELECT操作都看到一致的快照,避免了不可重复读,但无法防止其他事务插入新行(幻读)。
串行化 脏读、不可重复读、幻读 “写”会加“写锁”,“读”会加“读锁”。当出现读写锁冲突的时候,后访问的事务必须等前一个事务执行完成,才能继续执行。通过强制事务串行执行,防止了脏读、不可重复读和幻读,提供了最高级别的隔离。

由于脏写导致的数据一致性问题非常严重,任何一种隔离级别下都不允许发生,对数据的修改操作必须通过锁串行执行

InnoDB 在解决 并发事务时,分成两种情况对应不同的解决方案

  1. 快照读-MVCC
  2. 当前读- 锁

1 快照读的隔离性-MVCC

  • 一致性视图:在快照读中,事务会创建一个一致性视图(Consistent Read View),确保当前事务读取到的都是事务开始时的数据状态。它依赖于MVCC的实现。
  • 无需锁定:快照读是一种无锁的读取,即读取数据时不需要对行记录进行锁定,因此它不会阻塞事务的读写,同时也不会被其他事务的读写操作阻塞。
  • 隔离级别影响:快照读的行为受事务隔离级别的影响,不同的隔离级别会影响读取到的版本。
    • 在读未提交隔离级别下,所有事务都读取最新事务;
    • 在串行化隔离级别下,使用锁控制数据的访问。
    • 在读已提交和可重复读隔离级别下,使用MVCC 来控制数据的可见性。

InnoDB存储引擎的MVCC(多版本并发控制)机制是基于ReadView和Undo Log共同实现的,关键是通过TRX_IDROLL_PTR两个行记录隐藏列来跟踪和管理每一行的修改版本。

1.1 版本链 -undo log

在基于undo log 实现原子性 一文中,可以看到

  1. 行记录中有roll_pointer,
  2. update 操作中TRX_UNDO_UPD_EXIST_REC 和 TRX_UNDO_DEL_MARK_REC 类型的undo log 中,也有roll_pointer,
    通过这些roll_pointer, 可以形成一条行记录的版本链。

insert 操作中,对应的undo log没有roll_pointer 属性,因为insert 操作就是一个行记录的初始版本,没有比它更早的操作了。

以下是一个通过roll_pointer 组成的版本链,每个undo log 进行了内容省略以展示链接的重点内容

1.2 readview

有了版本链,那么该如何判断哪个版本的数据对当前事务可见呢,这里需要引入readview 概念。

Read View 主要包含以下几个关键的部分:

  1. m_ids:当前系统中活跃的事务ID列表。这些事务在生成 Read View 时已经开始但尚未提交。
  2. min_trx_id:生成 Read View 时,活跃事务ID中的最小值。这是因为任何 ID 小于此值的事务在 Read View 生成前已经提交。
  3. max_trx_id:生成 Read View 时,已知的下一个事务ID。任何大于或等于此 ID 的事务在生成 Read View 后开始的。
  4. creator_trx_id:生成这个 Read View 的事务的事务ID。 一个事务只有进行修改操作时,才会被分配trx_id, 否则一个事务的trx_id 默认都是0, 所以 creator_trx_id 也有可能时0

运作方式

  • 当事务执行查询操作时,它会根据自己的 Read View 来判断数据行的可见性。具体来说,每行数据都有自己的系统版本号(trx_id,即事务ID)。Read View 通过以下逻辑来确定行的可见性:
    1. 如果行及记录的trx_id 和creator_trx_id 相等,说明当前事务在访问自己修改的数据,数据可见。
    2. 如果行的版本号小于 min_trx_id,说明行是在 Read View 生成之前被创建或最后修改的,因此对当前事务可见。
    3. 如果行的版本号大于或等于 max_trx_id,说明行是在 Read View 生成之后被创建或修改的,因此对当前事务不可见。
    4. 如果行的版本号在 min_trx_id 和 max_trx_id 之间,还需要检查这个版本号是否属于 m_ids 列表中的某个事务:
      • 如果属于,说明该行可能由尚未提交的事务修改,对当前事务不可见。
      • 如果不属于,说明该行由已提交的事务修改,对当前事务可见。

如果某个版本的数据对当前事务不可见,那就顺着版本链找洗一个版本的数据,并按照上面的步骤进行判断。如果一个数据直到最后一个版本都不可见,那就说明该条数据对当前事务完全不可见

1.2.1 readview 和 读已提交(Read Committed)

  • 生成时机:在 RC 隔离级别下,Read View 不是在事务开始时生成,而是在一个事务内每次执行 SQL 查询时都会生成新的readview, 所以该事务内是可以看到其他事务已提交的对数据的修改,这在数据一致性上就表现为不可重复读
  • 行为:每次查询都创建一个新的 Read View,包含当前时刻所有未完成的事务ID。这确保了查询只能看到那些在执行查询前已经提交的事务所做的更改。

1.2.2 readview 和 可重复读(Repeatable Read)

InnoDB 的 默认隔离级别。

  • 生成时机:在 RR 隔离级别下,Read View 是在事务的第一次查询操作开始时创建的,且在整个事务期间保持不变。这意味着整个事务中所有的查询都将看到相同的数据快照。
  • 行为:一旦生成,这个 Read View 将包含事务开始时刻的所有活跃事务ID。无论这些事务后来如何提交或回滚,当前事务的后续查询都不会感知到这些变化。

1.2.3 两者的对比

  • 数据可见性:在读已提交中,事务可能看到其他事务提交的更新(即事务中的查询可能返回不同的结果),而在可重复读中,事务保证了始终对数据的一致视图。
  • Read View 的生成频率:读已提交每次查询都重新生成 Read View,而可重复读只在事务开始时生成一次。
  • 系统开销:由于读已提交每次查询都需要生成 Read View,可能会有更高的系统开销,尤其是在查询频繁的场景中。相比之下,可重复读的开销主要集中在事务开始阶段。

2 当前读的隔离型-锁

当前读指的是读取数据时总是获取数据的最新版本,并通过加锁(行级别的排他锁,S锁或X锁)以确保一致性,防止其他事务修改或删除这些数据。

当前读通常用于需要修改数据的查询,如

  1. select…lock in share mode (共享读锁)
  2. select…for update
  3. UPDATE
  4. DELETE

关于行锁和间隙锁的具体加锁规则,和隔离级别和索引有关,大家可以参考何登成的加锁分析文章
MySQL 加锁分析

3. 幻读bad case

在前面介绍隔离级别时,提到 在可重复读隔离级别下 加上 间隙锁, 可以一定程度上解决幻觉。
但是如果一个事务中 快照读和当前读混用,就会出现幻读bad case.

幻读被完全解决了吗? 这篇文章中例举两个幻读 bad case,讲的比较清晰,可以参考

4. 当前读vs快照读

  • 快照读(Snapshot Read)
    • 读取数据时使用的是某一时间点的快照,不会加锁。
    • 使用MVCC机制,根据事务的隔离级别和版本号返回合适的行版本。
    • 通常用于SELECT查询。
  • 当前读(Current Read)
    • 始终读取最新版本的行。
    • 可能会加锁,防止其他事务修改或删除读取的数据。
    • 通常用于修改数据的查询操作,如SELECT ... FOR UPDATESELECT ... LOCK IN SHARE MODEUPDATEDELETE

相关文章

Intro to 事务
Intro to InnoDB 事务
InnoDB事务-原子性的实现,undo log
InnoDB事务-持久性的实现, binglog & redo log&undo log