MySQL -- MVCC
前言
最近在学MySQL
,决定记录一下,能写多少写多少,不定时更新,加油。
正文
分几个部分来吧,大致如下:
-
字符集与比较规则
-
行格式与数据页
-
InnoDB
索引 -
访问方法与连接
-
explain 与 子查询优化
-
redo
与undo
日志 -
MVCC
与 锁
本文为第四部分
MVCC
原理解析
主要就是想先写这个, 其他的后面有空再补~
由于我们跳过一些东西, 下面说MVCC的原理可能会各种懵
所以, 我们先简单过几个点吧.
一、温故知新 -- 两个隐藏列的含义
行格式的时候说过,InnoDB会为每行加上两个隐藏列(row_id并不是必加的)trx_id
和 roll_pointer
这俩哥们简直生猛的一塌糊涂.
小声明:
事务T1的编号是100
事务T2的编号是200
-
trx_id
这个明显是事务ID的意思, 就是记录一下最近操作此条记录的事务ID. 比如事务T1中插入了一条记录X,那X的trx_id
就是100,如果在T1中继续操作这条记录是不会修改X的trx_id
的.如果事务T2修改了这X记录,那么其trx_id
就变成200. -
roll_pointer
字面意思就是回滚指针, 指针存放的是一个地址, 指向了某个值. 它指向的是一条undo日志记录
.
这里简单提一下undo日志
,后面会专门写一篇详细讲的[有生之年系列]。
一条undo日志
就是一条记录, 存放在页中,叫undo日志页
.大致分为两种,插入类型(insert
)和修改类型(update
、delete
),这里只需要知道他们有一个很大区别:插入类型的undo日志是没有指向下一条undo日志的属性的,也就是说他们组成了一个undo日志链表,又称版本链
。
- 第一条是真实记录,0是记录类型,H是记录头,300是
trx_id
R是roll_pointer
, 1、3.. 是记录的真实数据 - 可以看到每条undo日志都有一个事务ID(图中我画在最后面,实际并不是存在最后),这个属性其实是叫
old trx_id
, 表示当前undo日志
对应的事务ID,也称此版本的创建事务ID
- 插入类型undo日志没有
old roll_pointer
指向上一条roll_pointer
,因为它本来就是版本链
的第一条 - 这条undo链表可以看出,此记录由事务ID为100的事务插入,在事务ID为200的事务中修改了两次,而事务ID为300的事务正在修改此记录。
- 记住这个顺序,后面有用。
二、老生常谈 -- 隔离级别
说这个隔离级别之前,我们先想想为什么要有这个东东?
其实每个新技术或新名词的出现, 都可以问这几个问题
1.这个东东解决了什么问题吗?
2.现有的技术解决不了吗?
3.如果能, 它比当前的解决方案强在哪些方面呢?
4.不足或改进之处.
- 以上纯属扯淡
随着互联网的发展,并发已经是一道绕不开的坎。各种问题都不断冒出来,那并发事务访问数据库会发生什么样的问题呢?
一个一个来看下。
- 脏写(
Dirty Write
)
T1事务开启
T1修改:X=5
T2事务开始
T2修改:X=6
T2事务提交
T1事务提交
结果:X=5
这个时候T2的事务所做修改就丢失了.
一个事务修改了另一个未提交事务修改过的数据,此为脏写。
- 脏读(
Dirty Read
)
数据状态:X=5
T1事务开启
T2事务开始
T2修改:X=6
T1读取:X=6
T2事务回滚
T1事务提交
T1读到的X=6,库中X=5
一个事务读取了另一个未提交事务修改过的数据,此为脏读。
- 不可重复读(
Non-Repeatable Read
)
事务T1开启
事务T2开启
T1读取:X=5
T2修改:X=6
事务T2提交
T1读取:X=6
。。。
T1两次读取到的同一条记录的值不一样。
每次事务提交后,当前事务都能读取到记录的最新值,此为不可重复读。
- 幻读(
Phantom
)
库中数据: X=6
事务T1开启
事务T2开启
T1读取:X>5 (得到一条X=6)
T2新增:X=7
T1读取:X>5 (得到两条X=6和X=7)
。。。
T1第二次读取的记录数量比第一次读取到的记录数量多。
如果事务T1根据条件N查询数据,事务T2添加了满足条件N的记录并提交了,T1再次根据N查询数据能查询T2新增的记录,此为幻读。
注意几个点:
- 不可重复读是针对单条记录的改动(包括删除与修改)
- 幻读是针对查询条件的范围内记录的新增
- 幻读只是针对新增,如果有范围内记录的删除或修改,都属于不可重复读
为了解决这些问题,有一帮人提出了一个SQL标准,给出了四种隔离级别,用以解决上诉问题:
READ UNCOMMITTED
READ COMMITTED
READ REPEATABLE
SERIALABLE
SQL标准中规定,
-
READ UNCOMMITTED
解决脏写 -
READ COMMITTED
解决脏读 -
READ REPEATABLE
解决脏读与不可重复读 -
SERIALABLE
解决幻读
数据库对脏写的问题是零容忍,哪怕最低的隔离级别都不允许出现
然而MySQL里的大佬还是牛逼,他们在READ REPEATABLE
级别就已经解决了幻读问题。
实现一般有两种方式,第一是加锁,第二是MVCC
。
实际上,二者都有用到.
三、千呼万唤 -- MVCC的原理
ReadView登场
先看下其大致结构
ReadView结构-
m_ids
存放当前系统中活跃的事务集合 -
min_trx_id
为m_ids
中最小的事务ID -
max_trx_id
分配给下一个开启事务的事务ID -
creator_trx_id
创建此ReadView的事务ID
事务ID注意两点:
- 只有在对表中的记录做改动时(执行
INSERT
、DELETE
、UPDATE
这些语句时)才会为事务分配事务id,否则在一个只读事务中的事务id值都默认为0。- 按分配顺序递增
怎么突然蹦出来一个ReadView
? 先看看怎么用.
如果你不是一条鱼的话,应该还记得前面说过,每条记录都有一个版本链
吧,当一个事务要访问某条记录时,对着这个ReadView
的操作是这样的:
- 判断
trx_id
与creator_trx_id
是否相等,是则意味着此版本正在被当前事务操作,可以访问 - 判断
trx_id
是否小于min_trx_id
,表示此版本的生成事务已经提交,可以访问 - 判断
trx_id
是否大于等于max_trx_id
,表示此版本的生成事务已经提交,可以访问 - 判断
trx_id
是否在m_ids
中,若不在,则意味着此版本的创建事务已经提交,可以访问;若在,则表明此版本在创建ReadView时还在活动,不能访问。
若无法访问则顺着版本链找下一个版本,如果到最后一个版本(也就是Insert的undo日志)仍无法访问,那么此记录对当前事务不可见。
现在知道这玩意有多牛逼了吧~
那跟隔离级别有啥关系呢?
READ COMMITTED
与 READ REPEATABLE
的最大区别就是生成ReadView
的时机不一样
-
READ REPEATABLE
事务开启时生成 -
READ COMMITTED
每次查询前生成
想想
ReadView
与其生成时机如何能解决脏读
/幻读
问题~
喊我来加班,到公司都写完一篇MVCC了,还没见到人~~~~[允悲]