数据库的隔离级别以及幻读脏读不可重复读等概念
一、脏读,幻读,不可重复读概念
脏读:脏读是指一个事务中访问到了另外一个事务未提交的数据

如果会话 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快照读(照片)
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

浙公网安备 33010602011771号