【3.18】MySQL事务整理

4.1 事务隔离级别是怎么实现的?

在提到隔离级别之前,注意并不是所有的引擎都能支持事务,比如 MySQL 原生的 MyISAM 引擎就不支持事务,也正是这样,所以大多数 MySQL 的引擎都是用 InnoDB。

想要理解事务,就不得不提到事务的ACID特性:

  • 原子性(Atomicity)同一个事务中的所有操作,要么全部完成,要么全部回滚
    • 就好比买一件商品,购买成功时,则给商家付了钱,商品到手;购买失败时,则商品在商家手中,消费者的钱也没花出去。
  • 一致性(Consistency)事务的执行不会破坏数据的完整性约束
    • 什么叫完整性约束?其实就是要符合业务逻辑。比如完整性约束要求A + B的和为100,那么在事务执行后,应该依然满足完整性约束。
  • 隔离性(Isolation):隔离性针对多个事务,多个事务同时使用相同的数据时,不会相互干扰
    • 因为每个事务都有一个完整的数据空间,对其他并发事务是隔离的。举个例子就是消费者购买商品这个事务,是不影响其他消费者购买的。
  • 持久性(Durability):事务一旦提交,对数据的修改就是永久的。即使系统故障,也不应该丢失数据。那么MySQL主要通过redo log 来实现持久性的。

InnoDB 引擎通过什么技术来保证事务的这四个特性的呢?

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

MySQL 有两种开启事务的命令,分别是:

  • 第一种:begin/start transaction 命令;当执行了增删查改操作的 SQL 语句,才是事务真正启动的时机。
  • 第二种:start transaction with consistent snapshot 命令;马上启动事务。

事务的隔离级别

MySQL 服务端是允许多个客户端连接的,在同时处理多个事务的时候,事务并发就有可能出现**脏读(dirty read)、不可重复读(non-repeatable read)、幻读(phantom read)**的问题。

  • 脏读:一个事务读取到了另一个事务修改但未提交的数据,就发生了脏读。如果另一个事务发生回滚,那么刚才得到的数据就是过期的数据。
  • 不可重复读:一个事务内多次读取同一个数据,前后出现数据不一样的情况,就意为着发生了不可重复读现象。
  • 幻读:一个事务多次查询某个符合查询条件的记录数量,前后查询到的记录数量不一样,就意味着发生了幻读现象。

