这篇文章上次修改于 210 天前,可能其部分内容已经发生变化,如有疑问可询问作者。
事务
事务十多个数据库操作组合承德一个不可分割的、同时成功或失败的工作单元。
例如:
在银行转账的场景中,涉及到账户的扣款和加款操作,必须保证两者同时成功或同时失败。
事务的特性
ACID四大特性
原子性
- 不可分割,事务单元全部成功或者全部失败。
- 如果在执行的过程中出现故障,就需要撤销事务内已经完成的部分语句,使整个事务都处于未执行。
例如:
银行转账过程中,A用户转钱给B用户,B用户增加金额,A用户扣除金额。如果A用户金钱不够,就直接ROLLBACK,把B用户增加的金额还原。
持久性
一旦事务完成提交,即使数据库发生故障,该事务的执行结果也不会丢失,可以被正确地恢复,仍然对后续事务可见。
例如:
转账完成后,事务执行结果“转账成功”已返回,这个事务完成会永久生效,不论之后故障,已经完成的结果将保存。
一致性
事务的执行会改变系统状态,一致性是指事务的执行会保证数据库从一个正确(一致)的状态转移到另一个正确的状态
例如:
负数金额是不正确的数据库状态,要确保事务执行后数据库的状态依旧正确。或者设定转账的两人总金额不变作为正确性。
隔离性
互不干扰,多个事务并发执行的情况下,能确保和串行执行的结果一致,不冲突。(并行执行确保了效率)
如果两个事务之间没有关联性,则两个事务可以并发执行。
- 写读冲突:脏读
事务A:小A账户+100,小B账户-100
事务B:查询小A账户金额
如果两个事务并行,在事务A添加100元,小B账户-100发现余额不足,则要回滚。如果事务B发生在回滚之前,那么读取到的就是+100之后的余额,然而这个余额是不真实的,读取了未提交的数据,也就是读取了脏页数据。我们称作脏读。
- 读写冲突:不可重复读
事务A:小A查询余额 —— 小A查询余额
事务B:小A余额+100,小B余额-100
如果并行,事务A两次读取余额事务,并且数据不一致
也就是事务B导致了其他事务不可重复读取
- 写写冲突:导致脏写
事务A:读取小A余额,数值+100,更新新数值到余额
事务B:读取小A余额,数值 -100,更新数值到余额
社会小A初始余额100,如果并行,同时读取到一开始的100,事务A尝试写入200,事务B尝试写入0.
以上问题使用并发控制解决。
- 幻读
由于插入删除导致的多次读取不一致。
可串行化调度
由于保证隔离性,需要使用调度对遇上的冲突进行避免。
- 串行调度:是事务按照一定的顺序,完成一个后再去进行下一个。不会出现以上的冲突。
- 可串行化调度:事务同时进行,但对事务内的操作有一定的调度,以确保不出现冲突。使得调度后的结果和串行调度的结果一致。
如何判断一个调度是否可串行化?如何实现可串行化调度? 验证代价较大,一般找一个容易验证的可串行化调度方案
- 终态可串行化:判读最终执行结果的状态
- 冲突可串行化:判断操作之间是否冲突
- 视图可串行化:判断操作流程是否一致 – 初始化读的一致性 – 读写顺序的一致性 – 最终写的一致性
包含关系:
调度⊇可串行化调度⊇视图可串行化调度⊇冲突可串行化调度⊇串行化调度
冲突可串行化调度
从冲突的角度,针对一个调度去发现其等价的串行调度来确定该调度为可串行化调度。
利用了操作交换为基础。
操作交换:交换事务调度序列中相邻的两个操作,也就是变化了一个调度。
等价交换:交换不会影响两个调度的一致性时,被等价交换操作前后的两个调度等价。
等价操作
- 交换连续两个相同数据读取操作的顺序:RT1(A) RT2(A)
- 交换连续两个不同数据读写操作的顺序:RT1(A) WT2(B);WT1(A) RT2(B);WT1(A) WT2(B)
非等价操作
- 交换连续两个相同数据的读写、写写操作: RT1(A) WT2(A);WT1(A) RT2(A);WT1(A) WT2(A)
根据不断地等价交换操作,对于目标调度,是否能变成一个串行调度,如果可以,则目标调度是一个可串行化调度,如果不行,则该调度不是可串行化调度。
我们称S是串行调度,S‘ 是有冲突的调度。
根据规律总结结论:调度S’中Ti,Tj 的某两个操作存在冲突(比如读写、写写冲突),且Ti的冲突操作在Tj冲突操作之前时。若调度S’存在冲突等价的串行调度S,则在串行调度S 中Ti一定在Tj之前
简化验证手段:
利用以上结论,当找到冲突时(读写,写写,写读)且Ti 的冲突操作在Tj 冲突操作之前,则Ti向Tj 连一条有向边(Ti,Tj)。这个图称作优先图。
冲突可串行化充要条件:优先图无环
易得:若有拓扑序列C,则C为其等价的串行调度。
冲突可串行化调度是可串行化调度的一种,是易于验证和实现的可串行化调度。
视图可串行化调度
根据包含关系,冲突可串化调度一定是视图可串化调度。
也就是说,存在有调度通过冲突可串化调度判断为否,但实际上,该调度为可串化调度。
视图可串行化调度从视图等价的定义出发,针对一个调度S’,通过等价转换找到等价的串行调度S。
满足三个条件的两个调度视图等价:(对任意的数据库记录X)
- 若调度S’中事务Ti读取了X的初始值,则调度S 中Ti 也需要读取X的初始值。
- 若调度S’中事务Ti读取了Tj更新的X值,则调度S 中Ti也需要读取Tj更新的X值
- . 若调度S’中事务Ti最后写入了X的值,则调度S 中Ti也要最后写入了X的值。
对于是视图可串行化调度但不是冲突可串行化的例子,大多源自于盲写。
如果一个事务不读只写,我们称作盲写。
从冲突可串行化的角度上来看,盲写具有冲突意义,然而从意义上考虑,盲写并不存在写写冲突。
视图可串行化调度验证方法:
使用了一种带标记的优先图。
为目标调度S添加一个起始只写事务T0,和一个终止只读事务Tf。
建边条件:若调度S中事务Ti读取了事务Tj写入的值,则从Tj构建指向Ti的有向边,标记为0。
建图步骤:
- 对每个数据项A,枚举调度S中非T0的所有写操作,枚举到的写操作属于的事务用Tk表示。对于事务Tk的写操作write(A),若Ti读取了Tj写入A的值,且k,i,j 都不相等时,增加带标记的边。
- Ti=Tf时,由于Tf是终止事务,Tk必须在Tj之前 ,因此Tk引出一条标记为0的边指向Tj
- Tj=T0时,由于T0是起始事务,Tk必须在Ti之后,因此Ti引出一条标记为0的边指向Tk
- 当Ti不等于Tf且Tj不等于T0时,则Tk可以在Tj之前也可以在Ti之后 。从之前没有选择过的标号中,选择一个标号x,从Tk连出标记为x 的有向边指向Tj,并从Ti连出标记为x的有向边指向Tk。
验证步骤:
- 每种大于0的标记都有两条边,代表两种可能 • 写事务在冲突的写读操作之前 • 写事务在冲突的写读操作之后
- 枚举大于0的标记的边的两种可能性转换为普通优先图 • 每次优先图判定为O(M),M为边的数量 • 若有N种标记,复杂度为O(2N*M)
- 只要存在一种优先图无环,则视图可串行化
可恢复调度
为了保证调度的ACID中的隔离性,当数据库发生故障需要回滚的时候,数据库能高效的恢复事务数据。
对于调度来说,从可恢复角度来看,可分为:
- 可恢复调度:可恢复,例如串化事务。
- 不可恢复调度:在T1先操作后,T2再操作并且commit,此时,T1再想abort是不可能的了,因为T1的操作被T2commit了,不能回滚
- 级联回滚:在T1操作后,T2操作,T3操作,T1 abort,可以回滚,把T1,T2,T3的所有操作都回滚了,但是T2和T3需要重新操作一遍,浪费资源,降低效率。
总结以上可以发现,不可恢复调度读写了未提交事务修改的数据。
也可以发现,可恢复调度要求T1未提交时,读写了T1数据的T2不应该在T1之前提交。
已知级联回滚会导致效率低下,我们应该尽可能去减少级联回滚。
无级联回滚调度
当S中任意一个Tj读取Ti修改的数据X时,Ti都已经进行了提交,则调度S是无级联的(无级联调度都是可恢复调度)
事务的隔离级别
可串行化调度影响事务并发的效率,如果可串行化过于严格,有时还不如串行化调度。
先复习一下我们遇到的异常现象:
- 脏读:在执行过程中读到其他尚未提交的写事务的修改数据
- 不可重复读:指同一个事务先后两次读到的同一条记录的内容发生了变 化。(被其他写事务修改)
- 幻读:指同一个事务内先后两次读到的数据不同,即两次分别读到其他 并发写事务插入或者删除前后的数据。例如,统计银行总款两次,中间增加了一个新储户。
因此为了平衡可串行化严格度,建立了四种隔离级别:
- 读未提交:允许(脏读, 不可重复读,幻读)这三者。只有当业务场景中不同的事务之间没有相互影响时,才会考虑该隔离级别。
- 读已提交:允许(不可重复读,幻读)这两者。要求事务只能读取到已经提交的值,忽略其他事务未提交的值。(通过MVCC,锁和时间戳等技术)
- 可重复读:允许(幻读)这一者。
- 可串行化:都不允许,最严格的级别。
以上四种隔离级别从上往下,数据安全越来越强,并发程度越来越弱。
技术解决:
- 读未提交:一般不允许该级别
- 读已提交:对数据项加锁,可以立即释放读锁
- 可重复读:对数据项加锁,获取同一个时间戳或者缓存读取的数据
- 可串行化:两阶段锁,对整张表加锁 或者 加谓词锁