为什么需要事物?
数据库软件随时会失效
应用程序随时会崩溃
应用程序与数据库、数据库之间的连接随时会中断
多个客户端同时写入数据库引发冲突
客户端可能读到无意义、部分更新的数据
客户端之间由于边界条件竞争引入各种奇怪的问题
事物
ACID
ACID | |
---|---|
原子性(A) | 只处于操作之前或操作之后的状态,不存在中间状态 |
一致性(C) | 数据处于所期待的预期状态,如首付款的平衡等。该属性其实不是数据库能够保证的,应该由应用层保证 |
隔离性(I) | 并发执行的多个事物相互隔离,不能互相交叉互相影响 |
持久性(D) | 事物一旦提交成功,即使存在硬件故障或数据库崩溃,事物写入的数据也不会消失 |
单对象事物和多对象事物
单对象事物:只对单个对象操作,单对象事物如日志恢复提供原子性、对象加锁实现隔离性、高级原子操作(自增自减等)以及CAS等,通常意义上的事物指多对象操作
多对象事物:一个事物对多个对象进行操作,如外键更新、二级索引更新等操作
弱隔离级别
串行化使事物最终执行结果与串行一次一个无并发情况下的结果保持一致,但会严重影响性能,因此提供了弱隔离级别,很多流行的数据库也是采用了弱隔离级别
读-提交(脏读脏写)
- 读时只能看到已经成功提交的数据(防止脏读)
- 写时只会覆盖已经成功提交的数据(防止脏写)
解决办法:
使用行级锁避免脏写,事物必须等到前一个拿到锁的事物提交或终止后才能获取锁并更新数据
由于行级锁开销大所以不适用防止脏读,数据库会保持旧值与新值的方法防止脏读,事物提交之前所有操作均会读取旧值,提交成功后才会读取到新值
快照级别隔离(可重复读)
读提交在事物提交后会读取到新数据,并发时A事物的两次读取操作中间B操作完成了更新操作事物,会导致A事物两次读取的值不同,造成不可重复读。
解决办法:
使用快照级别隔离,类似都提交中新旧值,但将保留更多的版本,数据会同时存在多个不同版本的数据,每个事物的生命周期内只会读取同一版本的数据(MVCC,多版本并发控制)
防止更新丢失
读提交和快照隔离针对的是只读事务在面对并发写时会看到什么,而在两个写事物并发时,会存在其他问题,最著名的便是更新丢失
应用程序从数据库读取某个值,判断计算后写会新值,由于隔离性,两个事物对对方互相透明,当并发时,其中一个写数据可能会被覆盖从而造成丢失
解决办法:
- 原子写操作
如果支持通常是最好的方案,造成更新丢失的根本在于应用层的读-修改-写
操作上,如果能避免这种非原子性操作无疑最好。一般原子写操作的实现方式有独占锁和单线程执行等方法 - 显示加锁
通过加锁进行排他性操作可以解决 - 自动检测更新丢失
先并发执行,事务管理器检测到了更新丢失风险,会终止当前事务,强制回退到安全的读-修改-写
操作上 - CAS
持有旧值,只有当满足时才会更新,但可能有ABA问题发生
写倾斜和幻读
写倾斜是啥?举个例子
医院规定至少要有一个医生值班,此时A和B医生都在值班,但两人都想请假
A:查询系统->两人在班->条件满足->请假成功->A下线
B: 查询系统->两人在班->条件满足->请假成功->B下线
结果:A和B均下线,没有医生在值班了,与约束条件至少有一个在值班冲突
如果A和B的操作串行化操作,第二个医生的操作肯定会被拒绝,写倾斜作用在两个不同的对象上,最终的结果与某个约束条件造成冲突,正如你可以加锁让两个人无法移动同一枚棋子,但锁无法阻止两个棋子落在同一个位置上,这需要其他约束。即更新不同的对象可能造成写倾斜,更新同一个对象可能发生脏写或更新丢失。
写倾斜的防止无法应用但对象的原子操作,需要完全的串行化或其他外在约束一起限制。
对于医生值班的例子可以通过加锁来保证事物安全,但对于其他例子,它们通过都来检查满足或不满足的条件,其预期结果为空,因此无法使用锁来对不存在的记录进行加锁
写倾斜的产生在于读-决定-修改-写
的状态,写入(增、改、删)需要根据读的内容作为前提条件,而写后的值又会改变这个前提条件。即写入后再查询,会返回不同的结果。
这种由于在一个事物的写入改变了另一个事物查询结果的现象,称为幻读(我刚刚发生了幻觉看错了,导致我做错了事)
解决方法:
串行化:最强隔离级别,保证事物并行执行但结果与一个一个执行的结果相同,但开销太大,实际一般采用以下几种
- 存储过程
- 两阶段加锁
- 乐观并发控制
存储过程
使用存储过程封装事物,将交互式事物中的人为交互去掉,与内存式数据存储结合使得单线程执行所有事物变得可行;但存储过程语义丑陋,难以调试,规范不同且性能影响大
对于跨分区事物,存储过程需要跨越所有分区加锁保证系统的可串行化
两阶段加锁(2PL)
多个事物可以同时读取同一对象(共享读锁),但只要出现任何写操作,必须加独占锁,此时所有其他事物必须等独占锁释放后才能继续进行,即获取-释放两个阶段的锁操作,叫两阶段加锁
谓词锁:作用于某些搜索条件的所有查询对象,能够保护数据库中那些上不存在但可能马上会被插入的对象
两阶段加锁和谓词锁结合可以做到防止任何形式的写倾斜以及其他竞争条件,使隔离变得真正可串行化
谓词锁和两阶段加锁性能都不佳,因此大多数2PL实现的是索引区间锁
索引区间锁: 扩大保护对象范围,如要锁1月15日这一天,可以锁住整个一月,由于是包含关系,所以一定安全,当其他事物更新数据后也会对索引进行更新,此时由于索引被加锁就会处于等待状态
可串行化的快照隔离(SSI)
一种乐观并发控制,可能发生冲突仍继续执行,确实发生冲突时会中止重试
在之前的写倾斜中可以知道,查询结果与写入之间可能存在因果关系,因此为了提供可串行化的隔离,数据库必须检测事物是否会修改其他事物的查询结果,若更改了则中止写事物。
- 检测是否读取了过期的MVCC对象(读取之前已经有未提交的写入)
- 检测写是否影响即将完成的读取(读取之后,又有新的写入)
SSI与2PL相比,事物不需要等待其他事物所持有的锁,读写不会互相堵塞,从而使查询延迟低,更稳定;且可以突破单个CPU核的限制,跨分区跨主机分布,事物可以在多个分区上读写并保证可串行化的隔离
分布式事务的影响条件
不可靠的网络
网络会带来可能无限期的延迟、丢包以及乱序等故障,甚至网络切断
不可靠的时钟
由于石英晶体在物理上的差异,分布式系统中不同机器上的时钟无法保持完全同步,即使通过NTP时间协议也由于不可靠的网络而无法保证时间同步,对时间敏感和高依赖的系统可能会产生灾难性后果
进程暂停
- GC回收造成的stop the world
- 虚拟环境中的暂停虚拟机
- 电脑休眠
- 操作系统上下文切换
- 同步IO操作
- 内存交换中断
等均可能会造成进程的暂停,从而会导致响应时间无法保证,造成恶劣后果。
如A拿到了一个为期30秒的超时锁,此时进程暂停了一分钟,锁已过期,B拿到了新的锁进行操作,此时A恢复但并不知道自己的锁已经过期了,仍去操作,造成冲突。(解决方法可以使用fencing令牌,拿到锁时会得到一个单调递增的id,当数据库处理过更高序列的id时会对低序列id拒绝操作)
一致性与共识
可线性化
概念:让一个系统看起来好像只有一个数据副本,且所有的操作都是原子的
要求:就近性保证,一旦新值被写入或读取,所有后续的读看到的都是最新的值,直到再次被覆盖
ps:可串行化关注的是多个对象多个操作任意顺序之间的隔离性,重点在隔离
可线性化是单个对象单个操作对读写寄存器的最新值的保证,重点在最新值
使用场景
可线性化场景 | |
---|---|
加锁与主节点选取 | 所有节点都必须同意哪个节点持有锁 |
约束与唯一性保证 | 数据库中的唯一性的约束 |
跨通道的时间依赖 | 存在多个通信通道,如果没有线性化保证,不同通道可能产生竞争(抵达顺序乱掉) |
CAP理论
一致性,可用性,分区容错性,系统只能支持其中两个特性,网络故障是无法避免的问题,所以CAP看看就行
顺序保证
因果关系
因果关系会对所发生的事情施加某种排序,有因才有果,系统满足因果顺序,称为因果一致性
因果排序并非全序(支持任意两个元素比较),而是偏序(正如Git上的分支之间无法比较,但分支会对主干起到因果关系)
可线性化一定保证因果关系,因果一致性可以认为是不会由于网络延迟而显著影响性能,有能对网络故障提供容错的最强一致性模型
因果关系只需保证所有因果因果在前的请求都已完成即可
序列号排序
指定序列号,按照序列号顺序执行,如@Order
或过滤器的Order
次序
序列号的产生需要满足因果性,可以使用主节点进行分配,若不存在主节点,可以使用Lamport时间戳(由唯一节点ID,时间戳,计数器组成)
光有时间戳还不够,因为在并发获胜选举时,比较两个的时间戳,先来的获胜,需要收集了所有请求消息后才能做判断,系统要收集所有请求就必须检查每个节点,如果遇到节点或网络故障无法连接,该方法就无法正常运转
因此要确定全序关系何时确定,需要全序关系广播
全序关系广播
主节点确定全续关系并广播给所有节点,需要满足以下两点:
- 没有消息丢失,如果消息发送到了某一个节点,则它一定要发送到所有节点
- 消息总是以相同的顺序发给每个节点
主节点能力有限,突破单一节点的限制以及主节点失效时的故障切换是主要挑战
可将全序关系广播理解为日志,传递消息就像追加更新日志。
全序广播与线性化存储
全序广播实现线性化存储
全序广播基于异步模型,保证消息以固定顺序可靠的发送,但不保证何时发送成功(可能乱序到达但消息本身有固定顺序);
可线性化强调就近性:读取时保证能够看到最新写入值
线性化存储实现全序广播
每个要通过全序关系广播的消息,原子递增并读取该线性化的计数,然后将其作为序列号附加到消息中,接下来将消息广播到所有节点,接受者也严格按照序列化来发送回复消息
两阶段提交
引入协调者,具体步骤为:
- 协调者发送准备请求到所有节点
- 所有参与者回答是(承诺会提交事务,无论发生任何情况,无法反悔)
- 协调者发送提交请求(不可反悔)
- 所有节点完成提交
- 存在参与者回答否
- 向所有节点发送放弃请求(不可反悔)
该方案依赖协调者,如果协调者出现问题,节点无法独自做出判断下一步是提交还是放弃,只能等待协调者恢复
三阶段提交
- 引入超时机制。同时在协调者和参与者中都引入超时机制。
- 在第一阶段和第二阶段中插入一个准备阶段。保证了在最后提交阶段之前各参与节点的状态是一致的。
详细内容可见网友博文
共识算法
分布式一致性问题的根本——多个节点达成共识
有名的共识算法有:VSR,Paxos,Raft,Zab等,他们均决定了一系列值,然后采用全序广播算法
在每一轮,节点提出他们接下来下发送的消息,然后决定下一个消息的全局顺序,所以全序广播相当于持续的多轮共识,每一轮共识的决定对应于一条消息
- 由于协商一致性,所有节点决定以相同的顺序发送相同的消息
- 由于诚实性,消息不能重复
- 由于合法性,消息不会被破坏,也不是凭空捏造的
- 由于可终止性,消息不会丢失