mysql-事务隔离之可重复读

本文默认使用的 mysql 数据库引擎为 InnoDB,事务隔离级别为可重复读(repeatable read)

问题起因

最近在业务的开发过程中,碰到了一个问题。

线程A对某一数据进行更新操作之后(存在事务),立即开启线程B处理异步任务,查询该数据发现它并没有更新,导致后续操作失败。

分析问题后发现和数据库事务隔离级别相关,开发环境中 mysql 数据库的事务隔离级别设置的是可重复读(repeatable read)。

问题分析:

  • 线程A对数据进行更新操作,此时存在事务A,并且还没有提交;

  • 开启线程B处理异步任务,此时开启了一个新事务B,事务B开启时由于事务A还没有提交,因此读不到事务A导致的数据变化;

虽然找到了问题的原因,但是对于数据库如何实现这种事务隔离机制还是充满疑惑,借此机会,花了些时间了解 mysql 关于事务隔离的相关实现,在此记录学习笔记。

事务特性

在计算机领域,特别是数据库存储领域对于事务的运用比较广泛。简单来说事务将一个或者一组相关操作绑定在一块,事务中的操作要不都成功,要不都失败,很大程度保证了数据的一致性。

Mysql 原生的数据库引擎 MyISAM 是不支持事务的,后续出现了支持事务的数据库引擎 InnoDB, 因为这一特性也让 InnoDB 逐步成为当今主流的数据库引擎。

原子性(Atomicity)

一个事务必须被视为一个不可分割的最小工作单元,整个事务中的所有操作要么全部提交成功,要么全部失败回滚,对于一个事务来说,不可能只执行其中的一部分操作,这就是事务的原子性。

一致性(Consistency)

数据库总是从一个一致性的状态转换到另一个一致性的状态。

隔离性(Isolation)

隔离性是指,事务内部的操作与其他事务是隔离的,并发执行的各个事务之间不能互相干扰。严格的隔离性,对应了事务隔离级别中的Serializable (可串行化),但实际应用中出于性能方面的考虑很少会使用可串行化。本文主要讲解隔离性的相关实现。

持久性(Durability)

事务一旦提交,它对数据库的改变就应该是永久性的。

隔离级别

在谈隔离级别之前,首先要知道,隔离得越严实,效率就会越低。

Mysql 的 InnoDB 引擎支持以下 4 种隔离级别:

  • 读未提交(read uncommitted):一个事务还没提交时,它做的变更就能被别的事务看到。
  • 读提交(read committed):一个事务提交之后,它做的变更才会被其他事务看到。
  • 可重复读(repeatable read):一个事务执行过程中看到的数据,总是跟这个事务在启动时看到的数据是一致的。当然在可重复读隔离级别下,未提交变更对其他事务也是不可见的。
  • 串行化(serializable ):对于同一行记录,“写”会加“写锁”,“读”会加“读锁”。当出现读写锁冲突的时候,后访问的事务必须等前一个事务执行完成,才能继续执行。

上述的隔离级别由上至下,隔离性逐渐增强,效率逐渐下降。

读提交的隔离级别下,在sql语句执行时,会创建视图来保证数据读取的逻辑;注意这里的视图区别于我们平时所讲的虚拟表视图概念,这里称它为事务一致性视图

可重复读隔离级别,在事务开启时会创建一致性视图,在事务进行期间,数据读取逻辑都依赖于视图。

读未提交隔离级别下是直接返回记录的最新值;而串行化隔离级别下直接加读写锁来保证并发事务的有序性;它们都不涉及视图的创建。

这里再讲一下在并发情况下,这几种事务隔离级别对数据读取的影响。

顺便解释一下几个数据读取问题:

  • 脏读:读到其它事务还没有提交的数据更新,如果之后该事务出现异常回滚了,这就导致了脏数据的产生;

  • 不可重复读:同一事务先后两次读取同一个数据,两次读取的结果不一样,这种现象称为不可重复读。脏读与不可重复读的区别在于前者读到的是其他事务未提交的数据,后者读到的是其他事务已提交的数据。

  • 幻读:同一事务按照某个条件先后两次查询数据库,两次查询结果的条数不同,这种现象称为幻读。不可重复读与幻读的区别可以通俗的理解为前者是数据变了,后者是数据的行数变了。

可重复读

本文主要讲解 Mysql 关于可重复读的实现,可以认为前面的内容都只是铺垫,所以没有详细地展开论述。

再回顾我在开头提到的问题:

在可重复读隔离级别下,先后开启的两个事务 A,B,事务 A 对数据进行更新,在当前事务中是能够读到更新,在事务 B 中却读取不到这个更新。

