1. 隔离级别实现原理

事务-1、Spring事务(本地事务)
  1. 对于「读未提交」隔离级别的事务来说,因为可以读到未提交事务修改的数据,所以直接读取版本链上最新的数据
  2. 对于「串行化」隔离级别的事务来说,通过加读写锁的方式来避免并行访问;
  3. 对于「读已提交」和「可重复读」隔离级别的事务来说,它们是通过 Read View 来实现的,它们的区别在于创建 Read View 的时机不同。

「读已提交」隔离级别是在「每个语句执行前」都会重新生成一个 Read View
「可重复读」隔离级别是「启动事务时」生成一个 Read View,然后整个事务期间都在用这个 Read View

2. 事务实现原理⭐️🔴

%%
▶1.🏡⭐️◼️【🌈费曼无敌🌈⭐️第一步⭐️】◼️⭐️-point-20230406-0712%%
❕ ^9cpr5j

Mysql 里面的事务,满足 ACID 特性。

  1. 首先,A 表示 Atomic 原子性,也就是需要保证多个 DML 操作是原子的,要么都成功,要么都失败。那么,失败就意味着要对原本执行成功的数据进行回滚,所以 InnoDB 设计了一个 undo log 表,在事务执行的过程中,把修改之前的数据快照保存到 undo log 里面,一旦出现错误,就直接从 undo log 里面读取数据执行反向操作就行了。
  2. 其次,C 表示一致性,表示数据的完整性约束没有被破坏,这个更多是依赖于业务层面的保证,数据库本身也提供了一些,比如主键的唯一约束,字段长度和类型的保证等等。
  3. 接着,I 表示事物的隔离性,也就是多个并行事务对同一个数据进行操作的时候,如何避免多个事务的干扰导致数据混乱的问题。而 InnoDB 实现了 SQL92 的标准,提供了四种隔离级别的实现:
    RU(未提交读) RC(已提交读) RR(可重复读)Serializable(串行化)
    InnoDB 默认的隔离级别是 RR(可重复读),然后使用了 MVCC 机制解决了脏读和不可重复读的问题,然后使用了间隙锁的方式有效避免了幻读的问题
  4. 最后一个是 D,表示持久性,也就是只要事务提交成功,那对于这个数据的结果的影响一定是永久性的。不能因为宕机或者其他原因导致数据变更失效。理论上来说,事务提交之后直接把数据持久化到磁盘就行了

但是因为随机磁盘 IO 的效率确实很低,所以 InnoDB 设计了 Buffer Pool 缓冲区来优化,也就是数据发生变更的时候先更新内存缓冲区,然后在合适的时机再持久化到磁盘。那在持久化这个过程中,如果数据库宕机,就会导致数据丢失,也就无法满足持久性了。
所以 InnoDB 引入了 redo log 文件,这个文件存储了数据被修改之后的值,当我们通过事务对数据进行变更操作的时候,除了修改内存缓冲区里面的数据以外,还会把本次修改的值追加到 redo log 里面。当提交事务的时候,直接把 redo log 日志刷到磁盘上持久化,一旦数据库出现宕机,在 Mysql 重启在以后可以直接用 redo log 里面保存的重写日志读取出来,再执行一遍从而保证持久性。
因此,事务的实现原理的核心本质就是如何满足 ACID 的,在 InnDB 里面用到了 MVCC、行锁表锁、undo logredo log 等机制来保证。

MVCC 并发控制原理

1、在并发读写数据库时,可以做到在读操作时不用阻塞写操作,写操作也不用阻塞读操作,从而提高数据库的并发读写的处理能力。
2、能实现读一致性,从而解决脏读、幻读、不可重复读等不可重复读,但是不能解决数据更新丢失的问题。
3、采用乐观锁或者悲观锁用来解决写和写的冲突,从而最大程度地去提高数据库的并发性能。

