【Hello mysql】 深入理解隔离性

本文涉及的产品
RDS MySQL Serverless 基础系列,0.5-2RCU 50GB
云数据库 RDS MySQL,集群系列 2核4GB
推荐场景:
搭建个人博客
云数据库 RDS PostgreSQL,集群系列 2核4GB
简介: 【Hello mysql】 深入理解隔离性

数据库并发的三种场景

  • 读-读 :不存在任何问题,也不需要并发控制
  • 读-写 :有线程安全问题,可能会造成事务隔离性问题,可能遇到脏读,幻读,不可重复读
  • 写-写 :有线程安全问题,可能会存在更新丢失问题

在这三种场景中 读-读几乎没有任何问题 所以我们不需要并发控制

写-写并发只需要加锁控制即可

所以说我们今天重点讨论下读-写并发

MVCC

基本介绍

多版本并发控制( MVCC )是一种用来解决 读-写冲突 的无锁并发控制

为事务分配单向增长的事务ID,为每个修改保存一个版本,版本与事务ID关联,读操作只读该事务开始前的数据库的快照。 所以 MVCC 可以为数据库解决以下问题

  • 在并发读写数据库时,可以做到在读操作时不用阻塞写操作,写操作也不用阻塞读操作,提高了数据库并发读写的性能
  • 同时还可以解决脏读,幻读,不可重复读等事务隔离问题,但不能解决更新丢失问题

在我们理解MVCC之前 我们需要知道三个前提知识

  • 3个记录隐藏字段
  • undo 日志
  • Read View

我们下面就分别先介绍下这三个隐藏字段

三个前提知识介绍

三个隐藏字段

  • DB_TRX_ID :6 byte,最近修改( 修改/插入 )事务ID,记录创建这条记录/最后一次修改该记录的事务ID
  • DB_ROLL_PTR : 7 byte,回滚指针,指向这条记录的上一个版本(简单理解成,指向历史版本就行,这些数据一般在 undo log 中)
  • DB_ROW_ID : 6 byte,隐含的自增ID(隐藏主键),如果数据表没有主键, InnoDB 会自动以DB_ROW_ID 产生一个聚簇索引
  • 实际还有一个删除flag隐藏字段, 既记录被更新或删除并不代表真的删除,而是删除flag变了

假设我们现在创建并且插入了一条数据 代码和显示如下

mysql> create table if not exists student(
name varchar(11) not null,
age int not null
);
mysql> insert into student (name, age) values ('张三', 28);
Query OK, 1 row affected (0.05 sec)
mysql> select * from student;
+--------+-----+
| name | age |
+--------+-----+
| 张三 | 28 |
+--------+-----+
1 row in set (0.00 sec)

实际上在Linux隐藏字段的效果就是

对于上图做出一定解释

  • 假设插入的事务ID是9 那么TRX_ID字段实际上就是9
  • 因为这是我们插入的第一个数据 所以说隐式主键就是1
  • 因为这是第一个数据 没有更前面的数据了 所以说回滚指针指向的就是空
  • 其实还有其他的隐藏字段 比如说flag等 上面没有标识出

undo log日志

mySQL 将来是以服务进程的方式,在内存中运行。我们之前所讲的所有机制:索引,事务,隔离性,日志等,都是在内存中完成的,即在 MySQL 内部的相关缓冲区中,保存相关数据,完成各种判断操作。然后在合适的时候,将相关数据刷新到磁盘当中的。

所以,我们这里理解undo log,简单理解成,就是 MySQL 中的一段内存缓冲区,用来保存日志数据的就行。

read view 快照

关于快照读的知识下面模拟MVCC场景的时候会讲

Read View就是事务进行 快照读 操作的时候生产的 读视图 (Read View),在该事务执行的快照读的那一刻,会生成数据库系统当前的一个快照,记录并维护系统当前活跃事务的ID(当每个事务开启时,都会被分配一个ID, 这个ID是递增的,所以最新的事务,ID值越大)

Read View 在 MySQL 源码中,就是一个类,本质是用来进行可见性判断的。 即当我们某个事务执行快照读的时候,对该记录创建一个 Read View 读视图,把它比作条件,用来判断当前事务能够看到哪个版本的数据,既可能是当前最新的数据,也有可能是该行记录的 undo log 里面的某个版本的数据。

