MySQL MVCC

简介

MVCC全称Multi-Version Concurrency Control,即多版本并发控制。MVCC是一种并发控制的方法,一般在数据库管理系统中,实现对数据库的并发访问,在编程语言中实现事务内存。

多版本并发控制:指的是一种高并发技术。最早的数据库系统,只有读读之间可以并发,读写,写读,写写都要阻塞。引入多版本之后,只有写写之间相互阻塞,其他三种操作都可以并行,这样大幅度提高了InnoDB的并发度。在内部实现中,InnoDB是基于undo log实现的,通过undo log可以找回数据的历史版本。找回的数据历史版本可以提供给用户读(按照隔离级别的定义,有些读请求只能看到比较老的数据版本),也可以在回滚的时候覆盖数据页上的数据。在InnoDB内部中,会记录一个全局的活跃读写事务数组,其主要用来判断事务的可见性。

MVCC是一种多版本并发控制机制。

前置知识

InnoDB的当前读和快照读

  • 当前读:读取的记录是最新版本,读取时要保证其他事务不能修改当前记录,会对读取的记录进行加锁。

当前读主要包含以下场景

  • 共享锁:select ... lock in share mode
  • 排他锁:select for updateinsertupdatedelete
  • 快照读不加锁select操作属于快照读,即不加锁的非阻塞读。快照读基于多版本并发控制实现,即MVCC,正因为如此,可能导致快照读读取到的数据不是最新版本。

快照读的前提是事务的隔离级别不是串行级别,串行级别下快照读会退化成当前读。

MVCC就是为了实现读-写冲突不加锁,而这个读指的就是快照读,而非当前读,当前读实际上是一种加锁的操作,是悲观锁的实现。

数据库的并发场景

  • 读-读:不存在线程安全问题,不需要并发控制。
  • 读-写:存在线程安全问题,可能会造成事务隔离性问题,可能遇到脏读,幻读,不可重复读
  • 写-写:存在线程安全问题,可能会存在更新丢失问题,如第一类更新丢失,第二类更新丢失
  • 第一类更新丢失:事务A撤销时,把已经提交的事务B的更新数据覆盖。
  • 第二类更新丢失:事务A覆盖事务B已经提交的数据,造成事务B所做的操作丢失。

实现原理

MVCC主要依赖记录中的三个隐式字段undo logRead View来实现的。

隐式字段

每行记录除了我们自定义的字段外,还有数据库隐式定义的DB_TRX_IDDB_ROLL_PTRDB_ROW_ID等字段

  • DB_TRX_ID6byte, 最后修改(插入/更新)的事务ID
  • DB_ROLL_PTR7byte,回滚指针,配合undo log指向上一个版本的记录(存储于rollback segment
  • DB_ROW_ID6byte,隐含的自增ID(隐藏主键),如果数据表没有主键,InnoDB会自动以DB_ROW_ID产生一个聚簇索引
Column1 Column2 DB_TRX_ID DB_ROW_ID DB_ROLL_PTR
value1 value2 1 1 0x12446462

undo日志

  • insert undo log:事务在insert新记录时产生的undo log,只在事务回滚时需要,并且在事务提交后可以被立即丢弃。
  • update undo log:事务在进行updatedelete时产生的undo log,不仅在事务回滚时需要,在快照读时也需要,所以不能随便删除,只有在快速读或事务回滚不涉及该日志时,对应的日志才会被purge线程统一清除。

purge

  • 为了实现InnoDB的MVCC机制,更新或者删除操作都只是设置一下老记录的deleted_bit,并不真正将过时的记录删除。
  • 为了节省磁盘空间,InnoDB有专门的purge线程来清理deleted_bittrue的记录。为了不影响MVCC的正常工作,purge线程自己也维护了一个read view(这个read view相当于系统中最老活跃事务的read view),如果某个记录的deleted_bittrue,并且DB_TRX_ID相对于purge线程的read view可见,那么这条记录一定是可以被安全清除的。

MVCC依赖于update undo logundo log实际上就是存在rollback segment中旧记录链。

Read View

Read View是事务进行快照读操作时生产的读视图Read View,在该事务执行快照读的那一刻,会生成数据库系统当前的一个快照,记录并维护系统当前活跃事务的ID(当每个事务开启时,都会被分配一个ID, 这个ID是递增的,所以最新的事务,ID值越大)。

Read View主要是用来做可见性判断,Read View遵循一个可见性算法,主要是将要被修改的数据的最新记录中的DB_TRX_ID(即当前事务ID)取出来,与系统当前其他活跃事务的ID去对比(由Read View维护),如果DB_TRX_IDRead View的属性做了某些比较,不符合可见性,那就通过DB_ROLL_PTR回滚指针去取出undo Log中的DB_TRX_ID再比较,即遍历链表的DB_TRX_ID(从链首到链尾,即从最近的一次修改查起),直到找到满足特定条件的DB_TRX_ID,那么这个DB_TRX_ID所在的旧记录就是当前事务能看见的最新老版本

Licensed under CC BY-NC-SA 4.0
Powered by HugoStack