针对这些事务并发产生的问题,SQL 标准提出了四种隔离级别,隔离级别越高,性能效率就越低,这四个隔离级别如下:

  • (可以)读未提交(read uncommitted,指一个事务还没提交时,它做的变更就能被其他事务看到;

    • 可能发生脏读、不可重复读和幻读现象。
  • 读已提交(read committed,RC,指一个事务提交之后,它做的变更才能被其他事务看到;

    • 避免了脏读。
  • 可重复读(repeatable read,RR,指一个事务执行中,读取到的数据前后一致。是MySQL InnoDB 引擎的默认隔离级别

    • 可能发生幻读现象,但是不可能脏读和不可重复读现象。
  • 串行化(serializable,多个事务如果发生读写冲突,后访问的事务必须等前一个事务执行完成,才能继续执行;

    • 脏读、不可重复读和幻读现象都不可能会发生

四种隔离级别具体的实现方式如下:

  • 对于「读未提交」:直接读取最新的数据就好。
  • 对于「串行化」:通过加读写锁的方式来避免并行访问。
  • 对于「读提交」和「可重复读」:通过 **Read View **来实现,主要区别在于创建 Read View 的时机不同。
    • 「读提交」隔离级别是在「每个语句执行前」都会重新生成一个 Read View,而「可重复读」隔离级别是「启动事务时」生成一个 Read View,然后整个事务期间都在用这个 Read View。

Read View 在MVCC 中是如何工作的?

可以把 Read View 理解成一个数据快照,可重复读隔离级别在启动事务时会生成一个 Read View,然后整个事务期间都在用这个 Read View。读提交隔离级别是在每次读取数据时,都会生成一个新的 Read View。

Read View 有四个重要的字段:

  • creator_trx_id创建该 Read View 的事务 id

  • m_ids :创建 Read View 时,当前数据库中「活跃事务」的事务 id 列表

    • " 活跃事务 "指的就是,启动了但还没提交的事务
  • min_trx_id :创建 Read View 时,当前数据库中「活跃事务」中事务 id 最小的事务,也就是 m_ids 的最小值。

  • max_trx_id :创建 Read View 时,当前数据库中应该给下一个事务的 id 值,也就是全局事务中最大的事务 id 值 + 1。

MVCC全称是多版本并发控制 (Multi-Version Concurrency Control),只有在InnoDB存储引擎下存在。MVCC的机制可以避免同一个数据在不同事务之间的竞争。它只在读已提交和可重复读的事务隔离级别下工作。

在早期的数据库中,只有读读之间的操作才可以并发执行,读写,写读,写写操作都要阻塞,这样就会导致MySQL的并发性能极差。

采用了MVCC机制后,只有写写之间相互阻塞,其他三种操作都可以并行,这样就可以提高了MySQL的并发性能。

聚簇索引的记录中有两个隐藏列,作为实现MVCC的基础:

  • trx_id,保存事务id,表示这个数据是哪个事务生成的。

    • 在创建 Read View 后,我们可以将记录中的 trx_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列表中,表示生成该版本记录的活跃事务已经被提交,所以该版本的记录对当前事务可见
  • roll_pointer,是一个指针,指向上一个版本的记录。每次对某条聚簇索引记录进行改动时,都会把旧版本的记录写入到 undo log 中,于是就可以通过这个指针找到修改前的记录。

可重复读是如何工作的?

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

  • 假设事务 A (事务 id 为51)启动后,紧接着事务 B (事务 id 为52)也启动了,那这两个事务创建的 Read View 如下:

    在这里插入图片描述

    • 此时事务 B 读取小林的账户余额记录,读到余额是 100 万;

      • 因为此时记录的trx_id 为 50,比事务 B 的 Read View 中的 min_trx_id 值(51,最小活跃事务id)还小,这意味着修改这条记录的事务早就在事务 B 启动前提交过了,所以该版本的记录对事务 B 可见的,也就是事务 B 可以获取到这条记录。
    • 随后事务 A 将小林的账户余额记录修改成 200 万,并没有提交事务;

      • 事务 A 通过 update 语句将这条记录修改了(还未提交事务),将小林的余额改成 200 万,这时 MySQL 会记录相应的 undo log,并以链表的方式串联起来,形成版本链。在这里插入图片描述
    • 事务 B 读取小林的账户余额记录,读到余额还是 100 万。

      • 事务 B 第二次去读取该记录,发现这条记录的 trx_id 值为 51,在事务 B 的 Read View 的 min_trx_id 和 max_trx_id 之间,判断 trx_id 值在 m_ids 范围内**,判断的结果是在的,那么说明这条记录是被还未提交的事务修改的,这时事务 B 并不会读取这个版本的记录。而是沿着 undo log 链条往下找旧版本的记录,直到找到 trx_id 小于事务 B 的 Read View 中的 min_trx_id 值的第一条记录,所以事务 B 能读取到的是 trx_id 为 50 的记录,也就是小林余额是 100 万的这条记录。
    • 事务 A 提交事务;

    • 事务 B 读取小林的账户余额记录,读到余额依然还是 100 万。

读提交是如何工作的?

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

  • 假设事务 A (事务 id 为51)启动后,紧接着事务 B (事务 id 为52)也启动了,接着按顺序执行了以下操作:

    • 事务 B 读取数据(创建 Read View),小林的账户余额为 100 万;

      • 找到记录后,会先看这条记录的 trx_id,此时发现 trx_id 为 50,比事务 B 的 Read View 中的 min_trx_id 值(51,最小活跃事务id)还小,这意味着修改这条记录的事务早就在事务 B 启动前提交过了,所以该版本的记录对事务 B 可见的,也就是事务 B 可以获取到这条记录。

        在这里插入图片描述

  • 事务 A 修改数据(还没提交事务),将小林的账户余额从 100 万修改成了 200 万;

  • 事务 B 读取数据(创建 Read View),小林的账户余额为 100 万;

    • 事务 B 在找到小林这条记录时,会看这条记录的 trx_id 是 51,在事务 B 的 Read View 的 min_trx_id 和 max_trx_id 之间,接下来需要判断 trx_id 值是否在 m_ids 范围内,判断的结果是在的,那么说明这条记录是被还未提交的事务修改的,这时事务 B 并不会读取这个版本的记录。而是,沿着 undo log 链条往下找旧版本的记录,直到找到 trx_id 「小于」事务 B 的 Read View 中的 min_trx_id 值的第一条记录,所以事务 B 能读取到的是 trx_id 为 50 的记录,也就是小林余额是 100 万的这条记录。
  • 事务 A 提交事务;

  • 事务 B 读取数据(创建 Read View),小林的账户余额为 200 万;

    • 第三次创建的Read View(m_ids变为只有52,因为A事务id为51的已经提交了,min_trx_id变为52。):事务 B 在找到小林这条记录时,会发现这条记录的 trx_id 是 51,比事务 B 的 Read View 中的 min_trx_id 值(52,最小活跃事务id)还小,这意味着修改这条记录的事务早就在创建 Read View 前提交过了,所以该版本的记录对事务 B 是可见的。在这里插入图片描述

正是因为在读提交隔离级别下,事务每次读数据时都重新创建 Read View,那么在事务期间的多次读取同一条数据,前后两次读的数据可能会出现不一致,因为可能这期间另外一个事务修改了该记录,并提交了事务。

4.2 MySQL可重复读隔离级别,完全解决幻读了吗?

当同一个查询在不同的时间产生不同的结果集时,事务中就会出现所谓的幻象问题。例如,如果 SELECT 执行了两次,但第二次返回了第一次没有返回的行,则该行是“幻像”行。

MySQL InnoDB 引擎的默认隔离级别虽然是「可重复读」,但是它很大程度上避免幻读现象

  • 在可重复读隔离级别中,普通的 select 语句就是基于 MVCC 实现的快照读,也就是不会加锁的。而MySQL 里除了普通查询是快照读,其他都是当前读,比如 update、insert、delete,这些语句执行前都会查询最新版本的数据,然后再做进一步的操作。

    • 快照读(普通 select 语句),是通过 **MVCC(多版本控制)方式解决了幻读。**因为可重复读隔离级别下,开始事务并执行第一个查询语句后,会创建Read View,通过这个 Read View 就可以在 undo log 版本链找到事务开始时的数据,所以事务过程中每次查询的数据都是一样的,即使中途有其他事务插入了新纪录,是查询不出来这条数据的,所以就很好了避免幻读问题。

    • 当前读(select … for update 等语句),是**通过 next-key lock(记录锁+间隙锁)方式解决了幻读。**因为当执行 select … for update 语句的时候,会加上 next-key lock,如果有其他事务在 next-key lock 锁范围内插入了一条记录,那么这个插入语句就会被阻塞,无法成功插入,所以就很好了避免幻读问题。

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

        事务 A 执行了下面这条锁定读语句后,就在对表中的记录加上 id 范围为 (2, +∞] 的 next-key lock(next-key lock 是间隙锁+记录锁的组合)。

        img

        然后,事务 B 在执行插入语句的时候,判断到插入的位置被事务 A 加了 next-key lock,于是事物 B 会生成一个插入意向锁,同时进入等待状态,直到事务 A 提交,这就避免了幻读。

  • 幻读被完全解决了吗?

    • 第一个例子:因为当事务 A 更新了一条事务 B 插入的记录(此时事务A看不见该记录),那么事务 A 前后两次查询的记录条目就不一样了,所以就发生幻读。
      • 因为进行更新操作时,新记录的 trx_id 隐藏列的值就分配为事务 A 的事务 id,此时A再使用普通SELECT语句去查询该记录时就可以看到这条记录了。
    • 第二个例子:如果事务开启后,并没有执行当前读,而是先快照读(此时没有加锁),然后这期间如果其他事务插入了一条记录,那么事务后续使用当前读进行查询的时候,就会发现两次查询的记录条目就不一样了,所以就发生幻读。

    由此可见,**MySQL 可重复读隔离级别并没有彻底解决幻读,只是很大程度上避免了幻读现象的发生。**要避免这类特殊场景下发生幻读的现象的话,就是尽量在开启事务之后,马上执行 select … for update 这类当前读的语句,因为它会对记录加 next-key lock,从而避免其他事务插入一条新记录。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Sivan_Xin

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值