0
点赞
收藏
分享

微信扫一扫

SQLSERVER 的四个事务隔离级别到底怎么理解?

一:背景

1. 讲故事

在有关SQLSERVER的各种参考资料中,经常会看到如下四种事务隔离级别。

  • READ UNCOMMITTED
  • READ COMMITTED
  • SERIALIZABLE
  • REPEATABLE READ

随之而来的是大量的文字解释,还会附带各种 ​​脏读​​​, ​​幻读​​​, ​​不可重复读​​​ 常常会把初学者弄得晕头转向,其实事务的本质就是隔离,落地就需要锁机制,理解这四种隔离方式的​​花式加锁​​​,应该就可以入门了,那如何可视化的观察 ​​锁​​​ 过程呢?这里借助 ​​SQL Profile​​ 工具。

二:四种事务隔离方式

1. 测试数据准备

还是用上一篇创建的 ​​post​​ 表,脚本如下:

CREATE TABLE post(id INT IDENTITY,content char(4000))
GO

INSERT INTO dbo.post VALUES('aaa')
INSERT INTO dbo.post VALUES('bbb')
INSERT INTO dbo.post VALUES('ccc');
INSERT INTO dbo.post VALUES('ddd');
INSERT INTO dbo.post VALUES('eee');
INSERT INTO dbo.post VALUES('fff');

有了测试数据之后,我们按照隔离级别 ​​高 -> 低​​ 的顺序来观察吧。

2. SERIALIZABLE 事务

​事务串行化​​​ 其实很好理解,如果要在 C# 中找对应那就是 ​​ReaderWriterLock​​​,读写事务是完全排斥的,接下来把 SQLSERVER 的隔离级别调整为 ​​SERIALIZABLE​​。

SET TRAN ISOLATION LEVEL SERIALIZABLE
GO

BEGIN TRAN
SELECT * FROM dbo.post WHERE id=3
COMMIT

打开 profile,选择 ​​lock:Acquired, lock:Released,SQL:StmtStarting​​ 选项,开启观察。

SQLSERVER 的四个事务隔离级别到底怎么理解?_隔离级别

从图中可以清楚的看到,SQLSERVER 直接对 ​​post​​ 附加了 S 锁,在 COMMIT 之后才真正的释放,在 S 锁期间, Insert 和 Update 引发的 X 锁是进不来的,所以就会存在相互阻塞的情况,也许这就是串行化的由来吧。

sqlserver 是一个支持多用户并发的数据库程序,如果锁粒度这么粗,必定给并发带来非常大的负面影响,不过文章开头的那三个指标 ​​脏读, 幻读, 不可重复读​​ 肯定都是不会出现的。

2. REPEATABLE READ 事务

什么叫 ​​可重复读​​​ 呢?简而言之就是同一个 select 查询执行二次,不会出现记录修改的情况,在真实场景中两次 ​​select​​​ 查询期间,可能会有其他事务修改了记录,如果当前是 ​​REPEATABLE READ​​ 模式,这是被禁止的,接下来的问题是如何落地实现呢?我们来看看 SQLSERVER 是如何做到的,参考sql 如下:

SET TRAN ISOLATION LEVEL REPEATABLE READ
GO

BEGIN TRAN
SELECT * FROM dbo.post WHERE id=3
COMMIT

SQLSERVER 的四个事务隔离级别到底怎么理解?_隔离级别_02

这个图可能有些朋友看不懂,我稍微解释一下吧,数据库由数据页​​Page​​​组成,数据页由记录​​RID​​​ 组成,有了这个基础就好理解了, SQLSERVER 会在事务期间把 ​​1:489:0​​​ 也就是 ​​id=3​​​ 这个记录全程附加 ​​S​​​ 锁,直到事务提交才释放 ​​S​​​ 锁,在事务期间任何对它修改的 ​​X​​​ 锁都无法对其变更,从而实现事务期间的 ​​可重复读​​ 功能,如果大家不明白可以再琢磨琢磨。

这里有一个细节需要大家注意一下,​​可重复读​​​ 的场景下会出现 ​​幻读​​ 的情况,幻读就是两次查询出的结果集可能会不一样,比如第一次是 3 条记录,第二次变成了 5 条记录,为了方便理解我来简单演示一下。

  • 会话1

