数据库的隔离级别以及幻读脏读不可重复读等概念

一、脏读,幻读,不可重复读概念

脏读:脏读是指一个事务中访问到了另外一个事务未提交的数据

 如果会话 2 更新 age 为 10,但是在 commit 之前,会话 1 希望得到 age,那么会获得的值就是更新前的值。或者如果会话 2 更新了值但是执行了 rollback,而会话 1 拿到的仍是 10。这就是脏读

幻读:一个事务读取2次,得到的记录条数不一致(幻读仅专指“新插入的行”)

上图很明显的表示了这个情况,由于在会话 1 之间插入了一个新的值,所以得到的两次数据就不一样了。

不可重复读:一个事务读取同一条记录2次,得到的结果不一致

由于在读取中间变更了数据,所以会话 1 事务查询期间的得到的结果就不一样了。

二、隔离级别

Read uncommttied(读未提交):可以脏读,幻读,不可重复读

Read committed 简称rc(读已提交):解决了脏读,可以幻读,不可重复读

Repeatable read 简称rr(可重复读):解决了脏读,可以重复读,可以幻读(rr级别下才会有mvcc)

Serializable(可串行化):以上都不可以

三、快照读和当前读

快照读:读取的是记录数据的可见版本(可能是过期的数据),不用加锁

当前读:读取的是记录数据的最新版本,并且当前读返回的记录都会加上锁,保证其他事务不会再并发的修改这条记录

1、select快照读(照片)

当执行select的时候,innodb默认会执行快照读,相当于就是给你目前的状态找了一张照片,以后执行select 的时候就会返回当前照片里面的数据,当其他事务提交了也对你不造成影响,和你没关系,这就实现了可重复读了,那这个照片是什么时候生成的呢?不是开启事务的时候,是当你第一次执行select的时候,也就是说,当A开启了事务,然后没有执行任何操作,这时候B insert了一条数据然后commit,这时候A执行 select,那么返回的数据中就会有B添加的那条数据......之后无论再有其他事务commit都没有关系,因为照片已经生成了,而且不会再生成了,以后都会参考这张照片。
select...for update就是当前读

2、update、insert、delete 当前读

  当你执行这几个操作的时候默认会执行当前读,也就是会读取最新的记录,也就是别的事务提交的数据你也可以看到,这样很好理解啊,假设你要update一个记录,另一个事务已经delete这条数据并且commit了,这样不是会产生冲突吗,所以你update的时候肯定要知道最新的信息啊。

  update的过程,首先会执行当前读,然后把返回的数据加锁,之后执行update。加锁是防止别的事务在这个时候对这条记录做什么,默认加的是排他锁,也就是你读都不可以,这样就可以保证数据不会出错了。但注意一点,就算你这里加了写锁,别的事务也还是能访问的,是不是很奇怪?数据库采取了一致性非锁定读,别的事务会去读取一个快照数据。
  innodb默认隔离级别是RR, 是通过MVVC来实现了,读方式有两种,执行select的时候是快照读,其余是当前读,所以,mvvc不能根本上解决幻读的情况

四、innodb怎么解决幻读

产生幻读的原因是,行锁只能锁住行,但是新插入记录这个动作,要更新的是记录之间的“间隙”。因此,为了解决幻读问题,InnoDB 只好引入新的锁,也就是间隙锁 (Gap Lock)。

间隙锁是在可重复读rr隔离级别下才会生效

间隙锁 (Gap Lock):比如初始化表t里插入如下6条记录,就产生了7个空隙

所以为了解决幻读,执行select...for update的时候,不只是给6条记录加了行锁,也给7个空隙加了间隙锁

跟间隙锁存在冲突关系的,是“往这个间隙中插入一个记录”这个操作。

间隙锁和行锁合称 next-key lock,每个 next-key lock 是前开后闭区间。也就是说,表 t 初始化以后,如果用 select * from t for update 要把整个表所有记录锁起来,就形成了 7 个 next-key lock,分别是 (-∞,0]、(0,5]、(5,10]、(10,15]、(15,20]、(20, 25]、(25, +supremum]。

五、间隙锁带来的问题(间隙锁不互斥

业务逻辑如下:任意锁住一行,如果这一行不存在的话就插入,如果存在这一行就更新它的数据,

代码如下:

begin;
select * from t where id=N for update;

/*如果行不存在*/
insert into t values(N,N,N);
/*如果行存在*/
update t set d=N set id=N;

commit;

现象:这个逻辑一旦有并发,就会碰到死锁

用两个session模拟下,假设n=9

  执行分析:

1.session A 执行 select … for update 语句,由于 id=9 这一行并不存在,因此会加上间隙锁 (5,10);

2.session B 执行 select … for update 语句,同样会加上间隙锁 (5,10),间隙锁之间不会冲突,因此这个语句可以执行成功;

3.session B 试图插入一行 (9,9,9),被 session A 的间隙锁挡住了,只好进入等待;

4.session A 试图插入一行 (9,9,9),被 session B 的间隙锁挡住了。

至此,两个 session 进入互相等待状态,形成死锁,当然,InnoDB 的死锁检测马上就发现了这对死锁关系,让 session A 的 insert 语句报错返回了。

所以间隙锁的引入,可能会导致同样的语句锁住更大的范围,这其实是影响了并发度的。

间隙锁是在可重复读隔离级别下才会生效的。所以,如果把隔离级别设置为读提交的话,就没有间隙锁了。但同时,你要解决可能出现的数据和日志不一致问题,需要把 binlog 格式设置为 row。这,也是现在不少公司使用的配置组合。

这两种方案到底哪一种更合理?这就要根据业务场景有关了,如果业务场景不需要可重复读的保证,那么就可以考虑用读已提交,这样操作数据的锁范围更小(没有间隙锁),这个选择是合理的。

但是“用读提交就够了”这个结论是怎么得到的?

逻辑备份 mysqldump 备份线程用的是可重复读,如果业务用的是读提交,同时存在两种不同的隔离级别会不会有什么问题,所以如果用读提交之前一定要把这些问题搞清楚。

间隙锁的加锁规则

原则 1:加锁的基本单位是 next-key lock。next-key lock 是前开后闭区间。

原则 2:查找过程中访问到的对象才会加锁。(查找中访问的字段才会加锁,所以要考虑绕过覆盖索引的优化)

优化 1:索引上的等值查询,给唯一索引加锁的时候,next-key lock 退化为行锁。(比如给主键索引加锁时,就是给当前数据行加锁)

优化 2:索引上的等值查询,向右遍历时且最后一个值不满足等值条件的时候,next-key lock 退化为间隙锁。(不管是唯一索引还是普通索引,都是这样)

一个 bug:唯一索引上的范围查询会访问到不满足条件的第一个值为止。

面试题:https://zhuanlan.zhihu.com/p/140876416

posted @ 2021-03-10 11:20  我是张某某  阅读(712)  评论(0)    收藏  举报