前言
在上一篇文章当中,我们讨论了事务的一般理论以及事务的隔离级别的起源,并讨论了SQL-92标准的问题的修正的论文 「A Critique of ANSI SQL Isolation Levels」。
改论文在标准的基础上推广出了异常现象P1-P3广义的解释,然后根据A1-A3的容忍度补充了标准内的隔离级别P0的个隔离级别。本文继续讨论 「A Critique of ANSI SQL Isolation Levels」 中提出的异常现象。
异常的补充P4
在前文中,我们P0-P3中均是读写交互的,也就是收一个事务读,一个事务写。那么接下来,要涉及到两个写操作的异常现象。
P4 和 P4C 异常现象
Critique 论文中,针对标准新增了P4、P4C异常现象,并提出了:
- P4(Lost Update):r1[x]…w2[x]…w1[x]…c1
事务 T2 对 x 的修改被事务 T1 后续对 x 的修改覆盖了,之后事务 T1 提交,从外界看来,事务 T2 对 x 的修改丢失了。
Txn1 | Txn2 |
---|---|
begin | |
r(x,1) | |
… | begin |
… | w(x,3) |
w(x, x + 1) | |
… | |
commit(x = 2) | |
其含义为T1要更新x的数据,其首先读取x的值,然后再该值上加1再写回数据库。但是在读取x后,T2写入了新的x,而T1还是在老的x值的基础上加1。这样,T2的更新对于T1而言就像被丢弃了一样。P4 看起来有点像P0, 从定义上来看P4是在P0的基础上增加了读取的状态并影响写入,也就是说,P4是一个附加在第一次读的状态上的更新,他并没发生所谓脏读。对于Txn2来说,x = 3的操作就像丢失了一样。
跟P0的区别是什么?
- P0:在事务T1写入操作w1[x]和提交操作(c1 or a1)之间,被写入了事务T2的数据;
- P4:在事务读取操作r1[x]和写入操作w1[x]之间写入了另外事务T2的数据。
P4提出的目的是让数据提供“有状态更新”。例如,事务T1想先读取x当前的数值,如果x为1,则在此基础上把x加1,但如果在这中间有另一个事务T2更改了x的值并提交成功,那么T1的判断就应该失效了。但是写锁机制下,以及Mysql的repeatable read隔离级别下都不能屏蔽这个异常,因为他们只在写操作时才开始上锁。因此,想要解决这个问题,需要将mysql中的读从快照读,变成当前读,也就是我说的select * for update。 因为,Mysql引入了MVCC机制,为了性能牺牲部分一致性,在Mysql的快照读时候,是无法避免的P4异常的。
- P4C (Lost Update):rc1[x]…w2[x]…w1[x]…c1
P4 的 Cursor 版本。如果对 Cursor 陌生,可以看看 MySQL 或者 PostgreSQL 关于 Cursor 的文档。
Read Skew 和 Write Skew
P0-P4异常现象中,都是对单一的数据做操作从而导致的异常,现在我们来讨论两个以上的数据违反数据项约束异常现象。取名为A5 (Data Item Constraint Violation):
假设 C() 是两个数据 x 和 y 之间的一个约束条件「例如 x + y = 100 就可以理解为一个约束」,打破 C() 的异常现象可以分为两类:
- A5A:Read Skew (读偏序)
- A5B:Write Skew (写偏序)
A5A:Read Skew (读偏序)
A5A: r1[x]…w2[x]…w2[y]…c2…r1[y]…(c1 or a1)
读偏序(Read Skew:如果数据项x与y存在一致性约束,T1先对读x,而后T2修改x和y后commit,此时T1再读y。T1得到的x与y不满足原有的一致性约束。
下面假设 C() 约束为 X + Y = 100
Txn1 | Txn2 |
---|---|
begin | |
r(x,50) | begin |
… | w(x,25) |
… | w(y,75) |
commit | |
r(y, 75) | |
… |
事务T1首先读取x = 50,同时事务T2写入了 x = 25 和 y = 75。然后事务T1 继续读取了 x = 75。这时x + y = 125,违背了一致性约束。注意一点,P2级别的异常现象是一种特殊的读偏序现象(当x = y的时候)
A5B:Read Skew (Write Skew)
A5B:r1[x]…r2[y]…w1[y]…w2[x]…(c1 and c2)
事务T1先读了x 和 y 的值,发现满足约束条件,然后事务T2 也读了x 和 y,更新了x 后再提交,接着事务T1如果更新 y 的值再提交。两个事务执行完后就可能发生 x 和 y 之间的约束被打破的情况。
假设我们的约束是 x + y≤100。(x = 30 ,y= 10)
Txn1 | Txn2 |
---|---|
begin | |
r(x,30) | begin |
r(y,10) | |
w(y,60) | |
… | w(x,50) |
… | … |
事务T1读取了 x = 30,并将y = 60 写入了数据库,事务T2 读取了y = 10,并将x = 50 写回了数据库。但从两个事务中并没有违反约束,但是此时数据库中 x + y = 110,违背了一致性约束。
至此,所有的异常现象定义完毕,针对这些异常现象,我们需要该处具体的解方案,也就是定义我们的隔离级别。
符号 | 名称 | 序列 |
---|---|---|
P0 | 脏写(Dirty Write) | w1[x]…w2[x]…(c1 or a1) |
P1 | 脏读(Dirty Read) | w1[x]…r2[x]…(c1 or a1) |
P2 | 不可重复读(Non-Repeatable Read or Fuzzy Read) | r1[x]…w2[x]…(c1 or a1) |
P3 | 幻读 (Phantom) | r1[P]…w2[y in P]…(c1 or a1) |
P4/P4C | 丢失更新(Lost Update /Cursor ) | r1[x](rc1[x])…w2[x]…w1[x]…c1 |
A5A | 读偏序(Read Skew) | r1[x]…w2[x]…w2[y]…c2…r1[y]…(c1 or a1) |
A5B | 写偏序(Write Skew) | r1[x]…r2[y]…w1[y]…w2[x]…(c1 and c2) |
Critique 论文中,给出了针对以上四种异常现象锁定义的隔离级别:
ANSI基于锁调度的隔离级别
锁调度下的隔离级别
当定义好异常序列之后,我们就开始想解决办法了。上文分析,异常的产生大都是由于读写同一个谓词下的数据导致的,因此,解决办法也自然而然出来了,那就是锁。在谈论这些解决问题之前,我们先假定数据是单版本。然后再定义一个标准名叫一致性等级(degrees of consistency),每解决一个异常现象,我们提升等级Level(这个一致性等级可以等价为隔离级别)。
论文给出了锁调度和这个一致性等级的关系:
图中出现的 Read Locks 就是读锁,Wirte Locks 就是写锁。在锁调度的机制下,一致性等级的提升跟写锁、读锁的等级,种类和释放时机有着密切的关系。表中还有一些其他的概念,我们先来看一下几个名词解释:
predicate lock
谓词锁(条件可能更合适,predicate lock): 给定一个搜索条件,对满足这个条件的数据加锁。不仅仅对存在的数据,还包括不存在的数据。
比如select * from t where age > 1 and age < 8;对age在(1,8)范围加锁,加锁之后,其它的事务不能操作(1<age<8)的数据,也不能插入新的满足条件(1<age<8)的数据,比如insert into t(age) values(6);是不允许的。
这类似于MySQL InnoDB的gap lock。
well-formed reads/writes
事务在读写一个数据,或者满足某个条件的一些数据时,先加上对应的读写锁,就称为well-formed reads/writes。一个事务的读写操作都是well-formed那么这个事务就是well-formed transaction。
long-duration/short-duration locking
事务持有某个锁,一直到事务提交或回滚,这个锁就称为long-duration。否则就是short-duration,就是访问完就释放的锁。
Critique 将每一个一致性等级是否能解决异常现象又出了一个表格:
接下来我们详细分析这两张表格的联系。
degree 0 (P0-4均未解决)
Degree 0 在Table2中并没有对读加锁,只对写加锁(Short Duration Write Lock(写完立马释放),这种一致性等级情况下下同时允许 P1(Dirty Read) 和 P0(Dirty Write)异常现象的,由于没有对谓词加锁,因此也会P3异常现象。Degree 0并没有解决任何异常问题
例子:
Txn1 | Txn2 |
---|---|
begin | |
lock(x) | begin |
w(x,2) | lock(x) |
unlock(x) | waiting |
… | w(x,3) |
… | unlock(x) |
commit | |
… | |
commit | |
degree 1 READ UNCOMMITTED(解决P0问题)
这一层,我们起码要解决Degree 0并没有解决的P0问题,之所以出现P0是因为Short Duration Write Lock的存在,即写完就释放,如果我不释放,一直保留这锁,让Tnx2的写入阻塞主,那么就能解决P0级别的问题,因此我们将这个层级起一个别名叫 READ UNCOMMITTED
例子如下:
Txn1 | Txn2 | Tnx3 |
---|---|---|
begin | ||
lock(x) | begin | |
w(x,2) | lock(x) | begin |
waiting | r(x,2) | |
… | waiting | commit |
unlock(x) | waiting | |
commit | w(x,3) | |
unlock(x) | ||
commit |
虽然解决了p0的问题,但是我们的读是不加锁的,只有写才加锁,意味着读取是不会阻塞的,因此,我们看到Tnx3读取到了 Tnx1没有提交的数据x = 2, 因此出现了P1的脏读问题。
degree 2 READ COMMITTED(解决P0、P1问题)
这一层,我们起码要解决Degree 1并没有解决的P1问题,之所以出现P1是因为没有加读锁,到底加类型为 Short 还是 Long的读锁呢?秉着循序渐进的,我们先来上一Short锁,看是否能满足需求,这次我们使用x-lock表示写锁,s-lock表示读锁:
例子如下:
Txn1 | Txn2 | Tnx3 |
---|---|---|
begin | ||
x-lock(x) | begin | |
w(x,2) | x-lock(x) | begin |
waiting | s-lock(x) | |
unlock(x) | waiting | waiting |
commit | w(x,3) | waiting |
unlock(x) | waiting | |
commit | r(x = 3) | |
s-unlock(x) | ||
do sth things | ||
commit |
这样一来,我们就解决了P1的脏读问题。实时标明,Short读锁还是能满足需求的。我们在来看,P1解决了那P2(可重复度)解决了吗? 看下面的例子:
Txn1 | Txn2 | Tnx3 |
---|---|---|
begin | ||
x-lock(x) | ||
w(x,2) | begin | |
… | s-lock(x) | |
x-unlock(x) | waitting | |
commit | r(x,2) | |
s-unlock(x) | ||
begin | … | |
x-lock(x) | … | |
w(x, 3) | … | |
commit | … | |
s-lock(s) | ||
r(x, 3) | ||
s-unlock(x) | ||
commit |
可以看到,Tnx3的数据依旧出现了P2的问题(第一次读取的x和第二次读取的x不相同)。 所以结论 degree 2 READ COMMITTED 并没有解决P2的问题。
Cursor Stability
游标稳定性,如何理解这个级别呢?如果你不了解游标的含义,建议你去理解一下游标的概念。这里我先解决一下P4和P4C的问题,既然是想解决P4C的问题,那我来看一个情景。
Txn1 | Txn2 |
---|---|
begin | |
r(x,1) | |
… | begin |
… | w(x,3) |
w(x, x + 1) | |
… | |
commit(x = 2) |
这里,Tnx2的更新丢失了,因为Txn1在后边有个写的操作。 因此P4的出现是因为一个事务的读写之间又有写的操作,既然如此,我们将Tnx1的r的操作加一个长读锁,避免Tnx1写之前有别的事务写入,不就解决了?但是注意,我们这里还要兼容P0,P1的错误,因此还要给Txn2加上长写锁。
Txn1 | Txn2 |
---|---|
begin | |
s-lock(x) | |
r(x,1) | |
… | begin |
… | x-lock(x) |
… | waitng |
w(x, x + 1) | |
s-unlock(x) | |
commit(x = 2) | |
w(x = 3) | |
x-unlcok(x) | |
commit |
这样看起来可以解决P4的问题,但是似乎有点用力过猛了,因为我的目的是解决(r)+ (w) 的序列,也就是说,我读完之后在根据这个读出来的值进行写入,当我写入成功之后,就可以释放这个读锁了,所以说,没必要Tnx1上加一个严格意义上长读锁。
那么,我们可以将 (r)+(w) 作为一个游标区间,锁住这个区间就好了。如果你不理解,再来看一个例子:
no | Txn1 | Txn2 | Txn3 |
---|---|---|---|
区间1 | begin | ||
区间1 | s-lock(x) | ||
区间1 | r(x, 1) | begin | begin |
区间1 | … | x-lock(y) | x-lock(x) |
区间1 | … | w(y, 2) | waiting |
区间1 | x-lock(x) | waiting | |
区间1 | w(x, x + 1)[x = 2] | waiting | |
区间1 | x-unlock(x) | waiting | |
区间1 | s-unlock(x) | waiting | |
区间2 | s-lock(y) | w(x, 3) | |
区间2 | waiting | x-unlock(y) | x-unlcok(x) |
区间2 | waiting | commit | commit |
区间2 | r(y, 2) | ||
区间2 | x-lock(y) | ||
区间2 | w(y, y + 1)[y = 3] | ||
区间2 | x-unlock(y) | ||
区间2 | s-unlock(x) |
Tnx1想读取x,y 并在每个值的基础上+1后存起来,当读取x的时候,不会对y进行加锁,因此Txn2可以顺利执行完毕。Txn3因为要写x,由于Tnx1在游标内对x加了读锁,因此不能写入,只能等待。
当Txn1写完x之后释放,这个时候Tnx3可以写入。Txn1此时写入y,进入区间2。需要等待Txn2的y的写锁释放。这样一来就优化了调度。因此,通过对某个数据加上区间长读锁来避免其他事务写入从而破坏读完之后在写的一致性约束,我们把这个级别定义为:游标稳定性隔离级别。
但是,这个级别依然没有解决P2的可重复读的问题。试想一下,当Txn1在区间1读取y,然后在区间2 再次读取y,那么区间1和区间2的y也会不相等,原因就在区间1内,Txn2重新写了y的值。P2的问题依旧存在。
结论是,这个级别一定程度上能避免P4C, 但是不一定能避免P4。但意义在于,如果数据库的实现可以通过游标锁来避免P4,那么你就达到了这个级别的隔离。
可以对比理解一下,P4C的避免可以通过游标锁,比如mysql select * for update。在看偏序问题,A5A的序列是Tnx1是连续两次次读,并没有写的操作,因此加锁的时候只会给当前读取的数据加锁,当Tnx1给y加锁的时候,Tnx2已经完成了对y的赋值操作,因此还是会出现A5A的异常现象,读者可以根据这个思路来思考一下A5B的问题。
Repeatabled Read 可重读度
要解决上一个层级的问题,势必需要组织Txn2对于y的写入同时也要阻止其他事务对x的写入,那么只好给x,y都是长读锁了。在长读锁期间,不允许任何属性对x和y进行修改。当然有一点需要说明,我们上面所有的隔离级别,均对谓词的读写没有加长读锁。也就意味着,读取完毕之后,对于满足谓词的区间均是可以插入数据和跟更新的。(可以理解为满足为谓词区间,并没有加间隙锁)也正是如此,以上的级别均没有解决幻读的问题。
这里我们以A5A的问题来看一下,这个隔离级别如何解决这个问题:
Txn1 | Txn2 |
---|---|
begin | |
s-lock(x) | |
r(x,50) | begin |
… | waiting… |
… | waiting… |
… | waiting… |
waiting… | |
s-lock(y) | waiting… |
r(y, 75) | waiting… |
s-unlock(x, y) | waiting… |
commit | x-lock(x) |
x-lock(x) | |
w(x,25) | |
x-lock(y) | |
x-unlock(x,y) | |
commit() |
Snapshot Isolation 快照隔离级别
前文说道,隔离级别的本质是让事务需要在并发能力和串行化效果之间进行平衡。因此上述的级别虽然解决了一下异常现象,但是实现上来说是,效率是及其低下的。因此,想要实现这个平衡,势必要换个思路,加锁的目的是为了防止前后读取不一致,那么允许别人对修改,但对于本事务来说,别人的修改对我来说不可见,那这不就可以达到每次读取的数据一致了吗?因此,多版本的机制就应运而生。
多版本的机制是为了解决一个事务对多个数据的有状态叠加而产生的不一致问题。
对于这个级别来说,事务写操作的同时,不会覆盖原始数据,而是给数据分配一个唯一版本号。而读取事务开始时刻时候会申请一个版本号,事务中的读操作永远不会被阻塞,因为读取的永远是事务开始的版本,这种方式最大的好处在于将读写分离到不同的数据版本中,实现解决冲突的目的。这个级别也是Critique论文补充ANSI-92标准的一个新隔离级别。
Snapshot Isolation是基于多版本事务并发控制的(MVCC)一种事务隔离级别。在 MVCC系统中,每个值在写的时候都会被分配一个新的版本号(Version)。每个事务开启的时间点记为该事务的Start Timestamp,提交时需要获取一个Commit Timestamp,需要比所有正在进行或已完成事务的 Start 和 Commit Timestamp 都大才可以提交成功。
每个事物只能读到在它的 Start Timestamp 之前提交的其他事务的数据版本。事务 T1 能成功提交的前提是:在它的 Start Timestamp 和 Commit Timestamp 这段时间区间内,不存在任何在这期间提交的事务。T2 和 T1 修改了同样的数据。如果发生了这样的情况,事务 T1 应该回滚。这个特性叫 First-Committer-Wins。显然,这个特性可以可以用来避免 P0 (Dirty Write) 和 P4 (Lost Update)。因为事务读的时候只能读到 Start Timestamp 那一刻数据库的快照和当前事务进行过的修改,所以不难分析 Snapshot Isolation 能够避免 P1 (Dirty Read)。
举个例子:
有个数据 A:version= 1 (version 的分配机制为全局分配)
Txn1 | Txn2 |
---|---|
begin (分配一个start_version = 2) | |
read(A (start_version(2) > A的version(1) 允许读取)) | begin (分配一个start_version = 3) |
write(A, A+1) | read(A (3 > A的version(1) 允许读取)) |
其他操作 | write(A, A+1) |
其他操作 | commit(获取新version = 4, 新的version 没有在任何事务的 start_version 和 commit_version 当中可以提交)A的version = 4 |
commit(获取新commit_version = 5, 发现A=verson 在 start_version(2) 和commit_version(5)之间,Txn1失败 |
Snapshot Isolation 和 其他隔离级别的对比
通过上面的分享,我们发现,Snapshot Isolation 是一个比 Degree 0,Read Uncommitted,Read Committed 和 Cursor Stability 更强的隔离级别。但是因为它不能避免避免下面的 H5,所以它比 Serializable 隔离级别弱:
H5: r1[x=50] r1[y=50] r2[x=50] r2[y=50] w1[y=-40] w2[x=-40] c1 c2。
例如:假设 要维护一个约束 x(1) + y(1) > 0
Txn1 | Txn2 |
---|---|
begin (分配一个start_version = 2) | |
read(X (start_version(2) > X的version(1) 允许读取)) | begin (分配一个start_version = 3) |
read(Y (start_version(2) > Y的version(1) 允许读取)) | |
read(X (start_version(3) > X的version(1) 允许读取)) | |
read(Y (start_version(3) > Y的version(1) 允许读取)) | |
write(Y, Y=-40, Y的start_version(2) > Y的version(1), 允许写入并未破坏x + y > 0的规则) | |
write(X, X=-40, X的start_version(3) > X的version(1), 允许写入并未破坏x + y > 0的规则) | |
commit(获取新version = 4, 新的version 没有在任何事务的 start_version 和 commit_version 当中可以提交)Y的version 变成了2 | |
commit(获取新version = 5, 新的version 没有在任何事务的 start_version 和 commit_version 当中可以提交)X的version = 3 | |
破坏约束 x(-40)+ y(-40) > 0 的约束 |
那接下来就是和 Repeatable Read 隔离级别相比了,我们会发现,它和 Repeatable Read 是不可比较的因为有些 Snapshot Isolation 能够避免的异常现象 Repeatable Read 不能避免,同时有一些 Repeatable Read 能够避免的异常现象 Snapshot Isolation 不能避免。例如上述的A5B。
Repeatable Read 能够避免的异常现象 Snapshot Isolation 不能避免的异常是因为 A5A 和 A5B 都存在一个事务修改另一个事务读过的数据的情况,所以如果我们能避免 P2(比如事务 T1 读了 x 后,其他事务都不能再写 x),那 A5A 和 A5B 就都能够避免。
另外还要注意到 Snapshot Isolation 不能够完全避免 P3(幻读)。类似 A5B,事务 T1 根据某个条件读取上来一些数据后,做了修改,事务 T2 也根据同样的条件读取上来同一批数据,但是修改了其他的值,事务 T1 和事务 T2 接下来都能够提交成功。把这样一个多版本系统中的事务历史转换成单版本系统中的事务历史后,会发现这种历史属于 P3 定义的异常行为集合中,因此 Snapshot Isolation 不能完全避免 P3。
Serializable
这一层是真正的串行化方案,针对于上一层级,这里补全了对谓词加长读锁,避免了在事务过程中新增满足谓词的数据插入,从而避免了幻读。
串行化理论概诉
通过上一个章节的分析,我们从第一级别一直到最高级别,讨论了锁调度对事务隔离性、一致性的影响。不能看出,我们从锁的种类,加锁时机,释放时机不断递进,最终导出了可串行化这个级别。
事实上,这些上锁的递进行为是有一套理论支撑的——封锁定理,首先我们来了解几个概念:
封锁协议
第一个就是封锁协议,封锁协议分为三个级别:
-
一级封锁协议:事务T在修改数据R之前必须先对其加X锁,直到事务结束才释放。事务结束包括正常结束(COMMIT)和非正常结束(ROLLBACK)。 一级封锁协议可以防止丢失修改,并保证事务T是可恢复的。使用一级封锁协议可以解决丢失修改问题。在一级封锁协议中,如果仅仅是读数据不对其进行修改,是不需要加锁的,它不能保证可重复读和不读“脏”数据。
-
二级封锁协议:在一级封锁协议之上,事务T在读取数据R之前必须先对其加S锁,读完后方可释放S锁。 二级封锁协议除防止了丢失修改,还可以进一步防止读“脏”数据。但在二级封锁协议中,由于读完数据后即可释放S锁,所以它不能保证可重复读。
-
三级封锁协议 :在一级封锁协议之上,事务T在读取数据R之前必须先对其加S锁,直到事务结束才释放。 三级封锁协议除防止了丢失修改和不读“脏”数据外,还进一步防止了不可重复读。
两段锁协议(two-phase locking protocol, 2PL)
每个事务分为两个阶段:
- 增长阶段:第一个阶段,事务可以获得锁,但不能释放锁
- 缩减阶段:第二个阶段,事务可以释放锁,不能获得锁
两阶段加锁协议可以保证可串行性的前提之一,但不满足可恢复性,因此不能避免级联回滚,不能保证数据一致性。看这个例子:
级联更新
Txn1 | Txn2 |
---|---|
begin | |
x-lock(x) | begin |
r(x) | s-lock(x) |
w(x) | waiting… |
… | waiting… |
r(x) | waiting… |
x-unlock(x) | waiting… |
error! | r(x) |
abort | |
s-unlock(x) | |
… |
2PL锁虽然将事务分为了两个阶段,但是并没有强调事务结束才释放锁,当事务结束之前释放上了锁,那么将会引起级联终止,
级联终止: T1 发生异常后,数据库找出读取了 x 数据的事务,将所有的事务一起终止。这种情况下,一个事务失败,引发级联效应,引起后边一系列事务的回滚。
严格两段锁协议
因此,除了这个问题,两阶段加锁协议在工程上很不容易实现,SQL是千变万化、条数不定的,数据库很难在事务中判定什么是加锁阶段,什么是解锁阶段。于是,将2PL + 三级封锁协议,引出了 S2PL 和 SS2PL的概念。
- 严格两阶段加锁协议(strict 2PL, S2PL):排它锁必须在事务提交后才能释放,避免级联回滚,
- 更加严格意义的2PL还有强两阶段加锁协议(strong 2PL, SS2PL):事务提交之前不释放任何锁。
但值得注意的是,两段锁协议并不要求事务必须一次将所有要使用的数据全部加锁,因此遵守两段锁协议的事务可能发生死锁。
封锁定理
了解两阶段锁协议之后,我们来看一下封锁定理。如果事务是良构的且是两阶段的,那么任何一个合法的调度都是可串行化的.我们看先提条件:良构的事务、两阶段、合法的调度,结论:是可串行化的。
-
如果事务的每个READ、WRITE、 UNLOCK都被响应的锁覆盖,且所有的锁都是在事务结束时释放,那么称这样的事物是良构的(规范的)。
-
如果一个事务可以分成两个阶段,只请求封锁的扩展阶段和只释放锁的收缩阶段,即所有的上锁操作都在解锁前面完成。那么称之为两阶段的事务。(mysql采用两阶段封锁协议)
-
调度是一组事务的操作的某种合并结果,遵守封锁协定的调度称之为合法调度,也就是说,一个调度不应该在一个对象被另外一个事务加上不相容的锁的时候,完成对该对象的加锁。(简单的说就是,如果对A加了共享锁,那么之后,就不会出现对A加排他锁成功的操作。)
一个满足上述封锁协议的调度有很多,数据库产品当中的可串行化隔离级毕竟要落地实现,那么是否有一种简单的规则且易于实现,让我们能快速的判断出这个调度是可串行化的呢?当然,有一种调度方案叫做冲突可串行化(conflict serializable)。
冲突可串行化(conflict serializable)就是这样的条件,按照两个不同的事务对数据库中的同一元素(需要特别注意,这里的元素不等同于一行数据,可能为一个条件范围,也可能是一张表)的操作组合,定义出三种冲突(conflict):
- Read-Write conflict
- Write-Read conflict
- Write-Write conflict
所谓冲突,指的是调度中的一对动作,满足:如果它们的顺序交换,则涉及到的事务中至少有一个的行为会改变。如果调度S通过交换调度中的非冲突动作可以变换为串行调度,这样的调度S称为冲突可串行化调度(conflict serializable schedule)。
冲突可串行化是一个更加容易验证的条件,因此更加适合作为事务并发控制的实现依据。事实上,现在隔离级别中常说的可串行化(serializable),其实是就是指冲突可串行化(conflict serializable)。
总结
可串行化固然会让用户感到安心,但是由于可串行化调度的验证方式往往伴随着大量的阻塞等待(比如2PL),难以达到很高的事务并发执行性能,为了提供更好的并发执行性能,数据库不得不放宽调度的验证,允许更多非可串行化的调度被执行。
显然,多个并发的事务执行结果可能会不再等价于任何一种串行执行的结果,也就是说,事务不再是“隔离”的,事务之间相互产生了影响,导致结果出现了错误。
没错,从隔离的角度来看,这样的事务并发执行结果就是错误的,但却是为了提高性能不得不付出的代价。为了规范用户使用,数据库需要给用户做出保证:什么样的错误会发生,而什么样的错误不会发生,这些不同的保证,就是数据库的隔离级别。
赞赏一下