什么是 mvcc
mvcc,Multi-version Concurrency Control,即多版本并发控制。mvcc 是一种并发控制的方法,一般在数据库管理系统中,实现对数据库的并发访问,在编程语言中实现事务内存
MVCC 在 mysql InnoDB 中的实现主要是为了提高数据库并发性能,用更好的方式去处理读-写冲突,做到即使有读写冲突时,也能做到不加锁,非阻塞并发读
什么是当前读和快照读
1.当前读
像 select lock in shared mode(共享锁),select for update。 update,insert, delete(排他锁)这些操作都是一种当前读,为什么叫当前读?就是它读取的是记录的最新版本,读取时还要保证其他并发事务不能修改当前记录,会对读取的记录加锁
2.快照读
像不加锁的 select 操作就是快照读,即不加锁的非阻塞读。快照读的前提是隔离级别不是串行级别,串行级别下的快照读会退化为当前读。之所以出现快照读的情况,是基于提高并发性能的考虑,快照读的实现是基于多版本并发控制,即 MVCC。可以认为 MVCC 是行锁的一个变种,但它在很多情况下,避免了加锁操作,降低了开销.既然是基于多版本,即快照读可能读到的并不一定是数据的最新版本,而有可能是之前的历史版本
说白了 MVCC 就是为了实现读-写冲突不加锁,而这个读指的就是快照读,而非当前读,当前读实际上是一种加锁的操作,是悲观锁的实现
当前读,快照读和 mvcc 什么关系
1.准确的说,MVCC 指的是”维持一个数据的多个版本,使得读写操作没有冲突”这么一个概念,仅仅是一个理想概念
2.而在 mysql 中,快照读就是实现 mvcc 理想模型的其中一个具体非阻塞读功能。而相对而言,当前读就是悲观锁的具体功能实现
3.快照读本身也是一个抽象概念。MVCC 模型在 mysql 中的具体实现是由 3 个隐式字段,undo 日志,read view 等去完成的
mvcc 能解决什么问题
数据库并发场景有 3 种,分别是:
读-读:不存在任何问题,也不需要并发控制
读-写:有线程安全问题,可能会造成事务隔离性问题,可能遇到脏读,幻读,不可重复读
写-写:有线程安全问题,可能会存在更新丢失问题,比如第一类更新丢失,第二类更新丢失
MVCC 带来的好处是?
MVCC 是一种用来解决读-写冲突的无锁并发控制,也就是为事务分配单向增长的时间戳,为每个修改保存一个版本,版本与事务时间戳关联,读操作只读该事务快照读之前的数据库的快照,所以 MVCC 可以为数据库解决以下问题
1.在并发读写数据库时,可以做到在读操作时不用阻塞写操作,写操作也不用阻塞读操作,提高了数据库并发读写的性能
2.同时还可以解决脏读,幻读,不可重复读等事务隔离问题,但不能解决更新丢失问题
组合:
1.MVCC+悲观锁
mvcc 解决读写冲突,悲观锁解决写写冲突
2.mvcc+乐观锁
mvcc 解决读写冲突,乐观锁解决写写冲突
mvcc 的实现原理
mvcc 的实现原理主要是依赖记录中的 3 个隐式字段,undo 日志,Read view 来实现的
隐式字段
每行记录除了我们自定义的字段外,还有数据库隐式定义的 DB_TRX_ID,DB_ROLL_PTR,DB_ROW_ID 等字段
DB_TRX_ID: 6byte,最近修改(修改/插入)事务 ID:记录创建这条记录/最后一次修改该记录的事务 ID
DB_ROLL_PTR: 7byte,回滚指针,指向这条记录的上一个版本(存储于 rollback segment 里)
DB_ROW_ID: 6byte,隐含的自增 id(隐藏主键),如果数据表没有主键,InnoDB 会自动以 DB_ROW_ID 产生一个聚簇索引
实际上还有一个删除 flag 隐藏字段,既记录被更新或删除并不代表真的删除,而是删除 flag 变了
undo 日志
undo log 主要分为两种:
insert undo log:代表事务在 insert 新记录时产生的 undo log,只在事务回滚时需要,并且在事务提交后可以被立即丢弃
update undo log:事务在进行 update 或者 delete 时产生的 undo log。不仅在事务回滚时需要,在快照读时也需要。所以不能随便删除,只有在快速读或事务回滚不涉及该日志时,对应的日志才会被 purge 线程统一清除
purge 线程
从前面的分析可以看出,为了实现 InnoDB 的 mvcc 机制,更新或者删除操作都只是设置一下老记录的 deleted_bit,并不真正将过时的记录删除
为了不影响磁盘空间,InnoDB 有专门的 purge 线程来清理 deleted_bit 为 true 的记录。为了不影响 MVCC 的正常工作,purge 线程自己也维护了一个 read view(这个 read view 相当于系统中最老活跃事务的 read view)。
如果某个记录的 deleted_bit 为 true,并且 DB_TRX_ID 相对于 purge 线程的 read view 可见,那么这条记录一定是可以被安全清除的
对 MVCC 有帮助的实质是 update undo log, undo log 实际上就是存在 rollback segment 中旧记录链,它的执行流程如下:
1.比如有个事务插入了 person 表插入了一条新纪录,记录如下:name 为 zhangsan,age 为 20,隐式主键是 1,事务 ID 和回滚指针,我们假设为 null
2.现在来了个事务 A 对该记录的 name 做了修改,改为 lisi
a.在事务 A 修改该行记录时,数据库会先对该行加排他锁
b.然后把该行数据拷贝到 undo log 中,作为旧纪录,即在 undo log 中有当前行的拷贝副本
c.拷贝完毕后,修改该行 name 为 lisi ,并且修改隐藏字段的事务 ID 为当前事务 A 的 Id,默认从 1 开始,之后递增,回滚指针指向拷贝到 undo log 的副本记录,即表示当前的上一个版本
d.事务提交后,释放锁
3.又一个事务 B,修改同一条记录,将 age 改为 25
a.在事务 B 修改该行数据时,数据库也先为该行加锁
b.然后把该行数据拷贝到 undo log,作为旧记录,发现该行已经有了 undo log,那么最新的旧数据作为链表的表头,插在该行记录的 undo log 最前面
c.修改 age 为 25,并且修改隐藏字段事务 id 为当前事务 B 的 id,自增即为 2,回滚指针指向刚刚拷贝到 undo log 的副本记录
d.提交事务,释放锁
从上面我们就可以看出,不同事务或者相同事务对同一条记录进行修改,就会导致该记录的 undo log 成为一条记录版本线性表,即链表,undo log 的链首就是最新的旧记录,链尾就是最早的一条旧记录(当然就像之前说的该 undo log 的节点可能会被 purge 线程清除掉,像图中的第一条 insert undo log,其实在事务提交之后可能就被删除丢失了)
read view (读视图)
什么是 read view
read view 就是事务进行快照读操作的时候产生的读视图,在该事务执行的快照读的那一刻,会生成数据库系统当前的一个快照,记录并维护系统当前活跃事务的 ID(当每个事务开启时,都会被分配一个 ID,这个 ID 是递增的,所以最新的事务 ID 最大)
read view 主要是用来做可见性判断的,主要是将要被修改的数据的最新记录中 DB_TRX_ID 取出来,与系统当前其他活跃事务的 ID(由 Read view 维护)去对比,如果 DB_TRX_ID 跟 Read View 的属性做了某些比较,不符合可见性,那就通过 DB_ROLL_PTR 回滚指针去取出 undo log 的 DB_TRX_ID 再比较,即遍历链表的 DB_TRX_ID(从链首到链尾,即从最近的一次修改查起),直到找到满足特定条件的 DB_TRX_ID,那么这个 DB_TRX_ID 所在的旧记录就是当前事务能看见的最新的老版本
那么这个判断条件是什么呢?如上,它是一段判断可见性的源码,即 changes_visible 方法,该方法展示了我们拿 DB_TRX_ID 去跟 read view 某些属性进行怎样的比较
在展示之前,我们可以把 read view 简单的理解成有三个全局属性
trx_list :一个数值列表,用来维护 Read view 生成时刻系统正活跃的事务 id
up_limit_id: 记录 trx_list 列表中事务 id 最小的 id
low_limit_id: read view 生成时刻系统尚未分配的下一个事务 id,也就是目前已经出现过的事务 id 的最大值+1
a.首先比较 DB_TRX_ID < up_limit_id,如果小于,则当前事务能看到 DB_TRX_ID 所在的记录,如果大于等于进入下一个判断
b.接下来判断 DB_TRX_ID 大于等于 low_limit_id,如果大于等于代表 DB_TRX_ID 所在的记录是在 read view 生成后才出现的,那对当前事务肯定不可见,如果小于则进入下一个判断
c.判断 DB_TRX_ID 是否在活跃事务之中,trx_list.contains(DB_TRX_ID),如果在则代表 Read view 生成时刻,DB_TRX_ID 对应的事务还在活跃,还没有 commit,DB_TRX_ID 对应事务修改的数据,当前事务也是看不见的。如果不在,说明 DB_TRX_ID 对应的事务在 read view 生成之前就已经 commit,DB_TRX_ID 对应的事务修改的结果,当前事务是能看见的
mvcc 整体流程
1.事务 A 和事务 C 进行中时,事务 B 对某行数据执行了快照读,数据库为该行数据生成了一个 read view 读视图,假设当前事务 ID 为 2,此时还有事务 A 和事务 C 在活跃中,事务 D 在事务 B 快照读前一刻提交更新了,所以 read view 记录了系统当前活跃事务 A,C 的 id,维护在一个列表上,假设称之为 trx_list
2.read view 并不仅仅会通过一个列表 trx_list 来维护事务 B 执行快照读那刻系统正活跃的事务 Id,还会有两个属性 up_limit_id(记录 trx_list 列表中事务 ID 最小的 ID),low_limit_id(记录 trx_list 列表中事务 ID 最大的 ID,也有人说快照读那刻系统尚未分配的下一个事务 ID 也就是目前已经出现过的事务 ID 的最大值+1)。所以在这里 up_limit_id 就是 1,low_limit_id 就是 4+1=5,trx_list 集合的值就是[1,3],read view 如下图
3.目前只有事务 D 修改过记录, 并在事务 B 执行快照读之前提交了事务,所以当前该行当前数据的 undo log 如下图所示。事务 B 在快照读该行记录的时候,就会拿该行记录的 DB_TRX_ID 去跟 up_limit_id,low_limit_id 和活跃事务 id 列表进行比较,判断当前事务 B 能看到该记录的版本是哪个
4.所以先拿该记录 DB_TRX_ID 字段记录的事务 ID4 去和 read view 的 up_limit_id 比较,看 4 是否小于 up_limit_id(1),所以不符合条件。继续判断 4 是否大于等于 low_limit_id(5),也不符合条件。最后判断活跃事务列表中的活跃事务 id,发现 4 不在列表中,符合可见性条件,所以事务 D 修改后提交的最新结果对事务 B 快照读时是可见的,所以事务 B 能读到的最新数据记录是事务 D 所提交的版本,而事务 D 提交的版本也是全局角度上的最新的版本
5.也正是 read view 生成的时机不同,造成 RC,RR 隔离级别下快照读的结果不同
MVCC 相关问题
RR 是如何在 RC 的基础上解决不可重复读的
当前读和快照读在 RR 级别下的区别
在上表的顺序下,事务 B 在事务 A 提交修改之后的快照读依旧是旧版本数据,而当前读是最新的数据
在第二张表的顺序下,事务 B 在事务 A 提交之后的快照读和当前读都是最新的数据 400。这里与第一张表的区别仅仅是第一张表的事务 B 在事务 A 修改金额前快照读过一次金额数据,而第二章表的事务 B 在事务 A 修改金额前没有进行过快照读
所以我们知道事务中快照读的结果是非常依赖该事务首次出现快照读的地方,即某个事务中首次出现快照读的地方非常关键,它有决定该事务后续快照读结果的能力
这里展示的是更新的流程,同时删除也是一样的,如果事务 B 的快照读是在事务 A 操作之后进行的,事务 B 的快照读也能读到最新的数据
RC,RR 级别下的 InnoDB 快照读有什么不同
正是 read view 生成时机的不同,从而造成 RC,RR 级别下快照读的结果的不同
在 RR 级别下的某个事务对某记录的第一次快照读会创建一个快照和 Read View,将当前系统活跃的其他事务记录起来,此后在调用快照读的时候,还是使用的同一个 read view,所以只要当前事务在其他事务提交更新之前使用过快照读,那么之后的快照读使用的都是同一个 read view,所以之后的修改对其不可见
即 RR 级别下,快照读生成 read view 时,read view 会记录此时其他活动事务的快照,这些事务的修改对于当前事务都是不可见的。而早于 Read view 创建的事务所作的修改均是可见的
而在 RC 级别下,事务中每次快照读都会新生成一个快照和 read view ,这就是我们在 RC 级别下的事务中可以看到别的事务提交的更新的原因
总之在 RC 隔离级别下,是每个快照读都会生成并获取最新的 read view 。而在 RR 级别下,则是同一个事务中的第一个快照读才会创建 Read view,之后的快照读获取的都是同一个 read view