数据库并发的三种场景
- 读-读 :不存在任何问题,也不需要并发控制
- 读-写 :有线程安全问题,可能会造成事务隔离性问题,可能遇到脏读,幻读,不可重复读
- 写-写 :有线程安全问题,可能会存在更新丢失问题
在这三种场景中 读-读几乎没有任何问题 所以我们不需要并发控制
写-写并发只需要加锁控制即可
所以说我们今天重点讨论下读-写并发
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快照
总结