一、MVCC
InnoDB是一个多版本存储引擎:它保存关于已更改行的旧版本的信息,以支持诸如并发和回滚之类的事务特性.可以认为MVCC是行级锁的一个变种,但它在很多情况下避免了加锁操作,因此开销更低。
innodb会为每一行添加三个字段:
- 6-byte ID字段:隐含主键ID。
- 6-byte DB_TRX_ID字段:记录创建时事务的版本,这个版本号随着事务的创建不断递增。
- 7-byte DB_ROLL_PTR字段:记录了指向undo日志记录的指针。undo日志用于事物回滚。
MCVV只在REPEATABLE READ和READ COMMITTED两个隔离级别下工作。
二、事务链表
MySQL中的事务在开始到提交这段过程中,都会被保存到一个叫trx_sys的事务链表中,这是一个基本的链表结构:
新建事务(当前事务)与commit 的事务不在活跃事务链表中。
三、undo log
当事务1更改该行的值时,会进行如下操作:
- 用排他锁锁定该行
- 记录redo log
- 把该行修改前的值Copy到undo log,即上图中下面的行
- 修改当前行的值,填写事务编号,使回滚指针指向undo log中的修改前的行
四、ReadView
有了前面隐藏列和事务链表的基础,接下去就可以构造MySQL实现MVCC的关键——ReadView。
ReadView说白了就是一个数据结构,在事务开始的时候被创建。这个数据结构中包含了3个主要的成员:ReadView{low_trx_id, up_trx_id, trx_ids},在并发情况下,一个事务在启动时,trx_sys链表中存在部分还未提交的事务,那么哪些改变对当前事务是可见的,哪些又是不可见的,这个需要通过ReadView来进行判定,首先来看下ReadView中的3个成员各自代表的意思:
- low_trx_id表示该事务启动时,当前事务链表中最大的事务id编号,也就是最近创建的除自身以外最大事务编号;
- up_trx_id表示该事务启动时,当前事务链表中最小的事务id编号,也就是当前系统中创建最早但还未提交的事务;
- trx_ids表示所有事务链表中事务的id集合。
上述3个成员组成了ReadView中的主要部分,简单图示如下:
InnoDB默认的隔离级别为Repeatable Read (RR),可重复读。InnoDB在开始一个RR读之前,会创建一个全局Read View。Read Commited (RC)在每次语句执行的过程中,重新在row_search_for_mysql函数中创建当前的一份read_view。Read View用于判断一条记录的可见性。Read View定义在read0read.h文件中。
具体的算法如下:
- 设该行的当前事务id为trx_id,read view中最早的事务id为up_trx_id, 最迟的事务id为low_trx_id。
- 如果trx_id< up_trx_id的话,那么表明该行记录所在的事务已经在本次新事务创建之前就提交了,所以该行记录的当前值是可见的。跳到步骤6。
- 如果trx_id>low_trx_id的话,那么表明该行记录所在的事务在本次新事务创建之后才开启,所以该行记录的当前值不可见,跳到步骤5。否则该行记录的当前值是可见的,跳到步骤6。
- 如果up_trx_id<=trx_id<=low_trx_id, 那么表明该行记录所在事务在本次新事务创建的时候处于活动状态,从up_trx_id到low_trx_id进行遍历,如果trx_id等于他们之中的某个事务id的话,那么不可见。跳到步骤5。
- 从该行记录的DB_ROLL_PTR指针所指向的回滚段中取出最新的undo-log的版本号,将它赋值该trx_id,然后跳到步骤2。
- 将该可见行的值返回。
五、事务隔离级别
REPEATABLE READ(默认隔离级别)
select:开始一个RR读之前,会创建一个全局Read View。会出现幻读
CREATE TABLE t (a INT NOT NULL, b INT) ENGINE = InnoDB;
INSERT INTO t VALUES (1,2),(2,3),(3,2),(4,3),(5,2);
UPDATE t SET b = 5 WHERE b = 3;
b字段是索引:只会锁住b=3的记录
b字段不是索引:会锁住全表记录
x-lock(1,2); retain x-lock
x-lock(2,3); update(2,3) to (2,5); retain x-lock
x-lock(3,2); retain x-lock
x-lock(4,3); update(4,3) to (4,5); retain x-lock
x-lock(5,2); retain x-lock
READ COMMITTED
select:每次语句执行的过程中,重新在row_search_for_mysql函数中创建当前的一份read_view。不可重复读
b字段是索引:只会锁住b=3的记录
b字段不是索引:会先锁住全表记录,然后释放b!=3的记录的锁
x-lock(1,2); unlock(1,2)
x-lock(2,3); update(2,3) to (2,5); retain x-lock
x-lock(3,2); unlock(3,2)
x-lock(4,3); update(4,3) to (4,5); retain x-lock
x-lock(5,2); unlock(5,2)
READ UNCOMMITTED
select:返回最新记录。脏读
SERIALIZABLE
select:InnoDB隐式地将所有普通SELECT语句转换为SELECT ... LOCK IN SHARE MODE。并且通过间隙锁(next-key locking)策略防止幻读的出现。