事务隔离性(Isolation)介绍

一、概念介绍

我们都知道,数据库的事务有ACID这4个需要具备的特性,本文主要介绍I即隔离性(Isolation)。

一个事务,就是一些对数据库的操作(增删改查)的组合,这个操作的组合需要满足ACID四个特性:

  1. A(atomic):原子性,整个事务要么全部完成,要么全部不执行,一般实现是通过在执行过程中发现出错时,将之前的操作回滚。
  2. C(consistency):一致性,意思是事务的执行前后,数据就要满足规定的一些规则,例如主键唯一,或者其他由用户规定的规则。这个特性并不完全由数据库提供,还与事务中的执行的操作有关。
  3. I(isolation):隔离性,一个事务不会被另一个事务影响,最理想的就是等待一个事务执行完成后再执行另一个事务,但处于性能上的考虑,一般都需要事务并发执行,就要求事务执行过程中不受到并行执行的事务的影响,例如不能读取到另一个未提交(提交就是指事务执行完成)事务写入的值。
  4. D(Durability):持久性,指事务一旦提交,它对数据库的改变就应该是永久性的,不应该受到故障或其他原因的影响,例如机器掉电不应该丢失这个事务的数据。

下面详细介绍隔离性,内容主要参考论文A Critique of ANSI SQL Isolation Levels

二、隔离性实现问题

上面说到,最理想的隔离性,就是等待一个事务执行完成后再执行另一个事务,同一个时间只有一个事务再执行,这样是最完美的隔离,这种实现方式一般叫做serial。而实现中,事务的执行是并行的,那么判断实现方式是否存在问题,就是与serial的实现方式进行比较,下面列出各种实现方式中可能出现的问题。

P0. Dirty Write

w1[x]...w2[x]...((c1 or a1) and (c2 or a2) in any order)

先说明一下符号代表的意思:

  • w1[x]表示事务T1写入数据x,同样的w2[x]表示事务T2写入数据x。
  • c1表示事务T1执行commit(提交),a1表示事务abort(回滚)。
  • …表示其他操作。

这个执行顺序的第一个问题是,两个事务执行完之后,可能部分数据是事务T1的,而部分数据是事务T2的。原本按照T1->T2或者T2->T1的顺序执行完,数据库状态应当是满足consistency的,但如果部分数据是T1的,部分数据是T2的,就不一定满足consistency了,例如需要满足x=y,而T1是执行(x=1,y=1),T2执行(x=2,y=2),最终x来自于T1,y来自于T2,那么结果就是(x=1,y=2),不满足x=y。

第二个问题是如果有事物需要abort时,例如w1[x]...w2[x]...a1,如果只有T1在执行,那么此时应该将x的值改回w1[x]执行之前,但由于执行了w2[x],那么这个值是不应该改回去的,因为x这个值已经被T2修改了,不应该是最开始的值了。但不修改也是有问题的,如果T2也需要abort,那么T2也需要回滚x,但是T2记录的原值是T1修改之后的,但是T1已经abort了,那这样abort也是存在问题。

这个问题的解决方法,一般是使用Write Lock,一个事务T1对每个值进行修改之前,对这个值加上Write Lock,其他事务拿不到这个Write Lock,就只能阻塞住,等待T1进行commit或者abort时将锁释放掉。另外,Write Lock可能会造成Dead Lock,这需要一些措施去解决Dead Lock问题,本文就不展开将这个问题了。

另外一种方法是使用快照snapshot的数据库,也能解决这个问题。使用snapshot的数据库,会给事务分配一个开始时间Start_Timestamp,执行完成之后,提交时会分配Commit_Timestamp。
读取数据时,只读取Start_Timestamp之前的最新数据,对于第一个问题,T1和T2的Commit_Timestamp是不同的,那么T1和T2执行完commit之后,用更大的timestamp去读取数据,就只能读取到Commit_Timestamp,都是同一个事务执行完的结果,也就不会有不满足consistency的问题。
第二个问题,需要abort时,其实一般数据是没有存入存储介质的,因为还没有分配commit_timestamp,只是在内存缓存,所以只要丢弃缓存的那些数据即可。

P1. Dirty Read

w1[x]...r2[x]...((c1 or a1) and (c2 or a2) in any order)

这个执行过程的问题,是T2读到了T1未提交的数据。
那么如果T1后面abort了,那么T2就使用了不应该存在的数据。
如果T1后面commit了,也是有问题的,例如T1执行(x1=x0-1, y1=y0+1),保证x+y不变(假设是一个consistency中的约束),而T2读取x时得到了x1,读取y时得到y0,那么是不满足约束的。

这个问题的第一个解决方式,是未进行commit的数据进行缓存,不写入到存储介质,也就不会被其他事务读取到。一般叫做READ COMMITTED。

还有lock方式就是加read lock,read与read可以并行,而如果有write正在进行,则read需要等待write完成;如果read正在进行,则write需要等待read完成。

