前言:
在尼恩的读者社群(50+个)中,经常遇到:MVCC、间隙锁、Undo Log链、表级锁、行级锁、页级锁、共享锁、排它锁、记录锁等等相关的面试题。
这里尼恩给大家做一下系统化、体系化的梳理,使得大家可以充分展示一下大家雄厚的 “技术肌肉”,让面试官爱到 “不能自已、口水直流”。
也一并把这个题目以及参考答案,收入咱们的 《尼恩Java面试宝典 PDF》,供后面的小伙伴参考,提升大家的 3高 架构、设计、开发水平。
注:本文以 PDF 持续更新,最新尼恩 架构笔记、面试题 的PDF文件,请从下面的链接获取:码云
聊聊:MVCC 机制如何解决幻读?
在RR的隔离级别下,Innodb使用MVCC和 next-key locks(行锁和间隙锁的组合)解决幻读,
- MVCC解决的是普通读(快照读)的幻读,
- next-key locks解决的是当前读情况下的幻读。
所以,来看看 MVCC 机制如何普通读(快照读)的幻读?
了解了这些概念之后,我们来看下当查询一条记录的时候,系统如何通过MVCC找到它:
- 首先获取事务自己的版本号,也就是事务 ID;
- 获取 ReadView 读试图;
- 查询得到的数据,然后与 ReadView 中的事务版本号进行比较;
- 如果不符合 ReadView 规则,就需要从 Undo Log 中获取历史快照;
- 最后返回符合规则的数据。
不同的隔离级别,或者的Read View 数量不同:
- 在隔离级别为RC 读已提交(Read Committed)时,一个事务中的每一次 SELECT 查询都会重新获取一次Read View。如表所示:

注意,此时同样的查询语句都会重新获取一次 Read View,这时如果 Read View 不同,就可能产生 不可重复读或者幻读的情况。
- 当隔离级别为 RR 可重复读的时候,就避免了不可重复读,这是因为一个事务只在第一次 SELECT 的时候会 获取一次 Read View,而后面所有的 SELECT 都会复用这个 Read View,如下表所示:

举例说明 :READ COMMITTED 隔离级别下
READ COMMITTED :每次读取数据前都生成一个ReadView。
现在有两个 事务id 分别为 10 、 20 的事务在执行:
# Transaction 10 BEGIN;
UPDATE student SET name="李四" WHERE id=1;
UPDATE student SET name="王五" WHERE id=1;
# Transaction 20 BEGIN;
# 更新了一些别的表的记录 ...
此刻,表student 中 id 为 1 的记录得到的版本链表如下所示:

说明:
事务执行过程中,只有在第一次真正修改记录时(比如使用INSERT、DELETE、UPDATE语句),才会被分配一个单独的事务id,这个事务id是递增的。所
以我们才在事务2中更新一些别的表的记录,目的是让它分配事务id。
假设现在有一个使用 READ COMMITTED 隔离级别的事务开始执行:
# 使用READ COMMITTED隔离级别的事务
BEGIN;
# SELECT1:Transaction 10、20未提交
SELECT * FROM student WHERE id = 1; # 得到的列name的值为'张三'
之后,我们把 事务id 为 10 的事务提交一下:
# Transaction 10
BEGIN; UPDATE student SET name="李四" WHERE id=1;
UPDATE student SET name="王五" WHERE id=1;
COMMIT;
然后再到 事务id 为 20 的事务中更新一下表 student 中 id 为 1 的记录:
# Transaction 20
BEGIN; # 更新了一些别的表的记录 ...
UPDATE student SET name="钱七" WHERE id=1;
UPDATE student SET name="宋八" WHERE id=1;
此刻,表student中 id 为 1 的记录的版本链就长这样:

然后再到刚才使用 READ COMMITTED 隔离级别的事务中,继续查找这个 id 为 1 的记录,如下:
# 使用READ COMMITTED隔离级别的事务
BEGIN;
# SELECT1:Transaction 10、20均未提交
SELECT * FROM student WHERE id = 1;
# 得到的列name的值为'张三'
# SELECT2:Transaction 10提交,Transaction 20未提交
SELECT * FROM student WHERE id = 1;
# 得到的列name的值为'王五'
这个SELECT2的执行过程如下:
步骤1∶在执行SELECT语句时会又会单独生成一个ReadView,该ReadView的trx_ids列表的内容就是[20],up_limit_id为20,low_limit_id为21, creator_trx_id为0。
步骤2∶从版本链中挑选可见的记录,从图中看出,最新版本的列name的内容是‘宋八’,该版本的tr×_id值为20,在trx_ids列表内,所以不符合可见性要求,根据roll.pointer跳到下一个版本。
步骤3∶下一个版本的列name的内容是’钱七’,该版本的trx_id值为20,也在trx_ids列表内,所以也不符合要求,继续跳到下一个版本。
步骤4∶下一个版本的列name的内容是’王五’,该版本的trx_id值为10,小于ReadView中的up_limit.id值20,所以这个版本是符合要求的,最后返回给用户的版本就是这条列name为’王五’的记录。
以此类推,如果之后事务id为20的记录也提交了,再次在使用READ CONMMITTED隔离级别的事务中查询表student中id值为1的记录时,得到的结果就是‘宋八’了,具体流程我们就不分析了。
REPEATABLE READ隔离级别下
使用 REPEATABLE READ 隔离级别的事务来说,只会在第一次执行查询语句时生成一个 ReadView ,之 后的查询就不会重复生成了。
比如,系统里有两个 事务id 分别为 10 、 20 的事务在执行:
# Transaction 10
BEGIN;
UPDATE student SET name="李四" WHERE id=

最低0.47元/天 解锁文章
174万+

被折叠的 条评论
为什么被折叠?