在 MVCC 中,通常不需要加锁来控制并发访问。相反,每个事务都可以读取已提交的快照,而不需要获得共享锁或排它锁。
在写操作的时候,MVCC 会使用一种叫为“写时复制”(Copy-On-Write)的技术,也就是在修改数据之前先将数据复制一份,从而创建一个新的快照。当一个事务需要修改数据时,MVCC 会首先检查修改数据的快照版本号是否与该事务的快照版本一致,如果一致则表示可以修改这条数据,否则该事务需要等待其他事务完成对该数据的修改。
另外,这个事物在新快照之上修改的结果,不会影响原始数据,其他事务可以继续读取原始数据的快照,从而解决了脏读、不可重复度问题。
所以,正是有了 MVCC 机制,让多个事务对同一条数据进行读写时,不需要加锁也不会出现读写冲突。

3. MVCC 隔离级别原理

%%
▶5.🏡⭐️◼️【🌈费曼无敌🌈⭐️第一步⭐️】◼️⭐️-point-20230413-1910%%
❕ ^a59z9q

MVCC 全称 Multi-Version Concurrency Control,MVCC 是一种通过增加版本冗余数据来实现并发控制的方法,一般在数据库管理系统中,实现对数据库的并发访问,在编程语言中实现事务内存。
%%
1103-🏡⭐️◼️MySQL 隔离级别的实现原理◼️⭐️-point-20230214-1103%%

3.1. 隐藏字段

==除了我们正常业务涉及的字段外==,InnoDB 在内部向数据库表中添加三个隐藏字段:

  • DB_TRX_ID:6-byte 的事务 ID。插入或更新行的最后一个事务的事务 ID
  • DB_ROLL_PTR:7-byte 的回滚指针。就是指向对应某行记录的上一个版本,在 undo log 中使用。
  • DB_ROW_ID:6-byte 的隐藏主键。如果数据表中没有主键也没有非 null 列,那么 InnoDB 会自动生成单调递增的隐藏主键(表中有主键或者非 NULL 的 UNIQUE 键时都不会包含 DB_ROW_ID 列)。

如上面的表没有设计 primary key,其中 id/name/city 是我们的业务字段,那么加上隐藏字段应该如下

image.png

为方便理解可以简称如下:

image.png

3.2. undo log 和 版本链

3.2.1. undo log

undo log 就是回滚日志,在 insert/update/delete 变更操作的时候生成的记录方便回滚。

3.2.1.1. insert undo log

当进行 insert 操作的时候,产生的 undo log 只有在事务回滚的时候需求,如果不回滚在事务提交之后就会被删除。

3.2.1.2. modify undo log

当进行 update 和 delete 的时候,产生的 undo log 不仅仅在事务回滚的时候需要,在快照读的时候也是需要的,所以不会立即删除,只有等不在用到这个日志的时候才会被 mysql purge 线程统一处理掉(delete 操作也只是打一个删除标记,并不是真正的删除)

3.2.2. 版本链

所谓的版本链就是多个事务操作同一条记录的时候都会生成一些 undo 日志,这些 undo 日志通过回滚指针串联在一起。

image.png

3.3. ReadView

%%
▶6.🏡⭐️◼️【🌈费曼无敌🌈⭐️第一步⭐️】◼️⭐️-point-20230413-1916%%
❕ ^6sakz1

使用 READ COMMITTED 和 REPEATABLE READ 隔离级别的事务,除了 readview 还需要用到上文提到的版本链,核心问题是:

需要判断版本链中的哪个版本是允许当前事务可见的

ReadView 中主要包含 4 个比较重要的内容:

image.png

  1. m_ids:表示在生成 ReadView 时当前活跃的读写事务的事务 id 列表。
    “活跃事务”指的就是,启动了但还没提交的事务
  2. min_trx_id:表示在生成 ReadView 时当前系统中活跃的读写事务中最小的事务 id,也就是 m_ids 中的最小值。(up_limit_id)
  3. max_trx_id:表示生成 ReadView 时系统中应该分配给下一个事务的 id 值,也就是全局事务中最大的事务 id 值 + 1。 (low_limit_id)
  4. creator_trx_id:表示生成该 ReadView 的事务的事务 id。
