原文 http://blog.csdn.net/endlu/article/details/51531397
InnoDB总为每一行后面加入了两个隐藏的列,来实现MVCC。这两个列分别纪录了数据最后一次被哪个事务创建、更新的事务号;该事物是否被删除,被删除的事务的事务号。事务号是递增的。虽然这格外增加的存储空间,每一行都要存储额外的历史版本,而且还要定期删除。但是多版本的实现,为大多数的读惭怍提供了方便:读数据只需要根据事务号来读取某一历史版本,不用再担心并发读写的问题而加锁,效率高,并且可以实现隔离级别中要求的只读取符合条件的值。我们称这种读叫快照读,相反如果读取当前的最新数据叫当前读。mysql在RC/RR下的普通select都为快照读,不用加锁。
在RR级别下,各种操作在MVCC下会有怎么样的效果呢?
- select时,读取版本号<=当前事务号并且删除版本号为空或>当前事务号的行。
- insert,保存事务号为当前事务号。
- delete,保存当前事务号为删除版本号。
- update,创建新的一行,保存事务号为当前事务号。
快照读/当前读
说完MVCC,我们回到隔离级别。上面说的隔离级别的介绍中,关于RR不能解决幻读的问题,是耳熟能详的,到处都是这么写的。但是实际呢?经过实验,session A select * from test where age = 12; 然后Session B插入一个age=12的行,Session A再次select,并没有返回新插入的行。由此可见Mysql中幻读的读问题已经解决了。原理就是上文提到的快照读。但是,当前读的冲突问题呢?
- 快照读:
- select * from test where ... ;
- 当前读:
- select * from test where ... in share mode ;
- select * from test where ... for update ;
- insert
- update
- delete
为了解决当前读的幻读问题,Mysql使用了Next-key锁。就是GAP(间隙锁)和行锁的合并。行锁可以避免并发修改同一行带来的问题,但是插入操作呢?间隙锁GAP就是为了解决这个问题。
在RC级别中,表test,age为主键:
- A:select * from test where age = 10; 返回一条记录
- A:update test set name = ‘a’ where age = 10;
- B:insert into test values(10,‘b’);
- B:commit;
- A:select * from test where age = 10; 返回两条记录
- A:commit ;
在RR级别中,重新进行这个实验,A的第二次读,仍然返回一条记录。因为在步骤二中,不止对age=10的行加了行锁,还有间隙锁。session B的插入将被阻塞,等待获取锁,A提交后才能被执行。
InnoDB行锁
上面说到,InnoDB当前读,会对行加锁,防止并发问题。这里有个前提:where条件是索引,可以通过索引来过滤行来对指定行加锁。如果不是索引,InnoDB会对所有行加锁,但是为了提供并发效率。等存储系统返回数据后,会过滤后再对不符合条件的行释放锁。还记得本文开头介绍两阶段锁时,说过有时为了效率会违反这个原则么。就是现在所说的这种情况。先获取全部锁,再释放掉多余的锁。
特殊情况
这里举一个例子:
- A:select * from test where age = 10; 返回一条记录
- B:insert into test values(10,‘b’);
- B:commit;
- A:update test set name = ‘qwe’where age = 10;
- A:select * from test where age = 10; 返回两条记录
这里A的前后两次读,均为快照读,而且是在同一个事务中。但是B先插入直接提交,此时A再update,update属于当前读,所以可以作用于新插入的行,并且将改行的当前版本号设为A的事务号,所以第二次的快照读,是可以读取到的,因为同事务号。这种情况符合MVCC的规则,如果要称为一种幻读也非不可,算为一个特殊情况来看待吧。