SET TRAN ISOLATION LEVEL REPEATABLE READ
GO

BEGIN TRAN
SELECT * FROM dbo.post WHERE id >3
WAITFOR DELAY '00:00:05'
SELECT * FROM dbo.post WHERE id >3
COMMIT

  • 会话2

在​​会话1​​​ 执行的 5s 期间执行 ​​会话2​​ 语句。

BEGIN TRAN 
INSERT INTO dbo.post(content) VALUES ('gggggg')
COMMIT

稍等片刻之后,会发现多了一个 ​​记录7​​ ,截图如下:

SQLSERVER 的四个事务隔离级别到底怎么理解?_隔离级别_03

3. READ COMMITTED

​提交读​​​ 是目前 SQLSERVER 默认的隔离级别,它是以不会出现 ​​脏读​​​ 为唯一目标,何为​​脏读​​​,简而言之就是读取到了别的事务未提交的修改数据,这个数据有可能会被其他事务在后续回滚掉,如果真的被其他事务 ​​回滚​​​ 了,那你读到了这样的数据就是 ​​错误​​ 的数据,可能会给你的系统带来非常隐蔽的 bug,为了说明这个现象,我们用两个会话来测试一下帮助大家理解。

  • 会话1

在这个会话中,将 ​​id=3​​​ 的记录修改成 ​​zzzzz​

BEGIN TRAN 
UPDATE dbo.post SET content='zzzzz' WHERE id=3
WAITFOR DELAY '00:00:05'
ROLLBACK

  • 会话2

这个会话中,重复执行sql查询。

BEGIN TRAN 
SELECT * FROM dbo.post WITH(NOLOCK) WHERE id =3 -- 脏读啦
WAITFOR DELAY '00:00:05'
SELECT * FROM dbo.post WITH(NOLOCK) WHERE id =3 -- 正确的数据
COMMIT

SQLSERVER 的四个事务隔离级别到底怎么理解?_sql_04

为了实现脏读这里加了 ​​nolock​​​ 关键词,从图中明显的看到,获取的 ​​zzzzz​​ 数据是错误的,在一些和钱打交道的系统中是被严厉禁止的。

有了这些基础再理解 ​​可提交读​​ 可能会容易些,是不是很好奇 SQLSERVER 是如何实现的呢? 参考 sql 如下:

SET TRAN ISOLATION LEVEL READ COMMITTED
GO

BEGIN TRAN
SELECT * FROM dbo.post WHERE id =3
COMMIT

SQLSERVER 的四个事务隔离级别到底怎么理解?_sql_05

从加锁流程看,SQLSERVER 会逐一扫描数据页附加 IS 锁,扫完马上就释放,不像前面那样保持到 COMMIT 之后,如果找到记录所在的 Page 时,会对下面的所有记录附加 S 锁,这个时候 X 锁就进不来了,这就是它的实现原理,大家可以把刚才的 ​​脏读​​ 的sql中的 nolock 去掉试试看,两次读取结果都是一样的。

4. READ UNCOMMITTED

本质上来说 ​​READ UNCOMMITTED​​​ 和 ​​nolock​​​ 的效果是一样的,会引发脏读现象,主要是因为 ​​READ UNCOMMITTED​​ 根本就不会对表记录使用任何锁,参考sql如下:

SET TRAN ISOLATION LEVEL READ UNCOMMITTED
GO

BEGIN TRAN
SELECT * FROM dbo.post WHERE id =3
COMMIT

接下来观察 sqlprofile 的输出。

SQLSERVER 的四个事务隔离级别到底怎么理解?_隔离级别_06

可以看到 ​​READ UNCOMMITTED​​​ 只会对​​表​​​和​​堆表结构​​​这种架构附加锁,不会对表中记录附加任何锁,也就会引发 ​​脏读​​ 现象。

三:总结

其实 SQLSERVER 还有带版本的 ​​SNAPSHOT​​ 隔离级别,在真实场景中往往会给 TempDB 造成很大的压力,这里就不介绍了。

相信通过 Profile 观察到的加锁动态过程,会让大家有更深入的理解。



举报

相关推荐

0 条评论