从因到果看懂事务隔离级别的实现原理

一、前言

1、事务的四大特性

原子性、一致性、隔离性、持久性。

2、事务并发带来的问题

事务的并发执行,可以提高性能,但如果不加以控制则会引发诸多的问题(死锁、更新丢失…),这就需要我们在性能和安全之间做合理的权衡,使用适当的并发控制机制保障并发事务的执行。

并发事务带来的问题:

  1. 脏读
  2. 丢失修改
  3. 不可重复读
  4. 幻读

3、事务隔离级别

以上事务并发问题严重程度的排序:
丢失更新 > 脏读 > 不可重复读 > 幻读
因此,如果我们可以容忍一些较轻严重程度的问题,我们就可以获取一些性能上的提升,于是SQL规范中便定义了事务的四种隔离级别

  • Read Uncommited
  • Read Committed(RC)
  • Repeatable Read(RR)
  • Serializable

实现事务的隔离级别一般有如下两种方法:

  1. LBCC(加4种隔离级别分别对应的锁,基于锁的并发;)
  2. MVCC(多版本并发控制)

二、事务隔离级别的实现方式(LBCC + MVCC)

LBCC:基于锁的并发控制
MVCC:基于多版本快照的实现
其中普遍select语句均是快照读(MVCC解决)
而delete/update/select for update等语句是加锁实现的(LBCC解决)

1、LBCC基于锁并发的控制实现

说明:InnoDB引擎的行锁机制,锁的是索引,而不是行记录。

1.1 以锁的模式分类

(1)共享锁S(行锁,读锁)

对某一资源加共享锁,自身可以该锁资源,其他人也可以读该资源(也可以再继续加共享锁,即可重入锁、多锁共存),但无法修改,想要修改就必须等所有共享锁都释放完之后才能进行。

//加锁
select * from table lock in share mode;
//释放锁
CommitRollback
(2)排他锁X(行锁、写锁)

某事务对某一资源上了排他锁后,只允许自身对其进行CRUD,其他人无法对其进行任何操作。因此,排他锁不能与其他锁共存。

//排它锁加锁方式
自动:DML语句默认会自动加锁;
手动:select * from user where id = 1 for update;

//释放锁
Commit;
Rollback;
(3)意向锁(表锁)

S锁和X锁使用时,会触发建立意向锁。且IS、IX锁均是表锁,且无法手动创建。
1、意向共享锁IS
事务在请求S锁前,要先获取IS锁;
2、意向排它锁IX
数据行加排他锁前提是已经获取到此表的IX锁;

【为什么要加入意向锁?】

  1. 用来告诉你,表中有没有已经锁定了的数据,从而提高了加表锁的效率
  2. 意向锁并不是用来锁定数据的;

当我们准备对整张表操作的时候,需要先看一下该表是否有数据正在被占用,而表的意向锁就可以让我们直观的知道该表是否被占用,而不用去遍历数据行判断。

1.2 以锁的算法分类

  • 记录锁(Record Locks)
  • 临键锁(Next-Key Locks)
  • 间隙锁(Gap Locks)

说明:以下内容均是where条件中的字段为索引的情况

(1)记录锁Record Locks

当使用条件是等值查询记录的时候的场景,会出现记录锁。eg:当查询某一个已经存在的id为1的记录时:

在这里插入图片描述

(2)间隙锁Gap Locks

范围查询的时候一般会出现它,他是InnoDB独有的,只存在于RR隔离级别

说明:间隙是根据数据库的记录划分的,不是根据条件划分的

在这里插入图片描述

//Session A
select * from table where id > 12 for update
//上面sql实际上锁住的区间是 大于10的区间,如下

在这里插入图片描述

//Session B
update table set name = 'xinwei' where id = 11;
//这个情况下,Session B这条语句是执行不成功的,只可以修改 <= 10这个范围的记录
  • 间隙锁与间隙锁本身不冲突;
  • 间隙锁是为了阻塞插入,仅有在InnoDB的RR级别中才有;
  • 有效解决了幻读错误
(3)临键锁(Next-Key Locks)

