跳到主要内容

MVCC 多版本并发控制

MVCC

  • 全称 Multi—Version Concurrency Control,即多版本并发控制。是一种并发控制的方式,一般在数据库管理系统中实现对数据库的并发访问,在编程语言中实现事务内存
  • MVCC 的思想就是保存数据的历史版本,通过对数据行的多个版本管理来实现数据库的并发控制,这样就可以通过版本号决定数据是否显示,读取数据时不需要加锁也可以保证事务的隔离效果
  • 可以认为 MVCC 是行级锁的一个变种,但是在很多情况下避免了加锁操作,因此开销更低,虽然各个数据库的实现机制有所不同,但大多都实现了非阻塞的读操作,写操作也只锁定必要的行
  • MVCC 在 MySQL InnoDB 中的实现主要是为了提高数据库并发性能,用更好的方式处理读写冲突,做到即使有读写冲突时,也能不加锁,非阻塞并发读
  • MVCC 没有一个统一的实现标准,典型的有乐观(optimistic)并发控制悲观(pessimistic)并发控制

快照读和当前读

快照读(Snapshot Read)

  • 快照读用于提高数据库的并发能力,也是 InnoDB 并发如此之高的核心原因之一
  • 快照读是一种一致性不加锁的非阻塞读
    一致性: 事务读取到的数据,要么是事务开始前就已经存在的数据,要么是事务自身插入的或者修改过的数据
  • 快照读有可能读到的不是数据的最新版本,而是之前的历史版本
  • 不加锁的简单的 SELECT 都属于快照读
    SELECT * FROM t WHERE id = 1  

当前读

  • 当前读读取当前记录的最新版本,读取时还要保证其他并发事务不能修改当前记录,会对读取的记录进行加锁
  • 当前读的操作
    • select lock in share mode (共享锁)
    • select for update (排他锁)

快照读、当前读、MVCC 的关系

  • MVCC 多版本并发控制指的是维持一个数据的多个版本使得读写操作没有冲突,快照读是 MySQL 实现的一个非阻塞读功能。MVCC 模块在 MySQL 中的具体实现是由三个隐式字段undo 日志read view 三个部分来实现的

MVCC 解决的问题

  • 数据库的并发场景

    1. 读读: 不存在任何问题,也不需要并发控制
    2. 读写: 有线程安全问题,可能造成事务隔离性问题,可能遇到脏读、幻读、不可重复读
    3. 写写: 有线程安全问题,可能存在更新丢失问题
  • 解决的问题

    • 读写之间阻塞的问题
      通过 MVCC 可以让读写互相不阻塞,提升事务并发处理能力

      提高并发的演进思路:

      • 普通锁: 只能串行执行
      • 读写锁: 可以实现读读并发
      • 数据多版本并发控制: 实现读写并发
    • 降低了死锁的概率
      因为 InnoDB 的 MVCC 采用了乐观锁的方式,读取数据时并不需要加锁,对于写操作,也只锁定必要的行
    • 解决了一致性读的问题
      一致性读即快照读,见快照读
  • MVCC 解决了脏读、幻读、不可重复读的问题,但是不能解决更新丢失问题

