在innodb中,cr和rr隔离级别对于select操作都没有共享锁,而是使用多版本控制,即一致性非锁定读,为了保证并发下的数据安全问题,可以手动对select加锁实现一致性锁定读
一. MVCC在InnoDB存储引擎中的意义
要了解MVCC的实际作用,首先必须了解InnoDB中事务的隔离性:
1. Read Uncommitted:会出现脏读的情况。事务B可以读到未提交事务A所做的修改。
2. Read Committed:避免了脏读,但是会出现幻读。事务B可以看到事务A提交之后的修改,考虑这种情况:
事务B
|
事务A
|
select name from user where id = 1 ;
name = "libis";
|
|
|
prepare;
update user set name = "fxx" where id = 1;
commit;
|
select name from user where id = 1;
name = "fxx";
|
|
3. Repeatable Read:避免了幻读。事务B只会看到自己这个事务内的记录版本,而对其他事务更新的记录版本是不可见的。MVCC实现了多版本机制,每个事务都会有一个版本。
4. Serializatable:事务必须严格的串行执行,在一定程度上会影响数据库性能。
二. 事务隔离性实现
事务之间的隔离性是如何实现的呢?
1. 数据结构
事务的隔离性实现基于两个基本的数据结构,一个是行记录,一个是read_view。其中前者实现了MVCC,后者在其基础上实行了记录的可见性。
(1)首先来看下innodb的行结构,innodb中每行都有两个隐藏列:DATA_TRX_ID,DATA_ROLL_PTR。其中DATA_TRX_ID表示更新此条记录的最新事务ID,DATA_ROLL_PTR指向此条记录项的undo信息,可以通过这个指针找到之前的版本。对行记录的更新操作可以通过如下示例表示:
原有记录:
id:1 |
name:divid |
age:25 |
DATA_TRX_ID:1 |
DATA_ROLL_PTR |
事务A执行update table set name = “fxx” where id = 1 ,行记录变更为:
事务B执行update table set name = “libis” where id = 1 ,行记录变更为:
可见MVCC是基于两个隐藏列和undo log实现的(可以重用undo log,而且不需要专门对undo log进行维护)。另一种简单地实现MVCC的方法是存放多个记录的版本,但是和实际方案相比,不仅浪费了磁盘的空间,而且多个版本记录的维护开销大,影响性能。
(2)read_view:用来实现行记录的可见性。
这里有必要解释一下什么是行记录的可见性,经过上文介绍可知,MVCC实现了多个并发事务更新同一行记录会时产生多个记录版本,那问题来了,新开始的事务如果要查询这行记录,应该获取到哪个版本呢?即哪个版本对这个事务是可见的。这个问题就是行记录的可见性问题。
下图是read_view_struct的结构体:
其中和可见性相关的两个变量分别low_limit_id和up_limit_id,根据注释可知,前者表示事务id大于此值的行记录都不可见,后者表示事务id小于此值的行记录都可见。具体的解释见下文。
2. 行记录的可见性实现
(1)生成read_view: 每个事务在开始的时候都会根据当前系统的活跃事务链表创建一个read_view,具体的创建过程:
假设当前的活跃事务链表如下图所示:
对应的read_view各个变量分别是:
read_view->creator_trx_id = ct-trx;
read_view->up_limit_id = trx3;
read_view->low_limit_id = trx11;
read_view->trx_ids = [trx11, trx9, trx6, trx5, trx3];
read_view->m_trx_ids = 5;
(2)现在事务A要查看记录R,同时R有三个版本R1,R2,R3,首先查看最新版本R1。
如果R1行记录DATA_TRX_ID大于low_limit_id,则R1对事务A不具有可见性,需要继续查看之前的版本R2。针对上图中的read_view,如果R1的DATA_TRX_ID=trx12,则R1不可见。
如果R1行记录DATA_TRX_ID小于up_limit_id,则R1对事务A具有可见性。针对上图中的read_view,如果R1的DATA_TRX_ID=trx2,则R1不可见。
如果R1在up_limit_id和low_limit_id之间,则遍历trx_ids,如果DATA_TRX_ID不在其中,则R1对事务可见,否则不可见。如果R1的DATA_TRX_ID = trx6,则R1不可见;而如果DATA_TRX_ID = trx7,则R1可见。
(3)why?为什么是上面的结论?
对于第一条,假如R1行记录DATA_TRX_ID大于low_limit_id,且R1对事务A可见。那如果再来了一个新事务B更新了这条记录得到新版本R0,按照同样的逻辑,R0也会对事务A可见,这样对事务A来说,就出现了幻读,即同一个事务前后读到的数据不一致。
而对于第二条,一方面R1行记录DATA_TRX_ID小于up_limit_id说明R1记录已经提交,不可能回滚;二是即使其他事务更新了此条记录形成了新的版本,根据第一条其也不具有可见性。这样就可以确保一个事务前后只会看到R1版本,而不会读到其他版本。
至于第三条,本质和第二条相同,都是基于上述两点来保证同一个事务前后只能读到同一个版本的。