1.四种隔离级别下数据不一致的情况
|
脏读
|
不可重复读
|
幻读
|
---|---|---|---|
RU | 是 | 是 | 是 |
RC(快照读) | 否 | 是 | 是 |
RC(当前读) | 否 | 否 | 是 |
RR(快照读) | 否 | 否 | 是 |
RR(当前读) | 否 | 否 | 否 |
Serializable(串行化) | 否 | 否 | 否 |
## 关于RR快照读时会不会造成幻读,我举一个例子,RR隔离级别,id主键
## 我也不知道这算不算幻读,
事务A | 事务B |
begin; | begin; |
select count(*) from test; 结果:1 |
|
insert into test values(4,'asd') | |
update test set name='zxcs'; |
|
阻塞 | |
commit | |
select count(*) from test; 结果:2 |
那什么是当前读,什么是快照读呢?
当前读:select * from table where ? lock in share mode;
select * from table where ?
for
update;
insert into table values (…);
update table set ? where ?;
delete from table where ?;
快照读: select * from table where ?
MVCC的实现原理:----UNDO LOG + 隐藏字段trx_id 和roll_pointer
## trx_id : 对该记录最新修改的事务id
## roll_pointer:老版本号 --保存在undo log中
## 如果一个表有两个字段id和name ,实际上是这样的,会有两个是隐藏字段,其实应该是3个隐藏字段,还有一个跟MVCC无关。
id
|
name
|
trx_id
|
roll_pointer
|
---|---|---|---|
1 | lxl | 40 | 上一个版本记录的地址 |
举个栗子,update table set name= 'lxlxlxl' where id=1;(id是主键),假如当先这条dml的事务号为41
id | name | trx_id | roll_pointer |
1 | lxl | 40 | 上一个版本记录的地址 |
### 这条记录会被放到undo log 中
### 然后
id |
name | trx_id | roll_pointer |
1 | lxlxlxl | 41 | trx_id=40的地址 |
###这条记录会放到表中
那快照读的时候是怎么判断版本号的呢?
ReadView中主要就是有个列表来存储我们系统中当前活跃着的读写事务,也就是begin了还未提交的事务。通过这个列表来判断记录的某个版本是否对当前事务可见。假设当前列表里的事务id为[80,100]。 如果你要访问的记录版本的事务id为50,比当前列表最小的id80小,那说明这个事务在之前就提交了,所以对当前活动的事务来说是可访问的。 如果你要访问的记录版本的事务id为85,发现此事务在列表id最大值和最小值之间,那就再判断一下是否在列表内,如果在那就说明此事务还未提交,所以版本不能被访问。 如果不在那说明事务已经提交,所以版本可以被访问。如果你要访问的记录版本的事务id为110,那比事务列表最大id100都大,那说明这个版本是在ReadView生成之后才发生的,所以不能被访问。 这些记录都是去版本链里面找的,先找最近记录,如果最近这一条记录事务id不符合条件,不可见的话,再去找上一个版本再比较当前事务的id和这个版本事务id看能不能访问,以此类推直到返回可见的版本或者结束。 版本链就是每条记录的隐藏字段roll_pointer组成的链表
那么我有一个问题,当一个事务执行一个dml,是在commit后对表,还是在执行完dml后对表进行修改?
我们可以来对比下RU 和 RC 隔离级别
RU 是为什么会造成脏读的现象?就是为什么会读到未提交的数据?
RC 为什么不会造成脏读?
我们假设执行完dml 就会对表进行修改,而不是commit之后修改
RU :id是主键
事务A | 事务B |
begin; | begin |
update table set name= 'lxlxlxl' where id=1; | |
select * from table | |
能读到事务B修改的数据 |
在RC,RR,RS中事务B需要commit 才能被其他事务看到
可以说明
执行完dml 就会对表进行修改,而不是commit之后修改
再来看RC:id是主键
事务A | 事务B |
begin; | begin; |
update table set name= 'lxlxlxl' where id=1; | |
select * from table where id=1 lock in share mode | |
此时事务A阻塞,S锁和 事务B的X锁冲突 此时事务B对表格已经修改,也成立 但是因为锁冲突的原理不会被其他当前读的事务所看到 如果这里采用快照读不会阻塞,也不会读到更改,因为只会读到上一个版本的记录 |
|
commit; | |
执行,并看到事务B的修改 |
2-PL :Two-phase Locking ,锁操作分为两个阶段,加锁阶段和解锁阶段,并且保证加锁阶段与解锁阶段不相交,
2PL就是将加锁/解锁分为两个完全不相交的阶段。加锁阶段:只加锁,不放锁。解锁阶段:只放锁,不加锁。
关于2-pl 有以下变种:
C2PL : 在事物开始时对所有需要访问的数据获取锁。不存在死锁问题,要么事务等待不能开始,要么就已经得到了全部所需的锁
S2PL : 严格2PL,事务持有的写锁必须提交后再释放,读锁在阶段二时释放
SS2PL: 强严格2PL,事务持有的所有锁必须在事务提交(完成)后释放;
3. 分析一下 一个简单的update在不同隔离级别下的效果
update test set name='lxl' where id=5 |
||||
id为主键 | id为二级唯一索引 | id为二级普通索引 | id不是索引 | |
---|---|---|---|---|
RU |
在主键上id=5的记录加上X锁 操作完成后释放所有X锁 |
先在二级唯一索引上进行带锁的当前读(for update), 找到id=5的记录后加上X锁, 然后通过主键值回到主键索引(聚集索引)中把对应的记录加上X锁, 操作完成后释放所有X锁 |
现在二级普通索引上进行待锁的当前读(for update), 找到所有id=5的记录后加上X锁, 然后通过主键值回到主键索引(聚集索引)中把对应的记录加上X锁, 操作完成后释放所有X锁, 跟唯一索引的区别是:唯一索引只有一条记录 |
使用半一致性读 SQL走聚簇索引的全扫描进行过滤把每条记录都加上X锁, 对于不满足where id=5的记录释放掉锁, 最终只有符合条件的记录带上X锁, |
RC |
在主键上id=5的记录加上X锁 操作完成后释放所有X锁 |
先在二级唯一索引上进行带锁的当前读(for update), 找到id=5的记录后加上X锁, 然后通过主键值回到主键索引(聚集索引)中把对应的记录加上X锁, 操作完成后释放所有X锁 |
现在二级普通索引上进行待锁的当前读(for update), 找到所有id=5的记录后加上X锁, 然后通过主键值回到主键索引(聚集索引)中把对应的记录加上X锁, 操作完成后释放所有X锁, (有人会说半一致性读,确实,半一致性读,能肯定的是,没执行update之前没有放锁,可以测试,但不能肯定他不合条件的是在获取所有锁->释放不合条件记录的X锁 -> 执行 还是获取所有锁-> 执行->释放不合条件记录的X锁 ,下面拿例子说一下这个不一致性读) 跟唯一索引的区别是:唯一索引只有一条记录 |
使用半一致性读
SQL走聚簇索引的全扫描进行过滤把每条记录都加上X锁, 对于不满足where id=5的记录释放掉锁, 最终只有符合条件的记录带上X锁, |
RR |
在主键上id=5的记录加上X锁 操作完成后释放所有X锁 |
先在二级唯一索引上进行带锁的当前读(for update), 找到id=5的记录后加上X锁, 然后通过主键值回到主键索引(聚集索引)中把对应的记录加上X锁, 操作完成后释放所有X锁 |
例如 id 的二级索引上有值【1,2,5,7】 现在二级普通索引上进行待锁的当前读(for update), 找到所有id=5的记录后加上X锁,以及把【2-5】【5-5】【5-7】之间加上间隙锁, 然后通过主键值回到主键索引(聚集索引)中把对应的记录加上X锁 操作完成后释放所有X锁和gap锁,跟唯一索引的区别是唯一索引只有一条记录 |
在聚集索引上扫表,
并对每条记录加上X锁, 但不会像RC那样把不符合条件的释放掉, 直到事务结束, 同时对每个间隙加上gap锁, 例如有主键[1,2,3,4], 在1-2 , 2-3, 3-4,4-~,所有间隙加上gap锁 操作结束,把所有X锁和gap锁释放掉 |
RS |
在主键上id=5的记录加上X锁 操作完成后释放所有X锁 |
先在二级唯一索引上进行带锁的当前读(for update), 找到id=5的记录后加上X锁, 然后通过主键值回到主键索引(聚集索引)中把对应的记录加上X锁, 操作完成后释放所有X锁 |
例如 id 的二级索引上有值【1,2,5,7】 现在二级普通索引上进行待锁的当前读(for update), 找到所有id=5的记录后加上X锁,以及把【2-5】【5-5】【5-7】之间加上间隙锁, 然后通过主键值回到主键索引(聚集索引)中把对应的记录加上X锁 操作完成后释放所有X锁和gap锁,跟唯一索引的区别是唯一索引只有一条记录 |
在聚集索引上扫表, 并对每条记录加上X锁, 但不会像RC那样把不符合条件的释放掉, 直到事务结束, 同时对每个间隙加上gap锁, 例如有主键[1,2,3,4], 在1-2 , 2-3, 3-4,4-~,所有间隙加上gap锁 操作结束,把所有X锁和gap锁释放掉 |
结论:update test set name='lxl' where id=5
(1)在id为主键的情况下,在RU,RC,RR,S隔离级别下加锁和释放锁的过程都是一样的
(2)在id为二级唯一索引时,在RU,RC,RR,S隔离级别下加锁和释放锁的过程也是一样的
(3)在id为二级普通索引时,在RU,RC下过程一样,在RR,S隔离级别下多了gap锁防止幻读
(4)在id不是索引的时,RU隔离级别下:;
RC隔离级别下,SQL走聚簇索引的全扫描进行过滤把每条记录都加上X锁,对于不满足where id=5的记录释放掉锁,
RR隔离级别下,SQL走聚簇索引的全扫描对每条记录加上X锁,但不会像RC那样把不符合条件的释放掉,直到事务结束,符合2PL原则。同时 对每个间隙加上gap锁
S隔离级别下,与RR级别一样,只不过强制把事务进行排序,不允许并发操作
4.分析select ,update,delete 在RC级别下不命中索引的操作
RC隔离级别,age不是索引
|
---|
select * from test where age=5; |
## 分析:最后只有主键上age=5的记录带S锁 ## 因为 S2PL的原因,读操作在读完可以把部分读锁释放不必等到commit再释放,而写锁必须事务提交才能释放 |
update viptest.test set name='lxl' where age=5; |
## 分析:最后只有主键上age=5的记录带X锁 ## 因为半一致性读的原因,当读到有锁冲突的记录的时候,mysql会判断,如果不是update需要的数据,如果不是则跳过该记录。 ## 如果读到没加锁的记录话,加锁和释放锁的操作不会被省略 |
delete from viptest.test where age=5; |
## 分析,假如主键上有age 【1,10】范围的数据,因为没走索引,【1,10】每个条记录加上X锁。 ## 半一致性读只对update有效 |