MVCC
须知
当前读:读取的都是最新版本,会对读取的内容进行加锁。 快照读:读到的不一定是最新的版本,不加锁的非阻塞读,隔离级别不能是串行化,不然会退化成当前读。基于多版本并发控制(MVCC)实现的。
MVCC简介
MVCC的全称是多版本并发控制,它在具体的每一行中都添加了三个隐藏字段。
- DB_ROW_ID:隐藏的自增ID,当数据库表没有指定主键的时候,会自动生成。
- DB_TRX_ID:记录插入或者更新该行数据的最后一个事务ID。
- DB_ROLL_PTR:回滚指针,指向该行的上一个记录。
所以当我们去select数据的时候,我们需要比较当前事务ID和数据表中最后一个更新数据的事务ID,若我们当前事务是在上一次提交事务之后,毫无疑问我们可以读到当前数据,否则则读不到当前的数据。那么我们要怎么读到我们当前事务读得到的数据呢?
Read View(读视图)
什么是读视图呢?其实事务执行快照读的时候,它会去维护一个与当前事务相关的一个表,去记录维护当前活跃的事务ID。
Read View可以简单理解为有三个属性:
- trx_list: 一个存放读视图生成时正在活跃的事务ID
- up_limit_id: 记录列表中最小的事务ID
- low_limit_id: 已经出现过的事务ID的下一个ID,即最大的+1
package main
import (
"context"
"errors"
"os"
"golang.org/x/xerrors"
"github.com/aquasecurity/trivy/pkg/commands"
"github.com/aquasecurity/trivy/pkg/log"
"github.com/aquasecurity/trivy/pkg/plugin"
"github.com/aquasecurity/trivy/pkg/types"
_ "modernc.org/sqlite" // sqlite driver for RPM DB and Java DB
)
func main() {
if err := run(); err != nil {
var exitError *types.ExitError
if errors.As(err, &exitError) {
os.Exit(exitError.Code)
}
var userErr *types.UserError
if errors.As(err, &userErr) {
log.Fatal("Error", log.Err(userErr))
}
log.Fatal("Fatal error", log.Err(err))
}
}
func run() error {
// Trivy behaves as the specified plugin.
if runAsPlugin := os.Getenv("TRIVY_RUN_AS_PLUGIN"); runAsPlugin != "" {
log.InitLogger(false, false)
if err := plugin.Run(context.Background(), runAsPlugin, plugin.Options{Args: os.Args[1:]}); err != nil {
return xerrors.Errorf("plugin error: %w", err)
}
return nil
}
app := commands.NewApp()
if err := app.Execute(); err != nil {
return err
}
return nil
}
可见性: 说白了,这个读视图的作用就是为了让我们能够得到当前事务该读到什么版本的数据,它遵循以下的算法:
- 首先比较数据行中的DB_TRX_ID是否小于up_limit_id,如果小于,则证明这行数据是在这些活跃事务之前就已经存在的了,那么当前事务对这行数据就是可见的。
- 否则,判断DB_TRX_ID是否大于low_limit_id,如果大于,则证明这行数据是在读视图的生成后被修改了,则对当前事务是不可见的。则会通过DB_ROLL_PTR回滚指针找到上一次的数据,然后重复刚开始的判断。
- 如果小于low_limit_id,则去正在活跃的事务列表中查找是否存在DB_TRX_ID,如果存在,则证明数据还未commit,所以当前数据对当前事务是不可见的,如果不在,则证明这个事务刚好在读视图创建之前提交的,此时是可见的。
RR、RC级别下的快照读
RC(读已提交)
我们都知道RC能够解决脏读(读到未提交的数据),那么是怎么实现的呢?
事务A | 事务B |
---|---|
开启事务 | 开启事务 |
快照读,a=500 | 快照读,a=500 |
更新为a=400 | |
提交事务 |
当事务A的事务提交完的时候,事务B再次去快照读,我们可以发现此时读出来的数据为a=400。那这是为什么呢?其实是这样的,当事务A还未提交的时候,事务B第一次快照读的Read View中,事务A为正在活跃是事务,所以事务A修改的数据事务B读取不到;但是当事务A提交完事务之后,事务B再一次的快照读,此时会重新生产Read View,此时事务A不再是活跃的事务了,此时事务B则读得到事务A提交的数据。
RR(可重复读)
可重复读可以解决脏读、不可重复读。 我们来看下面两个事务:
事务A | 事务B |
---|---|
开启事务 | 开启事务 |
快照读,a=500 | 快照读,a=500 |
更新为a=400 | |
提交事务 | |
快照读,a=500 |
我们可以发现,事务B第一次的快照读a=500,然后等待事务A提交后,再一次的快照读a还是为500,这保证了事务B的重复读取的数据是一样的,这是为什么呢?
上篇文章我们说到,事务的一次快照读都会伴随的创建与其对应的一个Read View。 当事务B第一次进行快照读的时候,此时建立了Read View,由于事务A属于正在活跃的事务,所以事务A的修改对事务B来说是不可见的(详细可以看上一篇文章)。当事务A提交事务时,事务B再一次的快照读,此时不会再去新建Read View,而是沿用了事务B的第一次快照读所创建的那个Read View,所以很明显此时读出来的a还是500。
所以总是来说,导致RC、RR这两种隔离级别的快照读不同愿意就是,RC级别下,每一次的快照读都会重新生产Read View,而RR级别下,则会沿用第一次快照读生成的Read View。