深入了解MVCC数据库并发控制
问题:
我们知道这个是因为数据库的隔离级别,那到底是怎么实现的呢?
思考这个问题的同时我们先了解一下mvcc的一些定义
定义:
MVCC全称Mutli Version Concurreny Control,多版本并发控制,也可称之为一致性非锁定读;它通过行的多版本控制方式来读取当前执行时间数据库中的行数据。实质上使用的是快照数据,这样就可以实现不加锁读。MVCC 主要应用于 Read Commited 和 Repeatable read 两个事务隔离级别。
MVCC能解决什么问题,好处是?数据库并发场景有三种,分别为
读-读:不存在任何问题,也不需要并发控制
读-写:有线程安全问题,可能会造成事务隔离性问题,可能遇到脏读,幻读,不可重复读
写-写:有线程安全问题,可能会存在更新丢失问题
MVCC带来的好处是?
多版本并发控制(MVCC)是一种用来解决读-写冲突的无锁并发控制,也就是为事务分配单向增长的时间戳,为每个修改保存一个版本,版本与事务时间戳关联,读操作只读该事务开始前的数据库的快照。 所以MVCC可以为数据库解决以下问题
在并发读写数据库时,可以做到在读操作时不用阻塞写操作,写操作也不用阻塞读操作,提高了数据库并发读写的性能 同时还可以解决脏读,幻读,不可重复读等事务隔离问题,但不能解决更新丢失问题
好的,现在进入正题
MVCC实现
MVCC 只是一种 乐观 的实现形式,它是通过 一种 可见性算法 来实现数据库并发控制
MVCC的实现依赖于 每行的隐藏字段,DB_TRX_ID,DB_ROLL_PTR,删除标记位,还有read_view
1 DB_TRX_ID 事务id占6 字节,表示这一行数据最后插入或修改的事务id。此外删除在内部也被当 作一次更新,在行的特殊位置添加一个删除标记(记录头信息有一个字节存储是否删除的标记)。2 DB_ROLL_PTR 回滚指针占7字节,回滚指针指向被写在Rollback segment中的undoLog记录,在该行数据被更新的时候,undoLog 会记录该行修改前内容到undoLog。
3 DB_ROW_ID 行ID占7字节,他就项自增主键一样随着插入新数据自增。如果表中不存主键 或者 唯一索引,那么数据库 就会采用DB_ROW_ID生成聚簇索引。否则DB_ROW_ID不会出现在索引中
其实还有一个删除的flag字段,用来判断该行记录是否已经被删除
MVCC 的两种读形式
在讲 MVCC 的实现原理之前,我觉很有必要先去了解一下 MVCC 的两种读形式。
快照读:读取的只是当前事务的可见版本,不用加锁。而你只要记住 简单的 select 操作就是快照读(select * from table where id = xxx)。
当前读:读取的是当前版本,比如 特殊的读操作,更新/插入/删除操作
而 MVCC 使用的是其中的** 事务字段,回滚指针字段,是否删除字段**。我们来看一下现在的表格
删除标志 事务Id 回滚指针 id name password
isDelete DB_TRX_ID DB_ROLL_PTR id name password
true/flase ax11122… ax11122… 1 小明 *****
那么如何通过这三个字段来实现 MVCC 的 呢?还差点东西! undoLog(回滚日志) 和 read-view(读视图)。
undoLog: 事务的回滚日志,是 可见性算法 的非常重要的部分,分为两类。
insert undo log:事务在插入新记录产生的undo log,当事务提交之后可以直接丢弃
update undo log:事务在进行 update 或者 delete 的时候产生的 undo log,在快照读的时候还是需要的,
所以不能直接删除,只有当系统没有比这个log更早的read-view了的时候才能删除。ps:所以长事务会
产生很多老的视图导致undo log无法删除 大量占用存储空间。
read-view: 读视图,是MySQL秒级创建视图的必要条件,比如一个事务在进行 select 操作(快照读)的时候
会创建一个 read-view ,这个read-view 其实只是三个字段。
alive_trx_list:read-view生成时刻系统中正在活跃的事务id。
up_limit_id:记录上面的 alive_trx_list 中的最小事务id。
low_limit_id:read-view生成时刻,目前已出现的事务ID的最大值 + 1。
其实主要思路就是:当生成read-view的时候如何去拿获取的 DB_TRX_ID 去和 read-view 中的三个属性(上面讲了)去作比较。我来说一下三个步骤,如果不是很理解可以参考着我后面的实践结合着去理解。
首先比较这条记录的 DB_TRX_ID 是否是 小于 up_limit_id 或者 等于当前事务id。如果满足,那么说明当前事务能看到这条记录。如果大于则进入下一轮判断
然后判断这条记录的 DB_TRX_ID 是否 大于等于 low-limit-id。如果大于等于则说明此事务无法看见该条记录,不然就进入下一轮判断。
判断该条记录的 DB_TRX_ID 是否在活跃事务的数组中,如果在则说明这条记录还未提交对于当前操作的事务是不可见的,如果不在则说明已经提交,那么就是可见的。
如果此条记录对于该事务不可见且 ROLL_PTR 不为空那么就会指向回滚指针的地址,通过undolog来查找可见的记录版本。
流程图
回到前面的问题
首先事务A开启了事务(当然这不算开启,在RR模式下 真正获取read-view的是在进行第一次进行快照读的时候)。我们假设事务A的事务id为2,事务B的id为3。
然后事务A进行了更新操作,如图所示,更新操作创建了一个新的版本并且新版本的回滚指针指向了旧的版本(注意 undo log其实存放的是逻辑日志,这里为了方便我直接写成物理日志)。
首先,在进行快照读的时候我们会创建一个 read-view 这个时候我们的 read-view 是
up-limit-id = 2
alive-trx-list = [2,3]
low-limit-id = 4
然后我们获取那两个没有被修改的记录(没有顺序,这里为了一起解释方便)
我们获取到(2,小方)和(3,小张)这两条记录,发现他们两的 DB_TRX_ID = 1
我们先判断 DB_TRX_ID 是否小于 up-limit-id 或者等于当前事务id
发现 1<2 小于 up-limit-id ,则可见 直接返回视图。
然后我们获取更改了的数据行
其实你也发现了这是一个链表,此时链表头的 DB_TRX_ID 为 2
我们进行判断 2 < 2 不符合,进入下一步判断
判断 DB_TRX_ID >= low_limit_id 发现此时是 2 >= 4 不符合 故再进入下一步
此时判断 Db_TRX_ID 是否在 alive_trx_list 活跃事务列表中,发现这个 DB_TRX_ID 在活跃列表中,所以只能说明该行记录还未提交,不可见。
最终判断不可见之后通过回滚指针查看旧版本,发现此时 DB_TRX_ID 为1 故再次进行判断 DB_TRX_ID < up-limit-id ,此时 1 < 2 符合 ,所以可见并返回
所以最终返回的是
理解了上面的问题,现在我们可以思考下面两个问题:
思考1:
思考2:
可重复读(REPEATABLE READ): 可以避免“脏读”,“不可重复读”两个问题,会有“幻读”问题。 MySQL默认隔离级别,但是在MySQL中,此隔离级别解决了“幻读”问题
参考:
https://www.jianshu.com/p/8845ddca3b23
https://juejin.im/post/5e97d6b7e51d4546f5790f7e#heading-0
https://draveness.me/database-concurrency-control