锁的是左开右闭的区间(与Gap锁的最大区别),相当于间隙锁 + 记录

select * from where id > 1 and id < 10 for update

执行上面sql时,锁住的区间为(1,5]和(5,10]
在这里插入图片描述

2、MVCC多版本并发控制

主要是为了提升并发性能的考虑,避免了很多情况下加锁的控制而增加的开销,使得:读不加锁、读写不冲突。且MVCC只适用于RC和RR两个隔离级别下工作,其他两个隔离级别不兼容。
在MVCC中,读操作可以分为两类:快照读、当前读。

  • 快照读:select操作,不加锁(有例外:select … lock in share mode)
  • 当前读:插入/更新/删除,需要加锁;
//加S锁
select ...... lock in share mode
//加X锁
select ...... for update
update
delete
insert

为什么 插入/更新/删除 都归为当前读?请看下节【加锁的过程分析update案例】

3、隔离级别的实现总结※

  • Read Uncommited
    可读取未提交记录,此隔离级别不会使用,忽略

  • Read Committed(RC)
    快照读:MVCC处理(每次进行快照读时生成新的readview,因此有不可重复读问题
    当前读:RC隔离级别保证对读取到的记录加锁(记录锁),存在幻读。

  • Repeatable Read(RR)
    快照读:MVCC处理(只有第一次进行快照读时才会生成readview,之后的读操作都会使用第一次生成的readview
    当前读:RR隔离级别保证对读取到的记录加锁(记录锁),同时保证对读取的范围加锁(间隙锁),使新的满足查询条件的记录不能够插入 ,不存在幻读现象

  • Serializable
    从MVCC并发控制退化为基于锁的并发控制。不区分快照读和当前读,所有的操作均为当前读,读加锁(S锁),写加写锁(X锁)。在此隔离级别下,读写冲突,因此并发度记录下降,不建议使用。

在MySQL的事务引擎中,InnoDB是使用范围最广的,它默认的事务隔离级别是RR(可重复读),在标准的事务隔离级别定义下,RR是不能防止幻读的,InnoDB使用了Next-key locks实现了防止幻读的发生。

三、案例理解:加锁的过程分析

【说明】MySQL的当前读操作执行流程如下,以update为例,假设该过滤条件为索引。
img

  1. MySQL Server会根据SQL中的where条件,读取第一条满足条件的记录;
  2. 然后InnoDB会将第一条记录加锁返回
  3. MySQL Server收到这条加锁的记录后,会update这条记录;
  4. 一条记录完成操作后,会再读取下一条记录,直到没有满足条件的记录为止。

根据上图的交互,针对一条当前读的SQL,InnoDB与MySQL Server的交互,是一条一条进行的,因此,加锁也是一条一条进行的。先对一条满足条件的记录加锁,返回给MySQL Server,做一些DML操作,再读取下一条…加锁…,直至读取完毕。

分析如下两条SQL分别加了什么锁?

//SQL1:
select * from t1 where id = 10;
//SQL2:
delete from t1 where id = 10;

**SQL1:**select操作均不加锁,使用MVCC解决并发问题;

SQL2:

在回答出详细答案之前,还需要知道以下前提,前提不同得到的答案就不同:

1. id是不是主键?
2. 当前的隔离级别是什么?
3. id列如果不是主键,id列上有索引吗?
4. id列上如果有二级索引,那么这个索引是唯一索引吗?
5. 2个SQL的执行计划是什么?索引扫描 or 全表扫描?

在明确这些条件后,给定的sql会加什么锁,也就一目了然了,分析如下:

组合1:id列主键,RC隔离级别
组合2:id列是唯一索引,RC隔离级别
组合3:id列是二级非唯一索引,RC隔离级别
组合4:id列上没有索引,RC隔离级别
组合5:id列是主键,RR隔离级别
组合6:id列是唯一索引,RR隔离级别
组合7:id列是二级非唯一索引,RR隔离级别
组合8:id列上没有索引,RR隔离级别
组合9:Serializable隔离级别

组合一:id唯一索引 + RC

结论:只需要在id = 10这条记录上加X锁即可。

组合二:id唯一索引 + RC

分析:设主键是name列,而id是二级的唯一索引,因此delete语句会选择走id列的非聚簇索引进行where条件的过滤,查找到id = 10的记录(叶子节点索引对应主键name)后,首先会在非聚簇索引上的id = 10索引记录上加X锁;同时,根据读取到的name,回主键索引,查询主键name = ‘d’ 对应的记录,并对该记录加锁

提问:为什么聚簇索引上的记录也要加锁?试想一下,如果存在另一个并发事务,该事务通过主键索引来更新:update ti id = 100 where name = ‘d’;此时delete语句如果没有将主键索引上的记录加锁,那么并发事务会感知不到delete语句的存在,违背了同一记录数据 修改/删除的串行执行约束。

image-20220610161506901

结论:若id列是唯一索引,那么该sql需要加2个X锁,一个对应id列唯一索引上的id = 10的记录,另一把锁对应主键索引上[name = ‘d’, id=10]的记录。

组合三:id非唯一索引 + RC

分析:在id列的普通索引上,通过where id = 10查询满足条件的记录,对满足条件的记录加锁;同时,这些记录对应的主键索引上的记录也都加上锁。

img

结论:若id列是普通索引(非唯一),那么普通索引中满足所有查询条件的记录都会被加锁;同时,这些记录在主键索引上的记录也会被加锁。

组合四:id无索引 + RC(注意!)

img

全加锁的原因】:由于id列没有索引,SQL会走主键索引进行全扫描,所以主键索引每条记录,无论是否满足条件都会被先加上X锁,加锁完返回到MySQL Server由它来进行对where的过滤。

但为了效率考虑,MySQL做了优化,对不满足条件的记录,在判断后释放锁,最终持有的,是满足条件的记录的锁,但不满足条件记录上的加锁/释放锁动作不会省略。

组合五:id主键 + RR

在主键索引中id = 10的记录上加X锁即可。

组合六:id唯一索引 + RR

与组合二一致,加两个X锁:id唯一索引满足条件的记录上一个,对应的主键索引上的记录一个。

组合七:id普通索引 + RR

img

在RR级别下,id为普通索引(不能保证唯一性,);

首先会通过id索引定位到第一条满足查询条件的记录,(1)加记录上的X锁,(2)加GAP上的GAP锁;(3)然后加主键索引上的记录X锁,然后返回;然后读取下一条,重复进行。直至进行到第一条不满足条件的记录[11, f]时,不需要加记录X锁,但仍需要加GAP锁,最后返回结束。

【什么时候会取得间隙所?】:这和隔离级别有关,只有在RR或以上级别下的特定操作才会取得间隙锁。

【RR级别下如何解决幻读的?】:MySQL InnoDB的可重复读并不保证避免幻读,需要应用使用加锁读来保证。而这个加锁读使用到的机制就是next-key locks。

mysql 的重复读解决了幻读的现象,但是需要 加上 select for update/lock in share mode 变成当前读避免幻读,普通读select存在幻读

组合八:id无索引 + RR

img

设name是主键索引

上图是一个恐怖的现象。在RR级别下,如果进行全表扫描的当前读那么会锁上表中的所有记录

首先,主键索引上的所有记录,都被加上了X锁。其次主键索引每条记录间的间隙GAP,也都同时被加上了GAP锁

上表中除了不加锁的快照读,其他任何加锁的并发SQL,均不能执行、不能更新、不能删除、不能插入,全表被锁死

【优化】

对于不满足查询条件的记录,Mysql Server会提前释放锁,同时不加GAP锁。

组合九:Serializable

结论:在MySQL/InnoDB中,所谓的读不加锁,并不适用于所有的情况,而是与隔离级别相关的。

Seriallizable隔离级别,读不加锁就不再成立,所有的读操作,都是当前读!

※解决幻读的方法

  1. 将事务的隔离级别升级为Serializable
  2. 在RR隔离级别下,给事务操作的这张表添加表锁
  3. 在RR隔离级别下,给事务操作的这张表添加Next-Key Locks临键锁
  • 6
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值