上面提到,在可重复读隔离级别下事务开启时会创建一致性视图,事务进行期间数据读取逻辑都依赖视图,这个视图可以理解为那个时刻数据库的『快照』。

当然这个快照的实现方式和我们平时想的不太一样,我一开始认为是直接拷贝一份数据库的数据,但这显然是不现实的,如果一个数据库本身存储的数据量非常大,通过这种 Copy 的方式打快照那得多慢啊。

实际上数据库存储的数据存在 3 个隐藏列,并且同一行数据在某一时刻可能存在多个版本的数据,这是实现多版本并发控制(MVCC)的基础。下图为某一数据行第一次被插入到表中的数据状态(紫色区域为隐藏字段)。

  • 事务IDDB_TRX_ID字段: 标记了最新更新这条行记录的transaction id,每处理一个事务,其值自动+1
  • 回滚指针(DB_ROLL_PTR)字段 : 指向当前记录项的rollback segment的 undo log(撤销日志记录), 找之前版本的数据就是通过这个指针
  • DB_ROW_ID字段: 当由innodb自动产生聚集索引时,聚集索引包括这个DB_ROW_ID的值,否则聚集索引中不包括这个值,这个用于索引当中。

1.假设事务A(transaction_id = 1)对数据行进行了修改,将 Field2 的值改为 3,效果如下:

当事务 A 更改该行的值时,会进行一下操作:

  • 用排它锁锁定该行
  • 把修改前的值 Copy 到 undo log
  • 修改当前行的值,填写事务编码,使回滚指针指向 undo log 中上个版本的数据 V1

2.假设事务A还没有提交,此时事务B(transaction_id = 2)也对改行数据进行了修改,效果如下:

当事务 B 对该行进行更改时,操作和前面一样,此时可以看到同一行存在 3 个版本的数据V1,V2,V3。

如果这个时候,事务 A 查询该行数据,应该选择哪一个版本的数据?这就涉及一致性视图了。

一致性视图

在事务开启时(RR 隔离级别下),会创建一个视图,那么这个视图包含什么信息呢?

在实现上, InnoDB 为每个事务构造了一个数组,用来保存这个事务启动瞬间,当前正在“活跃”的所有事务 ID。“活跃”指的就是,启动了但还没提交。

数组里面事务 ID 的最小值记为低水位,当前系统里面已经创建过的事务 ID 的最大值加 1 记为高水位。

这个视图数组和高水位,就组成了当前事务的一致性视图(read-view)。

而数据版本的可见性规则,就是基于数据的 row trx_id 和这个一致性视图的对比结果得到的。

这个视图数组把所有的 row trx_id 分成了几种不同的情况。

本段内容引用自极客时间专栏《Mysql实战45讲》第 8 讲——事务到底是隔离的还是不隔离的?讲得挺不错的,有兴趣的小伙伴可以去支持作者。

读取逻辑

讲完了一致性视图,接下来通过这个一致性视图,来讲解数据的查找逻辑。

再回顾一下上面例子中提到的问题:

  • 事务 A 先开启,transaction_id = 1,此时它的视图数组{1,2},将 Field2 值更新 2 -> 3
  • 事务 B 开启,transaction_id = 2,此时它的视图数组{1,2,3},将 Field2 值更新 3 -> 4
  • 事务 A 查询该行数据,此时该行数据存在 3 个版本

事务 A 应该读取哪个版本的数据?

读取逻辑如下:

1.事务 A 找到最新版本 V3 查看数据行的 DB_TRX_ID = 2,发现它落在视图数组{1,2}的高水位区,表示这个版本是由将来启动的事务更新的,对于当前事务不可见;

2.通过 undo log 找到上个版本的数据 V2 查看数据行的 DB_TRX_ID = 1,发现这个版本是当前事务更新的,因此可以直接读取数据;

3.最终事务 A 找到符合要求的 V2 版本数据作为查询结果返回;

这也解答了我在文章开头提到的疑问:

数据库如何实现可重复读这种事务隔离机制?

概括总结如下,一个数据版本,对于一个事务视图来说,除了自己的更新总是可见以外,有三种情况:

  • 版本未提交,不可见;
  • 版本已提交,但是是在视图创建后提交的,不可见;
  • 版本已提交,而且是在视图创建前提交的,可见。

注意:在可重复读的隔离级别下,更新操作都是当前读,也就是先读取数据的最新版本,再进行更新操作,并且此时会加写锁

总结

这篇文章,主要涉及的内容如下:

  • 不同事务间数据更新不可见问题
  • 事务隔离级别概述
  • 可重复读的实现机制
  • 一致性视图的概念

总结这篇文章的过程,也算是解决了一个困扰我挺久的疑惑,如果本文对你有所帮助,欢迎留言点赞。

评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值