注意 max_trx_id 并不是 m_ids 中的最大值,事务 id 是递增分配的。比方说现在有 id 为 1,2,3 这三个事务,之后 id 为 3 的事务提交了。那么一个新的读事务在生成 ReadView 时,m_ids 就包括 1 和 2,min_trx_id 的值就是 1,max_trx_id 的值就是 4。

3.3.1. low 与 up

%%
▶83.🏡⭐️◼️【🌈费曼无敌🌈⭐️第一步⭐️】◼️⭐️-point-20230306-2245%%
❕ ^yncd6p

其中最早的事务 ID 为 up_limit_id最迟的事务 ID 为 low_limit_id
%%
1457-🏡⭐️◼️low 与 up 的理解 ?🔜MSTM📝 up 表示最小的,最早出现的那个,low 的话指后面来的◼️⭐️-point-20230214-1457%%

image.png

如何记忆:up→起得早的就排在前面,即前面提交的事务 id,数值比较小。 ^1xd5nv

3.4. 可见性判断⭐️🔴

image.png

trx_id: 最后一次稳定事务 id,即刚刚提交的事务 id

  1. trx_id < up_limit_id (即 min_id) 读之前已提交
    表示这个版本的记录是在创建 Read View  已经提交的事务生成的,所以该版本的记录对当前事务 可见。跳转到步骤 5;

  2. trx_id > low_limit_id (即 max_id) 读之后才启动
    表示这个版本的记录是在创建 Read View  才启动的事务生成的,所以该版本的记录对当前事务 不可见。跳转到步骤 4;

  3. up_limit_id <= trx_id <= low_limit_id

需要判断 trx_id 是否在 m_ids 列表中:
1. 如果记录的 trx_id 在 m_ids 列表中,表示生成该版本记录的活跃事务依然活跃着(还没提交事务),所以该版本的记录对当前事务不可见。
2. 如果记录的 trx_id 不在 m_ids 列表中,表示生成该版本记录的活跃事务已经被提交,所以该版本的记录对当前事务可见。

从 up_limit_id 到 low_limit_id 进行遍历,如果 trx_id 等于他们之中的某个事务 id 的话,表示该记录的最后一次修改尚未保存,跳转到步骤 4。否则跳转到步骤 5;

  1. 从此记录的 DB_ROLL_PTR 指针所指向的 undo log(此记录的上一次修改,即上一个版本),将 undo log 的 DB_TRX_ID 赋值给 trx_id,跳转到步骤 1 重新开始计算可见性;
    取上一个版本进行判断,拿着 readview 从版本链上,从上往下移动判断%%
    1533-🏡⭐️◼️对版本链和 readview 可见性判断的理解◼️⭐️-point-20230214-1533%%
  2. 如果此记录的 DELELE_BIT 为 false,说明该记录未被删除,可以返回,否则不返回。

> 一个事务只能看到第一次查询之前已经提交的事务以及当前事务的修改。
> 一个事务不能看到当前事务第一次查询之后创建的事务,以及未提交的事务。

4. LBCC

LBCC(Lock-Base Concurrency Control)基于锁的并发控制。在前面的文章中我们学习过 MVCC,我们知道 MVCC 基于多版本控制可以提升 mysql 的并发读写能力,但是不能完全解决幻读,以及并发写的问题(出现更新丢失)

4.1. 读取分类

https://www.modb.pro/db/593096
当多个事务同时操作同一份数据内容时,可以分为两种获取方式:当前读、快照读。
image.png

4.1.1. 当前读 (锁定读)

直接从磁盘或 buffer 中获取当前内容的最新数据,读到什么就是什么。根据隔离级别的不同期间会产生一些锁,防止并发场景下其他事务产生影响;

在官方叫做 Locking Reads(锁定读):
https://dev.mysql.com/doc/refman/8.0/en/innodb-locking-reads.html

4.1.2. 快照读