下面是 ReadView 结构,但为了减少同学们负担,我们简化一下

class ReadView {
  // 省略...
private:
  /** 高水位:大于等于这个ID的事务均不可见*/
  trx_id_t m_low_limit_id;
  /** 低水位:小于这个ID的事务均可见 */
  trx_id_t m_up_limit_id;
  /** 创建该 Read View 的事务ID*/
  trx_id_t m_creator_trx_id;
  /** 创建视图时的活跃事务id列表*/
  ids_t m_ids;
  /** 配合purge,标识该视图不需要小于m_low_limit_no的UNDO LOG,
  * 如果其他视图也不需要,则可以删除小于m_low_limit_no的UNDO LOG*/
  trx_id_t m_low_limit_no;
  /** 标记视图是否被关闭*/
  bool m_closed;
  // 省略...
};
m_ids; //一张列表,用来维护Read View生成时刻,系统正活跃的事务ID
up_limit_id; //记录m_ids列表中事务ID最小的ID
low_limit_id; //ReadView生成时刻系统尚未分配的下一个事务ID,也就是目前已出现过的事务ID的最大值+1
creator_trx_id //创建该ReadView的事务ID

我们在实际读取数据版本链的时候,是能读取到每一个版本对应的事务ID的,即:当前记录的DB_TRX_ID 。

那么,我们现在手里面有的东西就有,当前快照读的 ReadView 和 版本链中的某一个记录的DB_TRX_ID 。

那么 现在我们的问题就是 当前快照读,应不应该读到当前版本记录。一张图,解决所有问题!

如果查到不应该看到当前版本,接下来就是遍历下一个版本,直到符合条件,即可以看到。上面的readview 是当你进行select的时候,会自动形成。

看到这里有的同学可能会产生这样一个疑问 如何遍历下个版本呢?

我们之前说过 undo log其实就是一个缓冲区 并且里面有着回滚指针连接着的各种数据 (实际上就是单链表连接的各种数据)

再次总结下

  • 我们第一次开启事务的时候会生成一个read view的结构体
  • 该结构体中会记录活跃的最小事务id和比最大事务id还要大一的事务id
  • 当我们第一次select读的时候会形成一个快照
  • 如果说当前版本的TRX_ID小于最小的id 那么我们就可以见到
  • 如果当前版本的TRX_ID大于等于最大ID我们就不能见到
  • 如果说在最小和最大区间里面 并且该TRX_ID不是活跃ID(已提交) 则我们可以看到
  • 如果说在最小和最大区间里面 并且TRX_ID还是活跃ID(未提交) 则我们不能看到

转化成现实中的例子

现在的我们能够看到我们出生之前所有人写的作品 但是我们不能看到还未出生的人写的作品

如果说写书的人跟我们同一个时代 我们就要判断这本书有没有发表 (是否提交) 如果提交了我们就能看见 如果没有提交 我们就不能看见

模拟MVCC场景

MVCC场景中有增删改查 下面我们分别进行讨论

我们插入的时候只需要形成一条新的undo log版本链 将回滚指针指向前面的数据 如果需要回滚直接通过回滚指针找到需要覆盖的数据进行覆盖即可

我们前面说过了 mysql中还有一个隐藏的falg字段 因此 如果需要删除的话 只需要将flag标志位设置即可

这是最麻烦的一个环节 我们使用一个例子来说明MVCC中的改

现在一个表中有如下的记录

现在有一个事务ID为10的事务 要修改表中的name张三为李四

  • 因为要修改 所以我们肯定要先给记录上锁
  • 修改之前 我们要将改之前的数据拷贝一份要undo log当中 假设地址为0x11223344
  • 之后我们修改原始数据中name为李四 并且将回滚指向0x11223344这个地址
  • 事务10commit提交 释放锁

过程图如下

如果还有事务要修改新的数据就参考上面的步骤即可

于是乎我们就形成了一条基于链表记录的历史版本链 undo log里面的一个个历史版本就称为快照

现在我们明白了

  • 所谓的回滚其实就是拿历史版本链中的某条数据覆盖当前数据

