1. 基础
1.1 MVCC概念
MVCC,全称Multi-Version Concurrency Control,即多版本并发控制。用更好的方式去处理读-写冲突,做到即使有读写冲突时,也能做到不加锁,非阻塞并发读。
InnoDB通过undo log保存每条数据的多个版本,并且能够找回数据历史版本提供给用户读,每个事务读到的数据版本可能是不一样的。在同一个事务中,用户只能看到该事务创建快照之前已经提交的修改和该事务本身做的修改。
MVCC只在 Read Committed 和 Repeatable Read两个隔离级别下工作。
1.2 MVCC解决问题
数据库并发场景?有三种, 分别为:
读-读:不存在任何问题,也不需要并发控制
读-写:有线程安全问题,可能会造成事务隔离性问题,可能遇到脏读,幻读,不可重复读
写-写:有线程安全问题,可能会存在更新丢失问题
多版本并发控制(MVCC)是一种用来解决读-写冲突的无锁并发控制,在并发读写数据库时,可以做到在读操作时不用阻塞写操作,写操作也不用阻塞读操作,提高了数据库并发读写的性能 同时还可以解决脏读,幻读,不可重复读等事务隔离问题,但不能解决更新丢失问题。
2. 实现
MVCC的目的就是多版本并发控制,在数据库中的实现,就是为了解决读写冲突,它的实现原理主要是依赖下面来实现的。
- 聚簇索引记录中两个跟事务有关的隐藏列;
- Read View
- undo日志
2.1 Read View
Read View 有四个重要的字段:
- m_ids :指的是在创建 Read View 时,当前数据库中「活跃事务」的事务 id 列表,注意是一个列表,“活跃事务”指的就是,启动了但还没提交的事务。
- min_trx_id :指的是在创建 Read View 时,当前数据库中「活跃事务」中事务 id 最小的事务,也就是 m_ids 的最小值。
- max_trx_id :这个并不是 m_ids 的最大值,而是创建 Read View 时当前数据库中应该给下一个事务的 id 值,也就是全局事务中最大的事务 id 值 + 1;
- creator_trx_id :指的是创建该 Read View 的事务的事务 id。
2.2 隐藏列
对于使用 InnoDB 存储引擎的数据库表,它的聚簇索引记录中都包含下面两个隐藏列:
- db_trx_id,当一个事务对某条聚簇索引记录进行改动时,就会把该事务的事务 id 记录在 trx_id 隐藏列里;
- roll_pointer,每次对某条聚簇索引记录进行改动时,都会把旧版本的记录写入到 undo 日志中,然后这个隐藏列是个指针,指向每一个旧版本记录,于是就可以通过它找到修改前的记录。
2.3 undo log 链
查询操作执行时,并不需要记录相应的undo log。增删改需要生成undo log,方便回滚。
- 第一个事务插入 name为Jerry, age为24岁
- 新的事务1 ,对name做出了修改,改为Tom
- 又来个事务2,修改person表的同一个记录,将age修改为30岁
不同事务或者相同事务的对同一记录的修改,会导致该记录的undo log成为一条记录版本线性表,即链表,undo log的链首就是最新的旧记录,链尾就是最早的旧记录。
3. 查看逻辑
在创建 Read View 后,我们可以将记录中的 trx_id 划分这三种情况:
3.1 查看算法
Read View遵循一个可见性算法,将TRX_ID(即当前事务ID)取出来,遍历链表的 TRX_ID(从链首到链尾,即从最近的一次修改查起),找到满足特定条件的TRX_ID,这个记录就是当前事务能看见的最新老版本。
如果记录的 trx_id 值小于 Read View 中的
min_trx_id
值,表示这个版本的记录是在创建 Read View 前已经提交的事务生成的,所以该版本的记录对当前事务可见。如果记录的 trx_id 值大于等于 Read View 中的
max_trx_id
值,表示这个版本的记录是在创建 Read View 后才启动的事务生成的,所以该版本的记录对当前事务不可见。如果记录的 trx_id 值在 Read View 的
min_trx_id
和max_trx_id
之间,需要判断 trx_id 是否在 m_ids 列表中:3.1 如果记录的 trx_id 在
m_ids
列表中,表示生成该版本记录的活跃事务依然活跃着(还没提交事务),所以该版本的记录对当前事务不可见。3.2 如果记录的 trx_id 不在
m_ids
列表中,表示生成该版本记录的活跃事务已经被提交,所以该版本的记录对当前事务可见。
3.2 可重复读(RR)
可重复读隔离级别是启动事务时生成一个 Read View,然后整个事务期间都在用这个 Read View。「可重复读」隔离级别下在事务期间读到的记录都是事务启动前的记录。
场景:
- 事务A启动为 51,活跃列表为51。
- 事务B 启动为 52,活跃列表为 【51,52】。
- B一直读余额,A在这个过程中修改为 200 万。
分析:
事务B第一次读:行记录是50,比自己的readview 的 51 还小,可见,读出100。
事务A修改200不提交,事务B读:行记录是51,在活跃中间,说明未提交的事务改的。不能读,往前找到100。
事务A修改200提交后,事务B读:和上面一样,还是读到100。
3.3 读已提交(RC)
读提交隔离级别是在每次读取数据时,都会生成一个新的 Read View。那么在事务期间的多次读取同一条数据,前后两次读的数据可能会出现不一致,因为可能这期间另外一个事务修改了该记录,并提交了事务,自己的新ReadView最小的trx_id会增长。
和上面一个场景,事务A修改200提交后,事务B读:会读到200。
因为事务B创建的时候是最小变成了52(事务A的51提交了)。
事务 B 看行记录 trx_id 是 51,比事务 B 的 Read View 中的 min_trx_id 值(52)还小,这意味着修改这条记录的事务早就在创建 Read View 前提交过了,所以该版本的记录对事务 B 是可见的。