简单的 SELECT 操作(不包括 lock in share mode,for update),根据隔离级别的不同,会在不同时机产生快照,事务读取的实际是快照内容,保证一致性的同时减少了锁之间的竞争;
官方叫做 Consistent Nonlocking Reads(一致性非锁定读取,也叫一致性读取): https://dev.mysql.com/doc/refman/8.0/en/innodb-consistent-read.html

4.2. RR 级别下并没有完全解决幻读

%%
1319-🏡⭐️◼️为什么说 MySQL 的可重复读并没有完全解决幻读?◼️⭐️-point-20230214-1319%%

MySQL 在「可重复读」隔离级别下,可以很大程度上避免幻读现象的发生(注意是很大程度避免,并不是彻底避免),所以 MySQL 并不会使用「串行化」隔离级别来避免幻读现象的发生,因为使用「串行化」隔离级别会影响性能。

在 RR 级别下普通查询是快照读,并不会看到其他事务插入的数据。这种幻读情况只有在快照读与当前读混合使用的情况下才会出现,这部分也是争议比较多的地方。

然而当前读的定义就是能从 buffer 或磁盘获取到已提交数据的最新值,所以这跟事务的可见性其实并不矛盾。
image.png

案例一分析:事务 B 的两次读中间 A 有提交,如果 B 第二次读是当前读,就会发生幻读。因为当前读不是通过 readview 来判断可见性,而是直接读取缓存中的最新数据

image.png

4.2.1. 解决方案

Innodb 引擎为了解决「可重复读」隔离级别使用「当前读」而造成的幻读问题,就引出了间隙锁

5. 幻读总结

MySQL InnoDB 引擎的可重复读隔离级别(默认隔离级),根据不同的查询方式,分别提出了避免幻读的方案:

  • 针对 快照读(普通 select 语句),是通过 MVCC 方式解决了幻读。
  • 针对 当前读(select … for update 等语句),是通过 next-key lock(记录锁 + 间隙锁)方式解决了幻读。

举例两个发生幻读场景的例子:

第一个例子:对于快照读, MVCC 并不能完全避免幻读现象。因为当事务 A 更新了一条事务 B 插入的记录,那么事务 A 前后两次查询的记录条目就不一样了,所以就发生幻读。

第二个例子:对于当前读,如果事务开启后,并没有执行当前读,而是先快照读,然后这期间如果其他事务插入了一条记录,那么事务后续使用当前读进行查询的时候,就会发现两次查询的记录条目就不一样了,所以就发生幻读。

所以,MySQL 可重复读隔离级别并没有彻底解决幻读,只是很大程度上避免了幻读现象的发生。

要避免这类特殊场景下发生幻读的现象的话,就要尽量在开启事务之后,马上执行 select … for update 这类当前读的语句因为它会对记录加 next-key lock,从而避免其他事务插入一条新记录。

for update 案例

6. RC 与 RR 隔离级别的区别

Repeatable Read 和 Read Committed 隔离级别都是基于 read view 来实现,不同之处在于:

  • Repeatable Read
    read view 是在执行事务中第一条 select 语句的瞬间创建,后续整个事务期间所有的 select 都是复用这个对象,所以能保证每次读取的一致性。(可重复读的语义)❕%%
    1537-🏡⭐️◼️可重复读,复用第一次查询时使用的 readview。与读已提交不同的是,读已提交每次都会生成一个 readview,相当于动态维护了 m_ids 集合◼️⭐️-point-20230214-1537%%

  • Read Committed
    事务中每条 select 语句都会创建 read view,这样就可以读取到其它事务已经提交的内容。

对于 InnoDB 来说,Repeatable Read 虽然比 Read Committed 隔离级别高,开销反而相对较小。

7. 实战经验

8. 参考与感谢

https://www.modb.pro/db/593096
https://juejin.cn/post/7020422614552150052
可见性: https://developer.aliyun.com/article/1094075 ^zqsfcl
https://juejin.cn/post/6844903774071291917 ^033zxt

image-20200201101126173

image-20200201101626392