还有snapshot方式,未提交的数据也是读取不到的(通过时间戳控制),所以就不存在这个问题了。
需要注意的是,单机上时间控制比较简单,当你确定了一个Start_Timestamp,那么未commit的提交,后面获取Commit_Timestamp时,很容易保证Commit_Timestamp大于Start_Timestamp,这样就不会出现后续提交的其他事务被读取到了。
但在分布式场景中,这个就不是那么容易保证了,这个话题比较大,本文就不展开了。

P2. Fuzzy Read(Non-Repeatable Read)

r1[x]...w2[x]...((c1 or a1) and (c2 or a2) any order)

这个问题,典型的例子是一个事务读取两次x,两次读取中间有另一个事务执行了并执行完成,修改了x,那么就会导致这两次读取x的结果不同。这个问题针对的应该是READ COMMITTED,不能解决这个问题。

如果加了READ LOCK,这两个任务是无法并行的,也就没有这个问题。READ LOCK应该一定程度上就是为了解决这个问题而设计的。

而snapshot指定了timestamp,也不会有这个问题。

P3. Phantom

r1[P]...w2[y in P]...(c1 or a1)

这个是广义的phatom(论文中定义),狭义的phatom:

r1[P]...w2[y in P]...c2...r1[P]...c1

这里新增了一个符号P,意思是一个条件,r1[P]就是T1读取满足条件P的内容,例如select count from xxx,调试就是xxx表。w2[y in P]意思就是T2新写入一个满足条件P的y。
对于狭义的phatom,其实就是类似与Nom-Repeatable Read,两次读取同一个内容,读取的过程中被修改了,导致两次读取到的内容不同。

这个问题的解决方法,一种是加gap锁,就是保证搜索出来的条目,它的上一条、下一条不会发生变化,如果条件是>、<或者==,那么gap锁住就会防止满足这个条件的条目发生变化。

使用snapshot也可以解决这个问题,因为指定了Start_Timestamp,新写入的数据并不会被这个事务读取到。但是对于广义的phantom,snapshot是解决不了的,例如:
r1[P]...r2[P]...w1[y in P]...w2[y in P]...c1...c2
再加上一个约束条件(例如r[P]得到的数据的和小于100),T1和T2单独执行都可以保证这个约束条件,但T1和T2同时执行后,就不一定能满足这个条件了。
而如果是gap锁,因为T1和T2是无法同时执行的,也就没有这个问题了。

P4. Lost Update

r1[x]...w2[x]...w1[x]...c1

这个问题是T2修改的数据被T1给覆盖掉了,例如T1执行x=x+1,T2执行x=x+2,那么按照上面这个流程去执行,最值只会得到T1执行后的结果就是x+1,但实际需要执行的应该是x+1+2,两个都要加上。

对于READ COMMITED,是有这个问题的。

对于加LOCK的,这个两个操作无法并行,不会出现这个问题。

而对于snapshot,如果不加修改的话,也是有这个问题的,snapshot本身并不限制这两个事务同时进行。snapshot解决这个问题,有几种方法,一种是事务T1执行commit时,检查在这个事务的Start_Timestamp到Commit_Timestamp之间是否有其他事务进行了commit,且与T1对同一个条目执行了write操作,如果有,则abort。第二种是rocksdb中的merge操作,就是将+1当作一个单独的记录,+2也当作一个单独的记录,那么操作执行完之后就有3条记录:x=10, x+1, x+2,去读取时将三条结合起来就能得到最终的结果。

A5A Read Skew

r1[x]...w2[x]...w2[y]...c2...r1[y]...(c1 or a1)

read skew是P2的一个更复杂的版本,类似的是在T1的执行过程中间,有一个任务T2修改了T1要读取的值,但read skew读取的是两个值x和y。
这个会导致什么问题呢,例如需要满足约束x+y=10,但T1读取到的y是被T2修改过的,而x没有被修改过,那么就会出现x+y不等于10的情况。

如果是加读写锁的方式,在r1[x]之后,拿到的读锁不释放,等待c1或者a1的时候再释放,那么w2[x]是无法执行的,T2会abort,那么就没有问题。

而snapshot的方式,r1[y]的时候,因为指定了Start_Timestamp,读取到的y是T2修改之前的,也没有问题。

A5B Write Skew

r1[x]...r2[y]...w1[y]...w2[x]...(c1 and c2 occur)

write skew也是约束的问题。例如要求x+y<=10,而我们进行这样的操作:
r1[x=1], r2[y=1], w1[y=9], w2[x=9], c1, c2
T1和T2执行时,都根据读取到的值保证x+y<=10,但T1和T2执行完之后,x=9,y=9,不满足x+y<=10。

读写锁的方式,T1和T2是无法并发执行的,所以没有问题。

snapshot不能避免这个问题。

总结:

在这里插入图片描述

在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值