什么是MVCC
全称Multi-Version Concurrency Control,即多版本并发控制
,解决读—写冲突的无锁并发控制。
当前读是一种加锁操作,是悲观锁
。同一行数据平时发生读写请求时,会上锁阻塞
住。
但mvcc用更好的方式去处理读—写请求,发生读—写请求冲突时不用加锁,提高数据库的并发性能,具体实现就是快照读。
MVCC维护多版本数据,为每个数据修改保存一个版本,版本与事务时间戳相关联。
因为myIsam不支持事务,MVCC都针对InnoDB引擎来讲。
当前读、快照读区别
当前读
它读取的数据库记录,都是当前最新
的版本
,会对当前读取的数据进行加锁
,防止其他事务修改数据。是悲观锁
的一种操作。
如下操作都是当前读:
- select lock in share mode (共享锁)
- select for update (排他锁)
- update (排他锁)
- insert (排他锁)
- delete (排他锁)
- 串行化事务隔离级别
快照读
快照读的实现基于多版本
并发控制。MVCC为事务分配单向增长
的时间戳
,为每个数据修改保存一个版本
,版本与事务时间戳相关联
。
读操作只读取
该事务开始前
的数据库快照
。
如下操作是快照读:
- 不加锁的select操作(注:事务级别不是串行化)
MVCC解决什么并发问题
数据库并发场景
-
读-读
:不存在任何问题,也不需要并发控制 -
读-写
:有线程安全问题,可能会造成事务隔离性问题,可能遇到脏读,幻读,不可重复读 -
写-写
:有线程安全问题,可能会存在更新丢失问题,比如第一类更新丢失,第二类更新丢失
解决读写冲突
-
并发读-写时
:可以做到读操作不阻塞写操作,同时写操作也不会阻塞读操作。 - 解决
脏读
、幻读
、不可重复读
等事务隔离问题,但不能解决上面的写-写 更新丢失
问题。
因此有了下面提高并发性能的组合拳
:
-
MVCC 悲观锁
:MVCC解决读写冲突,悲观锁解决写写冲突 -
MVCC 乐观锁
:MVCC解决读写冲突,乐观锁解决写写冲突
MVCC的实现原理
基于版本链
,undo日志
,Read View
来实现。
版本链
数据库中的每行数据,还有几个隐藏字段
,分别是最近修改事务db_trx_id
、回滚指针db_roll_pointer
。
- db_trx_id
最近修改(修改/插入)
事务ID
:记录创建
这条记录/最后一次修改
该记录的事务ID
。 - db_roll_pointer
回滚指针
,用于配合undo日志
,指向这条记录
的上一个版本
(存储于rollback segment里) - db_row_id是隐含的
自增ID
(隐藏主键),如果数据表没有主键
,InnoDB会自动以db_row_id产生一个聚簇索引
。 -
删除标识flag
, 记录被更新
或删除
并不代表真的删除,而是删除flag
变了
每次对数据库记录进行改动,都会记录一条undo日志
,每条undo日志也都有一个生成该版本时对应的事务id以及回滚指针(INSERT操作对应的undo日志没有该属性,因为该记录并没有更早的版本)。
随着更新次数的增多,所有的版本都会被roll_pointer
属性连接成一个链表
,我们把这个链表称之为版本链
,版本链的头节点就是当前记录最新的值。如下图一样:
undo log日志
Undo log 主要用于记录
数据被修改之前
的日志,在表信息修改之前先会把数据拷贝到undo log
里。
当事务
进行回滚时
可以通过undo log 里的日志进行数据还原
。
Undo log 的用途
- 保证
事务
进行rollback
时的原子性和一致性
,当事务进行回滚
的时候可以用undo log的数据进行恢复
。 - 用于MVCC
快照读
的数据,在MVCC多版本控制中,通过读取undo log
的历史版本数据
可以实现不同事务版本号
都拥有自己独立的快照数据版本
。
undo log主要分为两种:
- insert undo log 代表事务在insert新记录时产生的undo log , 只在事务回滚时需要,并且在事务提交后可以被立即丢弃
- update undo log(主要) 事务在进行update或delete时产生的undo log ; 不仅在事务回滚时需要,在快照读时也需要; 所以不能随便删除,只有在快速读或事务回滚不涉及该日志时,对应的日志才会被purge线程统一清除
Read View(读视图)
Read View主要是用来做可见性
判断的, 即当我们某个事务
执行快照读
的时候,对该记录创建一个Read View读视图,把它比作条件用来判断当前事务
能够看到哪个版本
的数据,既可能是当前最新
的数据,也有可能是该行记录的undo log里面的某个版本
的数据。
Read View几个属性
-
trx_ids
: 当前系统活跃(未提交
)事务版本号集合。(当每个事务开启时,都会被分配一个ID, 这个ID是递增的,所以越新的事务,ID值越大)。 -
low_limit_id
: 创建当前read view 时,当前系统最大事务版本号
1。 -
up_limit_id
: 创建当前read view 时,系统正处于活跃事务最小版本号
。 -
creator_trx_id
: 创建当前read view的事务版本号。
Read View可见性判断条件
-
db_trx_id
<up_limit_id
||db_trx_id
==creator_trx_id
(显示) 如果数据事务ID小于read view中的最小活跃事务ID
,则可以肯定该数据是在当前事务启之前
就已经存在
了的,所以可以显示
。 或者数据的事务ID
等于creator_trx_id
,那么说明这个数据就是当前事务自己生成的
,自己生成的数据自己当然能看见,所以这种情况下此数据也是可以显示
的。 -
db_trx_id
>=low_limit_id
(不显示) 如果数据事务ID大于read view 中的当前系统的最大事务ID
,则说明该数据是在当前read view 创建之后才产生
的,所以数据不显示
。如果小于则进入下一个判断 -
db_trx_id
是否在活跃事务
(trx_ids)中 -
-
不存在
:则说明read view产生的时候事务已经commit
了,这种情况数据则可以显示
。 -
已存在
:则代表我Read View生成时刻,你这个事务还在活跃,还没有Commit,你修改的数据,我当前事务也是看不见的。
-
Read View和事务隔离级别
Read View
用于支持RC
(Read Committed,读提交)和RR
(Repeatable Read,可重复读)隔离级别
的实现
。
RR、RC生成时机
- 读提交
RC
隔离级别下,每个快照读
都会生成并获取最新
的Read View
,在RC级别下的事务中可以看到别的事务提交的更新。 - 可重复读
RR
隔离级别下,同一个事务中
的第一个快照读
才会创建Read View
,之后的
快照读获取的都是同一个Read View
,所以一个事务的查询结果每次都是一样的
,对之后的修改不可见。
解决幻读问题
-
快照读
:通过MVCC来进行控制的,不用加锁。按照MVCC中规定的“语法”进行增删改查等操作,以避免幻读。 -
当前读
:通过next-key锁(行锁 gap间歇锁)来解决问题的。
参考:
通俗易懂数据库MVCC讲解:https://mp.weixin.qq.com/s/oOL4yradD5w73VsrfoyneA