mysql中的隔离性原理详解
作者:meihaoshy
数据库并发的三种场景
- 读-读 :不存在任何问题,也不需要并发控制
- 读-写 :有线程安全问题,可能会造成事务隔离性问题,可能遇到脏读,幻读,不可重复读
- 写-写 :有线程安全问题,可能会存在更新丢失问题
在这三种场景中 读-读几乎没有任何问题 所以我们不需要并发控制
写-写并发只需要加锁控制即可
所以说我们今天重点讨论下读-写并发
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隔离性内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!