MVCC即多版本并发控制。MVCC是一种并发控制的方法,一般在数据库管理系统中,实现对数据库的并发访问,在编程语言中实现事务内存。
多版本控制: 指的是一种提高并发的技术。最早的数据库系统,只有读读之间可以并发,读写,写读,写写都要阻塞。引入多版本之后,只有写写之间相互阻塞,其他三种操作都可以并行,这样大幅度提高了InnoDB的并发度。在内部实现中,与Postgres在数据行上实现多版本不同,InnoDB是在undolog中实现的,通过undolog可以找回数据的历史版本。找回的数据历史版本可以提供给用户读(按照隔离级别的定义,有些读请求只能看到比较老的数据版本),也可以在回滚的时候覆盖数据页上的数据。在InnoDB内部中,会记录一个全局的活跃读写事务数组,其主要用来判断事务的可见性。MVCC是一种多版本并发控制机制。
MVCC在MySQL InnoDB中的实现主要是为了提高数据库并发性能,用更好的方式去处理读-写冲突,做到即使有读写冲突时,也能做到不加锁,非阻塞并发读。
像select lock in share mode(共享锁), select for update ; update, insert ,delete(排他锁
)这些操作都是一种当前读,为什么叫当前读?就是它读取的是记录的最新版本,并发事务不能修改当前读取时还要保证其他记录,会对读取的记录进行加锁
快照读
像不加锁
的select操作就是快照读,即不加锁的非阻塞读;快照读的前提是隔离级别不是串行级别,串行级别下的快照读会退化成当前读;之所以出现快照读的情况,是基于提高并发性能的考虑,快照读的实现是基于多版本并发控制,即MVCC,可以认为MVCC是行锁的一个变种,但它在很多情况下,避免了加锁操作,降低了开销;既然是基于多版本,即快照读可能读到的并不一定是数据的最新版本,而有可能是之前的历史版本。
3个隐式字段
,undo日志
,Read View
等去完成的,具体可以看下面的MVCC实现原理。在数据库表的记录中,每一个记录都会添加三个字段:
DB_TRX_ID:6个字节,表示最近一次修改本记录的事务ID
DB_ROLL_PTR :7 个字节,回滚指针,指向回滚段中的 undo log record,用于找出这个记录的上个修改版本的数据。
DB_ROW_ID:6 个字节,一个单调递增的 ID,确定表中记录的唯一性。
数据库并发场景有三种,分别为:
读-读
:不存在任何问题,也不需要并发控制读-写
:有线程安全问题,可能会造成事务隔离性问题,可能遇到脏读,幻读,不可重复读写-写
:有线程安全问题,可能会存在更新丢失问题,比如第一类更新丢失,第二类更新丢失备注:第1类丢失更新:事务A撤销时,把已经提交的事务B的更新数据覆盖了;第2类丢失更新:事务A覆盖事务B已经提交的数据,造成事务B所做的操作丢失
多版本并发控制(MVCC)是一种用来解决读-写冲突
的无锁并发控制,也就是为事务分配单向增长的时间戳,为每个修改保存一个版本,版本与事务时间戳关联,读操作只读该事务开始前的数据库的快照。 所以MVCC可以为数据库解决以下问题
MVCC就是因为不满意只让数据库采用悲观锁这样性能不佳的形式去解决读-写冲突问题,而提出的解决方案,所以在数据库中,因为有了MVCC,所以我们可以形成两个组合:
MVCC + 悲观锁
MVCC + 乐观锁
这种组合的方式就可以最大程度的提高数据库并发性能,并解决读写冲突,和写写冲突导致的问题
MVCC的目的就是多版本并发控制,在数据库中的实现,就是为了解决读写冲突
,它的实现原理主要是依赖记录中的 3个隐式字段
,undo日志
,Read View
来实现的。所以我们先来看看这个三个point的概念
隐式字段
每行记录除了我们自定义的字段外,还有数据库隐式定义的DB_TRX_ID
,DB_ROLL_PTR
,DB_ROW_ID
等字段
DB_TRX_ID
修改/插入
)事务ID:记录创建这条记录/最后一次修改该记录的事务IDDB_ROLL_PTR
DB_ROW_ID
DB_ROW_ID
产生一个聚簇索引如上图,DB_ROW_ID
是数据库默认为该行记录生成的唯一隐式主键,DB_TRX_ID
是当前操作该记录的事务ID,而DB_ROLL_PTR
是一个回滚指针,用于配合undo日志,指向上一个旧版本
undo log主要分为两种:
insert
新记录时产生的undo log
, 只在事务回滚时需要,并且在事务提交后可以被立即丢弃update
或delete
时产生的undo log
; 不仅在事务回滚时需要,在快照读时也需要;所以不能随便删除,只有在快速读或事务回滚不涉及该日志时,对应的日志才会被purge
线程统一清除Read View(读视图)
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可见,那么这条记录一定是可以被安全清除的。
什么是Read View,说白了Read View就是事务进行快照读
操作的时候生产的读视图
(Read View),在该事务执行的快照读的那一刻,会生成数据库系统当前的一个快照,记录并维护系统当前活跃事务的ID(当每个事务开启时,都会被分配一个ID, 这个ID是递增的,所以最新的事务,ID值越大)
所以我们知道 Read View
主要是用来做可见性判断的, 即当我们某个事务执行快照读的时候,对该记录创建一个Read View
读视图,把它比作条件用来判断当前事务能够看到哪个版本的数据,既可能是当前最新的数据,也有可能是该行记录的undo log
里面的某个版本的数据。
Read View
遵循一个可见性算法,主要是将要被修改的数据
的最新记录中的DB_TRX_ID
(即当前事务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所在的旧记录就是当前事务能看见的最新老版本
正是Read View
生成时机的不同,从而造成RC,RR级别下快照读的结果的不同
总之在RC隔离级别下,是每个快照读都会生成并获取最新的Read View;而在RR隔离级别下,则是同一个事务中的第一个快照读才会创建Read View, 之后的快照读获取的都是同一个Read View。
说白了MVCC就是为了实现读-写冲突不加锁,而这个读指的就是快照读
, 而非当前读,当前读实际上是一种加锁的操作,是悲观锁的实现。
MVCC在大多数情况下代替了行锁,实现了对读的非阻塞,读不加锁,读写不冲突。缺点是每行记录都需要额外的存储空间,需要做更多的行维护和检查工作。
关注公众号,发送 ztzl 免费下载《中台战略:中台建设与数字商业》。