MVCC 实现原理

  • 事务版本号
    每开启一个事务,都会从数据库中获得一个事务 ID (即事务版本号),这个事务 ID 是自增长的,通过 ID 大小可以判断事务的时间顺序

  • MVCC 的实现主要依赖记录中的三个隐藏字段undo logread view

    • 隐藏字段:
      每行记录除了自定义的字段外,还有数据库隐式定义的字段 DB_ROW_ID、DB_TRX_ID、DB_ROLL_PTR 等

      • DB_ROW_ID: 6 个字节,隐藏的主键,如果数据表没有主键,InnoDB 会自动生成一个 DB_ROW_ID,数据库会采用这个隐藏主键来创建聚簇索引,提升数据的查找效率
      • DB_TRX_ID: 6 个字节,最近修改事务 ID,记录了创建这条记录或者最后一次修改这条记录的事务 ID
      • DB_ROLL_PTR: 7 个字节,回滚指针,指向这条记录的上一个版本(即这条记录的 undo log 信息)

      三个隐藏字段

    • undo log:
      undo log 也被称为回滚日志,InnoDB 将行记录快照保存在 undo log 中,可以在回滚段中查找,便于在进行 insert、delete、update 操作时进行回滚
      当进行 update 和 delete 操作的时候,产生的 undo log 不仅仅在事务回滚的时候需要,在快照读的时候也需要,因此不能随便删除,只有在快照读或事务回滚不涉及该日志是,对应的日志才会被 purge 线程统一清除

      当数据发生更新和删除操作的时候都只是设置一下老记录的 deleted_bit,而非真正的将过时记录删除,因为为了节省磁盘空间, InnoDB 有专门的 purge 线程对 deleted_bit 为 true 的记录进行清除,如果某个记录的 deleted_bit 为 true,并且 DB_TRX_ID 对于 purge 线程的 read view 可见,那么这条记录一定是可以被清除的

      • undo log 生成的记录链
        记录链
        记录链
        每次事务对记录进行修改时,数据库会对该行加排他锁,然后把该行数据拷贝到 undo log 中作为旧记录,拷贝完成后才对该行记录进行修改,并且修改隐藏字段的事务 ID 为当前事务 ID,回滚指针指向拷贝到 undo log 中的副本记录,事务提交后,释放锁
        不同事务或者相同事务对同一条记录的修改,会导致该记录的 undo log 生成一条记录版本线性表(即链表),undo log 的链首就是最新的旧记录,链尾就是最旧的旧记录
    • read view

      • read view 是事务进行快照读操作的时候产生的读视图,在该事务执行快照读的那一刻,会生成一个数据系统的当前的快照,记录并维护系统当前活跃事务的 ID,事务的 ID 是递增的
      • read view 最大的作用就是用来做可见性判断,当某个事务执行快照读的时候,对该记录创建一个 read view 的视图,把它当作条件去判断当前事务能够看到哪个版本的数据,有可能读取到的是最新的数据,也有可能读取到的是当前行记录的 undo log 中某个版本的数据
      • read view 遵循的可见性算法主要是将要被修改的数据的最新记录中的 DB_TRX_ID (当前事务 ID) 取出来,与系统当前其他活跃事务 ID 比较,如果 DB_TRX_ID 跟 read view 的属性做了比较后不符合可见性,那就通过 DB_ROLL_PTR 回滚指针去取出 undo log 中的 DB_TRX_ID 做比较(即遍历链表中的 DB_TRX_ID),直到找到满足条件的 DB_TRX_ID,这个 DB_TRX_ID 指向的旧记录就是当前事务所能看到的最新老版本数据
      • read view 的可见性规则
        • 三个全局属性
          • trx_list: 一个数值列表,用于维护 read view 生成时刻系统中正活跃的事务 ID
          • up_limit_id: 记录 trx_list 列表中最小的事务 ID
          • low_limit_id: read view 生成时刻系统尚未分配的下一个事务 ID
        • 比较规则
          1. 首先比较 DB_TRX_ID < up_limit_id,如果小于,则当前事务能看到 DB_TRX_ID 所在的记录,大于/等于则进入下一个判断
          2. 然后判断 DB_TRX_ID >= low_limit_id,大于/等于则 DB_TRX_ID 所在的记录是在 read view 生成后才出现的,对于当前事务不可见,如果小于则进入下一个判断
          3. 判断 DB_TRX_ID 是否在活跃事务列表中,如果在,则表示在 read view 生成时刻,这个事务仍处于活跃状态,还没有 commit,修改过的数据在当前事务不可见;如果不在,则说明事务在 read view 生成前已经 commit,修改结果当前事务可见

MVCC 整体处理流程

MVCC 整体处理流程

RC、RR 级别下的 InnoDB 快照读的不同

  • RC、RR 级别下的 InnoDB 快照读的结果不同,是因为 read view 生成的时机不同
    1. RR 级别下的某个事务对某条记录的第一次快照读会创建一个快照(即 read view),将当前系统中活跃的其他事务记录起来,此后调用快照读的时候,还是使用同一个 read view,所以只要当前事务在其他事务提交更新前使用过快照读,此后使用的都是同一个 read view,对生成快照读之后的修改不可见
    2. RR 级别下快照读生成 read view 时,read view 会记录此时所有其他活动和事务的快照,这些事务的修改对于当前事务都是不可见的,而早已 read view 创建的事务做出的修改是可见的
    3. RC 级别下,事务中每次快照读都会生成并获取最新的 read view,因此在 RC 级别下的事务是可以看到别的事务提交的更新的
  • 总结: RC 隔离级别下,每个快照读都会生成并获取最新的 read view,RR隔离级别下则是同一个事务中的第一个快照读才会创建 read view,之后的快照读使用的都是同一个 read view