数据库的隔离级别
Posted BUG指挥官
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了数据库的隔离级别相关的知识,希望对你有一定的参考价值。
数据库事务的隔离级别有4个,由低到高依次为Read uncommitted、Read committed、Repeatable read、Serializable,这四个级别可以逐个解决脏读、不可重复读、幻读这几类问题。
√: 可能出现 ×: 不会出现
事务的隔离级别 | 脏读 | 不可重复读 | 幻读 |
Read uncommitted | √ | √ | √ |
Read committed--Sql Server , Oracle | × | √ | √ |
Repeatable read--mysql | × | × | √ |
Serializable | × | × | × |
注意:我们讨论隔离级别的场景,主要是在多个事务并发的情况下,因此,接下来的讲解都围绕事务并发。
Read uncommitted 读未提交
公司发工资了,领导把5000元打到singo的账号上,但是该事务并未提交,而singo正好去查看账户,发现工资已经到账,是5000元整,非常高兴。可是不幸的是,领导发现发给singo的工资金额不对,是2000元,于是迅速回滚了事务,修改金额后,将事务提交,最后singo实际的工资只有2000元,singo空欢喜一场。
出现上述情况,即我们所说的脏读,两个并发的事务,“事务A:领导给singo发工资”、“事务B:singo查询工资账户”,事务B读取了事务A尚未提交的数据。
当隔离级别设置为Read uncommitted时,就可能出现脏读,如何避免脏读,请看下一个隔离级别。
Read committed 读提交
singo拿着工资卡去消费,系统读取到卡里确实有2000元,而此时她的老婆也正好在网上转账,把singo工资卡的2000元转到另一账户,并在singo之前提交了事务,当singo扣款时,系统检查到singo的工资卡已经没有钱,扣款失败,singo十分纳闷,明明卡里有钱,为何......
出现上述情况,即我们所说的不可重复读,两个并发的事务,“事务A:singo消费”、“事务B:singo的老婆网上转账”,事务A事先读取了数据,事务B紧接了更新了数据,并提交了事务,而事务A再次读取该数据时,数据已经发生了改变。
当隔离级别设置为Read committed时,避免了脏读,但是可能会造成不可重复读。
大多数数据库的默认级别就是Read committed,比如Sql Server , Oracle。如何解决不可重复读这一问题,请看下一个隔离级别。
Repeatable read 重复读
当隔离级别设置为Repeatable read时,可以避免不可重复读。当singo拿着工资卡去消费时,一旦系统开始读取工资卡信息(即事务开始),singo的老婆就不可能对该记录进行修改,也就是singo的老婆不能在此时转账。
虽然Repeatable read避免了不可重复读,但还有可能出现幻读。
singo的老婆工作在银行部门,她时常通过银行内部系统查看singo的信用卡消费记录。有一天,她正在查询到singo当月信用卡的总消费金额(select sum(amount) from transaction where month = 本月)为80元,而singo此时正好在外面胡吃海塞后在收银台买单,消费1000元,即新增了一条1000元的消费记录(insert transaction ... ),并提交了事务,随后singo的老婆将singo当月信用卡消费的明细打印到A4纸上,却发现消费总额为1080元,singo的老婆很诧异,以为出现了幻觉,幻读就这样产生了。
注:Mysql的默认隔离级别就是Repeatable read。
Serializable 序列化
Serializable是最高的事务隔离级别,同时代价也花费最高,性能很低,一般很少使用,在该级别下,事务顺序执行,不仅可以避免脏读、不可重复读,还避免了幻像读。
.........
READ UNCOMMITTED
READ UNCOMMITTED是限制性最弱的隔离级别,因为该级别忽略其他事务放置的锁。使用READ UNCOMMITTED级别执行的事务,可以读取尚未由其他事务提交的修改后的数据值,这些行为称为“脏”读。这是因为在Read Uncommitted级别下,读取数据不需要加S锁,这样就不会跟被修改的数据上的X锁冲突。比如,事务1修改一行,事务2在事务1提交之前读取了这一行。如果事务1回滚,事务2就读取了一行没有提交的数据,这样的数据我们认为是不存在的。
READ COMMITTED
READ COMMITTED(Nonrepeatable reads)是SQL Server默认的隔离级别。该级别通过指定语句不能读取其他事务已修改但是尚未提交的数据值,禁止执行脏读。在当前事务中的各个语句执行之间,其他事务仍可以修改、插入或删除数据,从而产生无法重复的读操作,或“影子”数据。比如,事务1读取了一行,事务2修改或者删除这一行并且提交。如果事务1想再一次读取这一行,它将获得修改后的数据或者发现这一样已经被删除,因此事务的第二次读取结果与第一次读取结果不同,因此也叫不可重复读。
实验1
query1:事务1
--step1:创建实验数据
select * into Employee from AdventureWorks.HumanResources.Employee
alter table Employee add constraint pk_Employee_EmployeeID primary key(EmployeeID)
--step2:设置隔离级别,这是数据库的默认隔离界别
SET TRANSACTION ISOLATION LEVEL READ COMMITTED
--step3:开启第一个事务
BEGIN TRAN tran1
--step4:执行select操作,查看VacationHours,对查找的记录加S锁,在语句执行完以后自动释放S锁
SELECT EmployeeID, VacationHours
FROM Employee
WHERE EmployeeID = 4;
--step5:查看当前加锁情况,没有发现在Employee表上面有锁,这是因为当前的隔离界别是READ COMMITTED
--在执行完step2以后马上释放了S锁.
SELECT request_session_id, resource_type, resource_associated_entity_id,
request_status, request_mode, resource_description
FROM sys.dm_tran_locks
查看锁的情况如下图所示,我们发现在只有在数据库级别的S锁,而没有在表级别或者更低级别的锁,这是因为在Read Committed级别下,S锁在语句执行完以后就被释放。
query2:事务2
--step6:开启第二个事务
BEGIN TRAN tran2;
--step7:修改VacationHours,需要获得排它锁X,在VacationHours上没有有S锁
UPDATE Employee
SET VacationHours = VacationHours - 8
WHERE EmployeeID = 4;
--step8:查看当前加锁情况
SELECT request_session_id, resource_type, resource_associated_entity_id,
request_status, request_mode, resource_description
FROM sys.dm_tran_locks
在开启另外一个update事务以后,我们再去查看当前的锁状况,如下图所示,我们发现在表(Object)级别上加了IX锁,在这张表所在的Page上也加了IX锁,因为表加了聚集索引,所以在叶子结点上加了X锁,这个锁的类型是KEY。
然后我们回到事务1当中再次执行查询语句,我们会发现查询被阻塞,我们新建一个查询query3来查看这个时候的锁状况,其查询结果如下,我们可以发现查询操作需要在KEY级别上申请S锁,在Page和表(Object)上面申请IS锁,但是因为Key上面原先有了X锁,与当前读操作申请的S锁冲突,所以这一步处于WAIT状态。
如果此时提交事务2的update操作,那么事务1的select操作不再被阻塞,得到查询结果,但是我们发现此时得到的查询结果与第一次得到的查询结果不同,这也是为什么将read committed称为不可重复读,因为同一个事物内的两次相同的查询操作的结果可能不同。
REPEATABLE READ
REPEATABLE READ是比READ COMMITTED限制性更强的隔离级别。该级别包括READ COMMITTED,并且另外指定了在当前事务提交之前,其他任何事务均不可以修改或删除当前事务已读取的数据。并发性低于 READ COMMITTED,因为已读数据的共享锁在整个事务期间持有,而不是在每个语句结束时释放。比如,事务1读取了一行,事务2想修改或者删除这一行并且提交,但是因为事务1尚未提交,数据行中有事务1的锁,事务2无法进行更新操作,因此事务2阻塞。如果这时候事务1想再一次读取这一行,它读取结果与第一次读取结果相同,因此叫可重复读。
实验2
query1:事务1
--step1:创建实验数据
select * into Employee from AdventureWorks.HumanResources.Employee
alter table Employee add constraint pk_Employee_EmployeeID primary key(EmployeeID)
--step2:设置隔离级别
SET TRANSACTION ISOLATION LEVEL REPEATABLE READ
--step3:开启第一个事务
BEGIN TRAN tran1
--step4:执行select操作,查看VacationHours
SELECT EmployeeID, VacationHours
FROM Employee
WHERE EmployeeID = 4;
--step5:查看当前加锁情况,发现在Employee表上面有S锁,这是因为当前的隔离界别是REPEATABLE READ
--S锁只有在事务执行完以后才会被释放.
SELECT request_session_id, resource_type, resource_associated_entity_id,
request_status, request_mode, resource_description
FROM sys.dm_tran_locks
查询锁状态的结果如下图所示,我们发现在KEY上面加了S锁,在Page和Object上面加了IS锁,这是因为在Repeatable Read级别下S锁要在事务执行完以后才会被释放。
query2:事务2
--step6:开启第二个事务
BEGIN TRAN tran2;
--step7:修改VacationHours,需要获得排他锁X,在VacationHours上有S锁,出现冲突,所以update操作被阻塞
UPDATE Employee
SET VacationHours = VacationHours - 8
WHERE EmployeeID = 4;
执行上述update操作的时候发现该操作被阻塞,这是因为update操作要加排它锁X,而因为原先的查询操作的S锁没有释放,所以两者冲突。我们新建一个查询3执行查询锁状态操作,发现结果如下图所示,我们可以发现是WAIT发生在对KEY加X锁的操作上面。
此时再次执行查询1中的select操作,我们发现查询结果跟第一次相同,所以这个叫做可重复读操作。但是可重复读操作并不是特定指两次读取的数据一模一样,Repeatable Read存在的一个问题是幻读,就是第二次读取的数据返回的条目数比第一次返回的条目数更多。
比如在Repeatable Read隔离级别下,事务1第一次执行查询select id from users where id>1 and id <10,返回的结果是2,4,6,8。这个时候事务1没有提交,那么对2,4,6,8上面依然保持有S锁。此时事务2执行一次插入操作insert into user(id) valuse(3),插入成功。此时再次执行事务1中的查询,那么返回结果就是2,3,4,6,8。这里的3就是因为幻读而出现的。因此可以得出结论:REPEATABLE READ隔离级别保证了在相同的查询条件下,同一个事务中的两个查询,第二次读取的内容肯定包换第一次读到的内容。
SERIALIZABLE
SERIALIZABLE 是限制性最强的隔离级别,因为该级别锁定整个范围的键,并一直持有锁,直到事务完成。该级别包括REPEATABLE READ,并增加了在事务完成之前,其他事务不能向事务已读取的范围插入新行的限制。比如,事务1读取了一系列满足搜索条件的行。事务2在执行SQL statement产生一行或者多行满足事务1搜索条件的行时会冲突,则事务2回滚。这时事务1再次读取了一系列满足相同搜索条件的行,第二次读取的结果和第一次读取的结果相同。
重复读与幻读
重复读是为了保证在一个事务中,相同查询条件下读取的数据值不发生改变,但是不能保证下次同样条件查询,结果记录数不会增加。
幻读就是为了解决这个问题而存在的,他将这个查询范围都加锁了,所以就不能再往这个范围内插入数据,这就是SERIALIZABLE 隔离级别做的事情。
隔离级别与锁的关系
- 在Read Uncommitted级别下,读操作不加S锁;
- 在Read Committed级别下,读操作需要加S锁,但是在语句执行完以后释放S锁;
- 在Repeatable Read级别下,读操作需要加S锁,但是在事务提交之前并不释放S锁,也就是必须等待事务执行完毕以后才释放S锁。
- 在Serialize级别下,会在Repeatable Read级别的基础上,添加一个范围锁。保证一个事务内的两次查询结果完全一样,而不会出现第一次查询结果是第二次查询结果的子集。
聊聊数据库事务隔离级别——如何定义隔离级别
1、说在前面
今天想和大家聊一聊数据库事务的隔离性到底想解决什么问题.我曾经对它的理解仅仅停留在事务有不同的隔离级别,而满足了这些隔离级别事务就能禁止不同的异常现象发生.也会有一种模糊的印象,事务隔离性是为了解决多个事务的并发问题(这里先就不纠结并发与并行的语意了),而且它也许用了锁来解决这个问题.然后就是一堆锁的概念,行锁、表锁、谓词锁等.又或者进一步去解了它的锁概念,发现还有共享锁、排他锁、U锁等等.又或许还了解过MySql的可重复读,发现MVCC是它的实现基础.但是即使堆积了这些概念,我还是不清楚隔离性到底想表达的是一个什么样的期望?同时为什么会定义这些隔离级别,而不是其他的隔离界别?而这就是这篇文章想讨论的问题,希望读完这片文章之后,你对隔离级别的定义有一个更好的认识.
2、或许你该先纠正对ACID的认识
在解释第一节(说在前面)中的问题之前,我们先来重新认识一下ACID.不知你是否有这种感觉,除了D(持久性)的概念是比较容易理解之外,A(原子性)、C(一致性)和I(隔离性)概念的边界似乎并不是很清晰(原子性,一致性,隔离性不都是为了解决并发的一致性问题吗?).这里的主要问题是我们时常将不同领域的概念混淆在了一起.因为如果你了解并发编程,很容易错误地将A和C的概念映射到与锁保护下的原子性和一致性概念.并发编程的锁原语有这么一个概念(这里更多的是java的并发编程,对与其他语言锁的原语概念应该也是一样的吧):锁具有原子性和可见性,线程对被锁(排它锁)保护的代码是独占的,并且代码中的数据变更(本地内存、缓存数据,对外设的数据不在它的管理范围内)对其他线程不可见,直到锁释放.而事务的A是如何解释的呢:A表示的是一个事务的执行是原子的,要不全部执行,要不全部不执行,不存在执行一部分.对比一下可以发现,事务的A其实并不关心可见性问题,而可见性恰恰是事务的I需要去解决的;同时事务A也不强调独占性(注1).事务的A关心的是当事务执行过程中失败了,需要进行回滚,将数据状态恢复到事务执行之前,就好像事务没有执行一样.这这里我们甚至可以说,事务的A表现了事务的可终止能力,数据库中往往用undo日志来实现该能力(注2).
上述说明了A强调的是事务的回滚能力,I强调的是事务的可见性问题.那么C呢?抱歉,C的概念真的很模糊,且不说一致性这个词被用在各种不同的领域(数据库事务一致性、并发编程锁一致性、分布式副本一致性等等),但事务的一致性其实想表达的是在数据库的支持下,从应用程序看到了预期的数据.所以说C不应该和A、I、D在同一层面的,而应该是A、I、D一起给应用程序提供了一致性的体验:(A、I、D)-->C.
D是一个比较容易理解的概念,即当事务提交后,数据被储存到持久性介质中,不会再丢失.不过这种保证也很难,比如磁盘损坏了.即使你用了RAID,甚至是分布式方案,都不可避免存在由于程序bug或者人为、自然灾害等原因导致数据丢失的概率,只不过概率太小或者有些问题本就不在数据库的能力范围之内了(比如地球被毁灭了.不过这样也很酷,毕竟数据库厂商可以说自己的产品和地球共存亡了).不过另一个问题却是很现实的,那就是内存数据刷入磁盘的性能问题.最可靠的方式就是每个事务提交之后都强制执行fsync,保证数据刷入磁盘后再回复用户事务提交成功,但是这样的性能却是不可接受的.当然我们也可以定期调用fsync,这样性能就大大提高了,但是带来了数据丢失的风险.设想事务写入缓存页并回复了客户端,但是在下一次执行fsync之前,操作系统崩溃了,那么在此期间的事务提交都将丢失.当然我们也可以用批量提交这种方式,事务写入缓存页后不回复用户,等待收集了足够多的事务后,一次性刷入磁盘,然后同时回复用户提交成功.这里可以增加更多的智能策略,更具系统当前的吞吐量,系统资源使用情况,用户超时情况来决策是否使用批量提交以及批量的数据量.
至此,ACID的概念算是介绍完了.接下来我们先了解一下事务怎么样才算满足隔离性.
3、你是否可以接受串行执行
在第2节中我们说过,锁具有独占性,线程就像排队经过被锁保护的代码;那么试想,事务如果是通过T1||T2||T3这种串行的方式被执行,那么事务之间并不存在并发竞争,也就不会出现脏读这样的并发可见性问题.在这样的事务执行(调度)方式下,事务是满足隔离性的.但是这样的性能却是很糟糕的(现在有些优秀的数据库也在采用串行的方式,如redis(注3)),完全发挥不了并发的优势.为了避免串行(注4)这种糟糕的方式,或许我们该仔细分析一下,是什么原因导致的并发问题的.
拆解事务,无非存在两种操作,读(r),写(w).而对同一个对象o的并发操作,除了r-r外,r-w,w-r,w-w都是会存在因果关系的(如果交换执行顺序,最终结果会不一样),也称为冲突.为了更加形式化地表现出这种冲突,我们采用对象版本变更的方式.<o,1>表示1号版本对象o.图1表示了对象经过事务的读写操作后的就像版本变更情况.图2通过对象版本变更说明事务在并发情况下会出现的异常现象(注5).
图1:对象版本变更
图2:对象版本变更解释事务异常现象
除了幻读外,图2基本已经涵盖了事务隔离界别需要解决的问题(至少ANSI SQL-92标准是这么定义的,当然后续文章会介绍这个标准存在问题).这里还有一个意外结论,异常现象的图中都存在环(这里可以给一个其他结论,只要图中存在环,那么就是存在异常现象的.可以这样解释:图中对象版本的流动其实是事务之间的因果关系,当存在环时,一个事务既是另一事务的因,又是它的果.这样的因果关系是混乱的).因此隔离性的任务是避免这种环产生,或者说是出现事务之间混乱的因果关系发生.而阻止这些问题最早期的方法就是二阶段锁.
4、SQL-92隔离级别的定义
根据我的理解,SQL-92标准中定义的隔离级别是通过逐级破坏两阶段锁来定义的.为了更好地说明这个问题,这里先在贴一下SQL-92的隔离级别定义.
4.1 异常现象
-
P1——脏读 事务T1写对象o, 但在T1还未提交时事务T2读对象o; 由于异常T1进行回滚, 最后. T2读到了不存在的对象o -
P2——不可重复读 事务T1读对象o, 事务T2写对象o完成提交, 此时T1再次读对象o, 最后在T1中, 先后两次读到的数据不一致; -
P3——幻读 事务T1查询集合满足条件P(where), 事务T2插入满足条件P的一条记录o并提交, 事务T1再次查询集合满足条件P; 最后导致T1先后两次读到的数据不一致;
4.2 隔离级别
图3:事务隔离级别
5、二阶段锁
二阶段锁可以实现隔离性(当然为了解决幻读,还需要引入谓词锁);二阶段锁的定义和其实现隔离性证明网上资料很多,这里不在赘述; 这里想重点介绍一下如何通过二阶段锁来定义隔离级别.为了说明问题, 这个还是简单介绍一下二阶段锁.
5.1 二阶段锁的定义
二阶段锁有两个概念,分别是合规事务和事务二阶段.
-
事务合规: 如果事务中所有的读写都有锁(lock)覆盖,并且每一个lock动作都有一个相应unlock操作, 那么该事务就是合规的. 图4说明了这是一个不合规事务
图4:不合规事务
-
事务二阶段: 如果所有的lock操作都在unlock操作之前, 那么该事务(封锁策略)是二阶段的. 因此整个事务可以分为加锁阶段和解锁阶段.
图5:锁的两个阶段
5.3 隔离级别
通过对二阶段锁的破坏,我们可以整理出图6.其中1级就是读未提交(在该级别下会存在脏读),2级是读已提交,3级是可重复读.而0级比1级存在更多隔离性问题,甚至存在脏写.注意4级要解决的是幻读问题,它是通过二阶段谓词锁实现的.原理和二阶段锁一样,只是谓词锁锁的不是单个对象,而是一个对象范围(SQL中满足WHERE条件的对象范围).
图6:通过二阶段锁定义隔离级别
至此,我想你对事务的隔离性以及隔离级别的划分应该会有一个更加感性的认识了.有任何问题欢迎留言一起探讨.
6、注
(1) 独占性其实是并发领域(或者分布式领域中)对系统可线性化的最简单模型,而这个简单性背后的代价是性能,这是数据库不能接受的.
(2)undo除了用于事务回滚,很多时候也被用于实现MVCC.
(3)单核并发的目的除了在人机交互中给你提供一种多应用同时运行的错觉之外,另一个更重要的原因是CPU计算能力与IO速率之间的严重不匹配.但是如果将数据全部加载到内存中,那么可以明显缓解这种不匹配.因此通过单线程也可以比较容易跑满CPU.而且还避免了线程上下文切换的成本.当然这种方式无法充分利用多核,不过却可以非常简化系统设计;
(4)对于一致性问题, 很多时候我们需要的是因果一致性,而不是线性化,而串行就是在时间上是线性化的;线性化一定满足因果一致性,而因果一致性一般不是线性化;例如User1和User2编辑个人信息,除了唯一Id外,其他信息一般都是不存在因果关系的,那么它们之间的完全可以并发进行;
(5)更新丢失和脏写需要区分一下,如果图中T1已经提交,那么属于更新丢失;如果未提交,那么应该叫做脏写;
7、参考资料
[1]《事务处理.概念与技术》
[2]《数据密集型应用系统设计》
以上是关于数据库的隔离级别的主要内容,如果未能解决你的问题,请参考以下文章
Java -- 每日一问:谈谈MySQL支持的事务隔离级别,以及悲观锁和乐观锁的原理和应用场景?