首先我们要理解两个概念 当前读和快照读

  • 当前读:读取最新的记录,就是当前读。增删改,都叫做当前读,select也有可能当前读,比如select lock in share mode(共享锁), select for update
  • 快照读:读取历史版本。快照读不会被加锁。

多个事务同时增删改的时候是当前读 需要加锁 在串行化的隔离级别下 select也是当前读 需要加锁

如果select是快照读 那么和增删改的当前读不冲突 所以说并行效率高 事务的隔离级别决定了select是当期读还是快照读 具体的判断方法可以参考前面read view部分的知识

RR和RC的区别

RR级别测试

演示一 两边开启事务 右边先进行快照读 左边插入数据之后commit 右边再进行快照读和当前读

我们可以发现的是 当右边使用快照读的时候不管左边有没有commit 读取到的数据是一样的

而使用当前读的时候 我们可以发现读取的数据就是最新的数据了 光靠这个一个试验我们看不出来什么 接下来我们看演示二

演示二: 左右两边同时开启一个事务 左边先插入数据之后提交 右边在左边提交之后进行快照读

我们发现 这个时候右边的快照读 读取了最新的数据

对比这两次试验加上之前的read view部分学习我们不难做出以下的推断

  • 在RR级别下 第一次select快照读的时候会生成一个read view快照 之后的读取就按照这个快照进行

而实际上在RC级别中 每一次的select快照都都会生成一个最新的read view快照

所以说RR和RC最本质的区别就是 RR只会生成依次read view快照 而RC快照读几次就会生成几次快照

四种隔离级别的不同处理方式

读–未提交

直接当前读 不加锁

串行化

当前读 加锁

读 提交

在RC级别中 每次的select读取都是快照读 每次都会生成一个最新的read view快照

可重复读

在RR级别中 每次select读取都是快照读 并且都会遵循第一次select读取时生成的read view快照

总结

相关实践学习
如何在云端创建MySQL数据库
开始实验后,系统会自动创建一台自建MySQL的 源数据库 ECS 实例和一台 目标数据库 RDS。
全面了解阿里云能为你做什么
阿里云在全球各地部署高效节能的绿色数据中心,利用清洁计算为万物互联的新世界提供源源不断的能源动力,目前开服的区域包括中国(华北、华东、华南、香港)、新加坡、美国(美东、美西)、欧洲、中东、澳大利亚、日本。目前阿里云的产品涵盖弹性计算、数据库、存储与CDN、分析与搜索、云通信、网络、管理与监控、应用服务、互联网中间件、移动服务、视频服务等。通过本课程,来了解阿里云能够为你的业务带来哪些帮助     相关的阿里云产品:云服务器ECS 云服务器 ECS(Elastic Compute Service)是一种弹性可伸缩的计算服务,助您降低 IT 成本,提升运维效率,使您更专注于核心业务创新。产品详情: https://www.aliyun.com/product/ecs
相关文章
|
关系型数据库 MySQL 索引
【Hello mysql】 mysql的视图
【Hello mysql】 mysql的视图
45 0
|
关系型数据库 MySQL
【Hello mysql】 mysql的内外连接 (重点)
【Hello mysql】 mysql的内外连接 (重点)
58 0
|
关系型数据库 MySQL
【Hello mysql】 mysql的复合查询 (重点)
【Hello mysql】 mysql的复合查询 (重点)
227 0
|
关系型数据库 MySQL 数据库
【Hello mysql】 mysql的内置函数(下)
【Hello mysql】 mysql的内置函数
62 0
|
SQL 关系型数据库 MySQL
【Hello mysql】 mysql的内置函数(上)
【Hello mysql】 mysql的内置函数
46 0
|
SQL 关系型数据库 MySQL
【Hello mysql】 mysql的约束(上)
【Hello mysql】 mysql的约束
57 0
|
存储 SQL 算法
【Hello mysql】 mysql数据类型(下)
【Hello mysql】 mysql数据类型(下)
68 0
|
存储 关系型数据库 MySQL
【Hello mysql】 mysql数据类型(上)
【Hello mysql】 mysql数据类型
61 0
|
存储 关系型数据库 MySQL
【Hello mysql】 数据库表操作
【Hello mysql】 数据库表操作
44 0
|
SQL 关系型数据库 MySQL
【Hello mysql】 数据库库操作(下)
【Hello mysql】 数据库库操作(下)
51 1