文章目录
前言
MVCC(多版本并发控制,Multi-Version Concurrency Control)是一种数据库管理系统(DBMS)中用于处理并发访问的机制。它的核心思想是通过保存数据的多个版本来实现高效的读写并发操作,避免事务之间的阻塞,从而提高系统性能, 这篇文章将从几个数据库来分析
MVCC
MVCC是为了在数据库中, 读写不阻塞的情况下, 提供一致性读的功能
在并发操作中,当正在写时,如果有用户在读,这时写可能只写了一半,如一行的前半部分刚写入,后半部分还没有写入,这时读取到的数据行,可能是前半部分是新数据,后半部分是旧数据的不一致的行。解决这个问题的最简单的办法是 使用读写锁,写的时候,不允许读,正在读的时候也不允许写,但这种方法导致读和写不能并发。于是,有人就想到了一种能够让读写并发的方法,这种方法就 是MVCC, MVCC的方法是写数据时,旧的版本数据并不删除,并发的读还能读到旧的版本数据,这样就不会有问题了
从几个经典的数据库来看MVCC可以分为两种, 一种是基于版本链的MVCC, 比如MySQL的Innodb存储引擎, 还有Oracle数据库, 这种是将旧数据放在回滚段, 通过一条记录的回滚指针来找到上一个版本, 另一种是直接将旧版本的数据依然放在当前数据文件中, 比如PostgreSQL
Innodb的MVCC
对于一条记录, 除开我们在创建表时规定的列, 还有几个隐藏字段, 在MVCC我们需要关注这俩个

- trx_id : 创建这条记录的事务号
- roll_pointer : 回滚指针, 指向上一个版本
版本链
通过回滚指针就可以形成版本链

- 在Innodb中存在purge线程,它会查询那些比现在最老的活动事务还早的undo log,并删除它们,从而保证undo log文件不至于无限增长。
回滚与提交
- 正常提交 : 一当事务正常提交时, innodb只需要更改事务状态为commit即可,不需做其他额外的工作
- 回滚 : Rollback需要根据当前回滚指针从undo log中找出事务修改前的版本,并恢复。如果事务影响的行非常多,回滚则可能会很慢并且还会加行锁, 阻塞其他事务,回滚时,也会产生redo日志
- Innodb的commit效率高,Rollback代价大
可见性判断
Innodb判断一条记录对于一个事务是否可见是通过读视图 ( Read View )来实现的
读视图可以看作一个数据结构, 里面有几个重点字段
- Create_trx_id : 表示创建该读视图的事务id
- m_ids : 表示创建该读视图时, MySQL中活跃的事务id集合
- min_try_id : 表示创建该读视图时, MySQL中活跃的事务id最小值
- max_try_id : 表示创建该读视图时, MySQL中全局最大事务id+1
那么对于一条记录
- 如果修改这条记录的事务id比读视图中的min_id还小, 表示这条记录是在创建该读视图之前就修改的, 那么对于当前事务这条数据是可见
- 如果这个id比读视图的Max_id还大, 说明是创建该读视图之后才修改的, 那么对于当前事务就是不可见的
- 如果在两者之间, 再看读视图中的m_ids, 如果这个id存在读视图的m_ids里, 说明这条记录还没被提交, 那么对于当前事务不可见, 如果不在, 说明该记录的事务已经被提交, 那么对于当前事务来说就可见
在Innodb的事务隔离级别中, 读提交和可重复读的区别就是创建读视图的时机不同, 读提交是在该事务每次执行语句都会创建一个读视图, 那么在此过程中, 其他事务提交了, 那么就会影响新创建的读视图的min_try_id 和m_ids, 使得提交的数据对于当前事务可见, 而可重复读是在事务启动时创建一个读视图, 整个过程中都用这一个读视图, 在此期间, 其他事务提交了, 对于当前事务也是不可见的
Oracle的MVCC
Oracle也是通过回滚段来实现MVCC的, 但oracle的实现更复杂,更精细一些。
Oracle相对MySQL的Innodb有几个重点区别
- Oracle中也有事务ID, 但不是递增的Undo Segment Number ( 回滚段 Number )+Transaction Table Slot Number ( 槽 Number ) +Wrap ( 槽重用次数), 所以不能通过事务id来判断事务的执行前后
- 事务信息并不是记录在每个数据行上的,而是在块头中的ITL槽上,所以相对来说更省空间

- ITL 槽存在数据块头部
- 每条记录的头部会有一个字节的数据表示使用了哪一个ITL槽 ( ITL 槽最大255 个)
- 因为Oracle数据库的事务ID不能判断事务的执行前后, 所以为了判断事务的执行前后引入了一个叫SCN的单调递增的序号
版本链

- Oracle的回滚信息是存在ITL槽中的, 每条记录的头部会有一个字节的数据表示使用了哪一个ITL槽, 通过这个ITL槽里的回滚指针, 就可以找到旧版本数据
- 在回滚段的数据会将旧的ITL槽和数据一起存, 这样就形成了版本链
PostgreSQL的MVCC
PostgreSQL数据库的MVCC不同于基于版本链的MVCC, 是没有回滚段的, 旧数据是存放在原有数据文件中的
- 为了清理旧数据有一个垃圾回收操作vacuum来做这个事。 同时还有有自动垃圾回收autovacuum
- 更新操作中新行的物理位置发生了变化使用了HOT技术。如果原有的数据块之间有空间,旧行与新行之间会建一个链接,索引上仍然指向旧的数据行。
MVCC实现
- 每行上有xmin和xmax两个系统字段, 当插入一行数据时,将这行上的xmin设置为当前的事务id,而xmax设置为0
- 当更新一行时,实际上是插入新行,把旧行上的xmax设置为当前事务id,新插入行的xmin设置为当前事务id,新行的xmax设置为0
- 当删除一行时,把当前行的xmax设置为当前事务id
- 当读到一行时,到commitlog ( 记录事务是提交还是回滚 ) 中查询xmin和xmax对应的事务状态是否是己提交还是回滚了,就能判断出此行对当前行是否是可见。

可见性判断
类似MySQL的innodb, PostgreSQL在执行一条SQL也会生成一个快照, 这里是SnapshotData的数据结构, 里面有几个重点字段
- xmin : 己完成的的最大事务
- xmax : 未开始的事务
- xip : 当前数据库中活动的事务id集合
那么对于一条数据
- 如果数据中的xmin>SnapshotData.xmax 说明这条数据是在当前事务之后的事务插入的, 那么对当前事务不可见
- 如果数据中的xmin在SnapshotData.xip中, 说明插入这条数据的事务还未提交, 那么对于当前事务不可见
- 如果数据中的xmin<SnapshotData.xmin, 说明插入这条数据的事务在当前事务之前, 那么就去commit log中查看该事务的状态, 如果是回滚, 则不可见, 如果是提交, 那么接下来看xmax
- 如果xmax==0 || xmax in SnapshotData.xip 说明这条数据是当前事务执行前的最新版, 对当前事务可见
- 如果xmax<SnapshotData.xmin 说明可能存在更新的版本或该数据被删除, 那么就去commit log中查看该xmax的事务的状态, 如果是回滚, 那么对当前事务可见, 如果是提交, 说明有更新的版本或者被删除, 那么这条就对当前事务不可见
特点
- 事务回滚可以立即完成,无论事务进行了多少操作, 数据可以进行很快更新
- 旧版本数据需要清理,有autovacuum进程vacuum命令处理