MYSQL隔离级别 与 锁

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 ?
 
简单来说,当前读就是带S锁读或者带X锁读,或者dml操作,快照读: 读不加锁,读写不冲突。
 
快照读要从MVCC说起
 
MVCC增加了一种可以不带锁的读取数据方式,但是读取的时版本快照,并不是最新内容
快照读读取的就是MVCC中的版本快照
MVCC只支持Mysql的InnoDB引擎中的已提交读(READ COMMITTD)和可重复读(REPEATABLE READ)这两种隔离级别下使用

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的修改  
 
 
 
 
 
 
 
 
###我的意思是其他在执行完DML后就会对表格进行修改,也即会更改行记录的trx_id,但是在commit之后才会被发现,因为在commit之前,执行dml之后该记录带X锁,会与其他想这条记录的事务冲突,所以在这个期间不会被其他事务看到修改。
 
2. 关于2pl

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有效

 

猜你喜欢

转载自www.cnblogs.com/start-from-zero/p/12580549.html