可重复读(repeatable read)定义: 一个事务执行过程中看到的数据,总是跟这个事务 在启动时看到的数据是一致的。
MVCC
-
MVCC,多版本并发控制 , 用于实现读已提交 和可重复读隔离级别。
-
MVCC的核心就是 Undo log多版本链 + Read view ,"MV"就是通过 Undo log来保存数据的历史版本,实现多版本的管理 ,"CC"是通过 Read-view 来实现管理,通过 Read-view原则来决定数据是否显示 。同时针对不同的隔离级别, Read view的生成策略不同,也就实现了不同的隔离级别。
Undo log 多版本链
每条数据都有两个隐藏字段:
-
trx_id : 事务id,记录最近一次更新这条数据的事务id.
-
roll_pointer : 回滚指针 ,指向之前生成的undo log
每一条数据都有多个版本 ,版本之间通过undo log链条进行连接通过这样的设计方式,可以保证每个事务提交的时候,一旦需要回滚操作,可以保证同一个事务只能读取到比当前版本更早提交的值,不能看到更晚提交的值。
ReadView
Read View是 InnoDB 在实现 MVCC 时用到的一致性读视图 ,即 consistent read view,用于支持 RC(Read Committed,读提交 )和 RR(Repeatable Read,可重复读)隔离级别的实现.
Read View简单理解就是对数据在某个时刻的状态拍成照片记录下来 。那么之后获取某时刻的数据时就还是原来的照片上的数据,是不会变的.
Read View中比较重要的字段有4个:
-
m_ids
: 用来表示MySQL中哪些事务正在执行,但是没有提交. -
min_trx_id
: 就是m_ids里最小的值. -
max_trx_id
: 下一个要生成的事务id值,也就是最大事务id -
creator_trx_id
: 就是你这个事务的id
当一个事务第一次执行查询 sql时,会生成一致性视图 read-view(快照),查询时从 undo log 中最新的一条记录 开始跟 read-view 做对比,如果不符合比较规则 ,就根据回滚指针回滚到上一条记录继续比较,直到得到符合比较条件的查询结果。
Read View判断记录某个版本是否可见的规则如下
1.如果当前记录的事务id落在绿色部分(trx_id < min_id),表示这个版本是已提交 的事务生成的,可读。 2.如果当前记录的事务id落在红色部分(trx_id > max_id),表示这个版本是由将来启动的事务生成的,不可读。
-
如果当前记录的事务id落在黄色部分(min_id <= trx_id <= max_id),则分为两种情况:
-
若当前记录的事务id在未提交事务的数组中,则此条记录不可读;
-
若当前记录的事务id不在未提交事务的数组中,则此条记录可读。
RC 和 RR 隔离级别都是由 MVCC 实现,区别在于:
-
RC 隔离级别时,read-view 是每次执行 select 语句时都生成一个;
-
RR 隔离级别时,read-view 是在第一次执行 select 语句时生成一个,同一事务中后面的所有 select 语句都复用这个 read-view 。
Repeatable Read 解决了幻读问题吗?
可重复读(repeatable read)定义: 一个事务执行过程中看到的数据,总是跟这个事务在启动时看到的数据是一致的。
不过理论上会出现幻读 ,简单的说幻读指的的当用户读取某一范围 的数据行时,另一个事务又在该范围插入了新行,当用户在读取该范围的数据时会发现有新的幻影行。
注意在可重复读隔离级别下,普通的查询 是快照读 ,是不会看到别的事务插入的数据的 。因此, 幻读在"当前读 "下才会出现(查询语句添加for update,表示当前读);
在 MVCC 并发控制中,读操作可以分为两类 : 快照读(Snapshot Read
)与当前读 (Current Read
)。
-
快照读 快照读是指读取数据时不是读取最新版本的数据,而是基于历史版本 读取的一个快照信息(mysql读取undo log历史版本) ,快照读可以使普通的SELECT 读取数据时不用对表数据进行加锁,从而解决了因为对数据库表的加锁而导致的两个如下问题
-
解决了因加锁导致的修改数据时无法对数据读取问题.
-
解决了因加锁导致读取数据时无法对数据进行修改的问题.
-
-
当前读 当前读是读取的数据库最新的数据 ,当前读和快照读不同,因为要读取最新的数据而且要保证事务的隔离性 ,所以当前读是需要对数据进行加锁 的(
插入/更新/删除操作,属于当前读,需要加锁
,select for update
为当前读)
表结构
id | key | value |
---|---|---|
0 | 0 | 0 |
1 | 1 | 1 |
假设 select * from where value=1 for update,只在这一行加锁(注意这只是假设),其它行不加锁,那么就会出现如下场景:
Session A的三次查询Q1-Q3都是select * from where value=1 for update,查询的value=1的所有row。
-
T1:Q1只返回一行(1,1,1);
-
T2:session B更新id=0的value为1,此时表t中value=1的数据有两行
-
T3:Q2返回两行(0,0,1),(1,1,1)
-
T4:session C插入一行(6,6,1),此时表t中value=1的数据有三行
-
T5:Q3返回三行(0,0,1),(1,1,1),(6,6,1)
-
T6:session A事物commit。
其中Q3读到value=1这一样的现象,就称之为幻读,幻读指的是一个事务在前后两次查询同一个范围的时候,后一次查询看到了前一次查询没有看到的行。
先对"幻读"做出如下解释:
- 要讨论「可重复读」隔离级别的幻读现象,是要建立在「当前读」的情况下,**而不是快照读,**因为在可重复读隔离级别下,普通的查询是快照读,是不会看到别的事务插入的数据的。
Next-key Lock 锁
产生幻读的原因是,行锁只能锁住行 ,但是新插入记录这个动作,要更新的是记录之间的"间隙"。因此,Innodb 引擎为了解决「可重复读」隔离级别使用「当前读」而造成的幻读问题,就引出了 next-key 锁,就是记录锁和间隙锁的组合。
-
RecordLock锁:锁定单个行记录的锁。(记录锁,RC、RR隔离级别都支持)
-
GapLock锁:间隙锁,锁定索引记录间隙 (不包括记录本身),确保索引记录的间隙不变 。(范围锁,RR隔离级别支持)
-
Next-key Lock 锁:记录锁和间隙锁组合 ,同时锁住数据 ,并且锁住数据前后范围。(记录锁+范围锁,RR隔离级别支持)
总结
-
RR隔离级别下间隙锁才有效,RC隔离级别下没有间隙锁;
-
RR隔离级别下为了解决"幻读"问题:"快照读 "依靠MVCC控制,"当前读"通过间隙锁解决;
-
间隙锁和行锁合称next-key lock ,每个next-key lock是前开后闭区间;
-
间隙锁的引入,可能会导致同样语句锁住更大的范围 ,影响并发度。
知识来源:马士兵教育