6.824 讲座10:分布式事物
主题:
分布式事务=并发控制+原子提交
有什么问题?
很多数据记录,分片在多个服务器上,很多客户端
[图:客户端,服务器,按密钥分片的数据]
客户端应用程序操作通常涉及多次读取和写入
银行转帐:借方和贷方
对文章进行投票:检查是否已投票,记录投票,增加计数
在社交图中安装双向链接
我们想向应用程序编写者隐藏交错和失败
这是传统的数据库问题
当今的资料源自[分布式]数据库
但是这些想法被用于许多分布式系统
示例情况
x和y是银行结余-数据库表中的记录
x和y在不同的服务器上(可能在不同的银行)
x和y开始为$ 10
客户端C1正在将$ 1从x转移到y
客户C2正在审核,以检查是否没有损失钱
C1:C2:
add(x,1)tmp1 = get(x)
add(y,-1)tmp2 = get(y)
打印tmp1,tmp2
我们希望什么?
x = 11
y = 9
C2打印10,10或11,9
有什么问题吗?
对C1和C2的操作的不愉快交织
例如C2完全在C1的两个操作之间执行,打印11,10
服务器或网络故障
帐户x或y不存在
传统计划:交易
客户告诉交易系统每笔交易的开始和结束
系统安排每笔交易为:
原子的:所有写入都会发生,或者即使发生故障也不会发生
可序列化的:结果就像事务一一执行
持久:提交的写入在崩溃和重新启动后幸免
这些是“ ACID”属性
应用程序依赖这些属性!
我们对分布式交易感兴趣
在多个服务器上分片的数据
本示例的应用程序代码可能如下所示:
T1:
begin_transaction()
加(x,1)
加(y,-1)
end_transaction()
T2:
begin_transaction()
tmp1 = get(x)
tmp2 = get(y)
打印tmp1,tmp2
end_transaction()
如果出现问题,交易可能会“中止”
中止撤消任何记录修改
交易可能会自动中止,例如如果该帐户不存在
系统可能会强制中止,例如打破锁定死锁
一些服务器故障导致中止
应用程序可能会(也可能不会)再次尝试事务
分布式事务有两个主要组成部分:
并发控制
原子提交
首先,并发控制
正确执行并发事务
传统的交易正确性定义是“可序列化”
您执行一些并发事务,这会产生结果
“结果”表示新的记录值,并输出
如果满足以下条件,则结果可序列化:
存在交易的串行执行顺序
产生与实际执行相同的结果
(串行表示一次一次-不能并行执行)
(此定义应提醒您线性化)
您可以通过以下方式测试执行结果是否可序列化
寻找产生相同结果的订单。
在我们的示例中,可能的序列订单是
T1; T2
T2; T1
因此正确的(可序列化的)结果是:
T1; T2:x = 11 y = 9“ 11,9”
T2; T1:x = 11 y = 9“ 10,10”
两者的结果不同;都可以
没有其他结果可以了
该实现可能已经并行执行了T1和T2
但它仍然必须产生结果,就像按顺序排列一样
如果T1的操作完全在T2的两个get()之间运行怎么办?
结果可以序列化吗?
T2将打印10,9
但是10,9并不是两个可序列化结果之一!
如果T2完全在T1的两个add()之间运行怎么办?
T2将打印11,10
但是11,10并不是两个可序列化结果之一!
可序列化性对程序员有好处
它让他们忽略并发
事务的两类并发控制:
悲观:
使用前锁定记录
冲突导致延迟(等待锁)
乐观的:
使用记录而不锁定
提交检查读/写是否可序列化
冲突会导致中止+重试,但如果没有冲突,则比锁定快
称为乐观并发控制(OCC)
今天:悲观的并发控制
下周:乐观的并发控制
“两阶段锁定”是实现可序列化的一种方法
2PL定义:
事务在使用之前必须获得记录的锁
事务必须持有其锁,直到提交或中止之后
以2PL为例
假设T1和T2同时开始
交易系统会根据需要自动获取锁
所以T1 / T2的第一个使用x将获得锁
其他等待
这禁止了不可序列化的交错
细节:
每个数据库记录都有一个锁
如果分发,则锁通常存储在记录的服务器上
[图:客户端,服务器,记录,锁]
(但是两相锁定不受分配的影响很大)
正在执行的事务在第一次使用时会根据需要获取锁
add()和get()隐式获取记录的锁
end_transaction()释放所有锁
所有锁都是排他的(在此讨论中,没有读/写锁)
全名是“强严格两相锁定”
与线程锁定(例如Go’s Mutex)有关,但更容易:
显式的begin / end_transaction
数据库了解正在锁定的内容(记录)
中止的可能性(例如治疗僵局)
为什么要在提交/中止后才保持锁定?
为什么不尽快完成记录发布呢?
结果问题的示例:
假设T2在get(x)之后释放x的锁
然后T1可以在T2的get()之间执行
T2将打印10,9
oops:这不是可序列化的执行:T1; T2或T2; T1都不
结果问题的示例:
假设T1写x,然后释放x的锁
T2读取x并打印
T1然后中止
糟糕:T2使用了一个从未真正存在过的值
我们应该中止T2,这将是“级联中止”;尴尬
2PL会禁止正确(可序列化)执行吗?
是; 例:
T1 T2
得到(x)
得到(x)
放(x,2)
放(x,1)
锁定将禁止这种交错
但结果(x = 1)可序列化(与T2; T1相同)
锁定会产生死锁,例如
T1 T2
get(x)get(y)
get(y)get(x)
系统必须检测(循环?锁定超时?)并中止其中一个事务
问题:描述两相锁定产生的情况
比简单锁定更高的性能。简单锁定:锁定每个
任何使用前记录;中止/提交后释放。
下一主题:分布式事务与失败
分布式事务如何应对失败?
假设在我们的示例中,x和y位于不同的“工作”服务器上
假设x的服务器加1,但是y的崩溃在减去之前就崩溃了?
或x的服务器加1,但是y意识到该帐户不存在?
还是x和y都发挥了作用,但是不确定对方是否做过吗?
我们想要“原子提交”:
一堆计算机正在合作完成某项任务
每台计算机都有不同的角色
要确保原子性:全部执行,或全部不执行
挑战:失败,绩效
我们将开发一个称为“两阶段提交”的协议
分布式数据库用于多服务器事务
我们假设数据库也处于锁定状态
两阶段提交,没有失败:
交易由交易协调员驱动
[时间图:TC,A,B]
TC将put(),get()和&c RPC发送到A,B
修改是临时的,仅在提交时安装。
TC看到transaction_end()
TC向A和B发送PREPARE消息。
如果A(或B)愿意提交,
回答是。
然后A / B处于“准备”状态。
否则,请回答“否”。
如果两者都说是,则TC发送COMMIT消息。
如果任何一个都表示“否”,则TC发送ABORT消息。
如果他们收到COMMIT消息,则进行A / B提交。
即,他们将临时记录写入真实的数据库。
并释放交易记录上的锁。
到目前为止,为什么这是正确的?
除非双方同意,否则A或B都不能承诺。
如果B崩溃并重新启动怎么办?
如果B在崩溃前发送“是”,则B必须记住!
因为A可能已经收到COMMIT并提交了。
因此,即使重新启动后,B也必须能够提交(或不能提交)。
因此,下属必须写入持久(磁盘上)状态:
B必须记住磁盘上的内容,然后再说“是”,包括已修改的数据。
如果B重新启动,则磁盘说是,但没有COMMIT,B必须询问TC,或等待TC重新发送。
同时,B必须继续持有交易的锁。
如果TC表示COMMIT,则B将修改后的数据复制到真实数据。
如果TC崩溃并重新启动怎么办?
如果TC可能在崩溃前发送了COMMIT,则TC必须记住!
因为一个工人可能已经承诺。
如果有人问(例如,A / B是否没有收到消息),请重复该操作。
因此,TC必须在发送COMMIT消息之前将COMMIT写入磁盘。
如果TC从未从B获得YES / NO,该怎么办?
也许B崩溃了并且没有恢复;也许网络坏了。
TC可能会超时并中止(因为尚未发送任何COMMIT消息)。
良好:允许服务器释放锁。
如果B等待TC的PREPARE时超时或崩溃怎么办?
B尚未回应PREPARE,因此TC无法决定提交
因此B可以单方面中止并释放锁
对以后的准备不回应
如果B对“准备”回答“是”,但未收到COMMIT或ABORT怎么办?
B可以单方面决定中止吗?
没有!TC可能两人都同意,
然后将COMMIT发送给A,但是在发送给B之前崩溃了。
因此,A将提交而B将中止:不正确。
B不能单方面承诺:
A可能投了反对票。
因此:如果B投票赞成,则它必须“阻止”:等待TC决定。
两阶段提交观点
当事务使用多个分片上的数据时,在分片数据库中使用
但是它的声誉很差:
缓慢:多轮讯息
慢:磁盘写入
在准备/提交交换上持有锁;阻止其他动作
TC崩溃可能会导致无限期阻塞,并保持锁定状态
因此通常仅在单个小域中使用
例如不在银行之间,不在航空公司之间,不在大范围内
更快的分布式事务是一个活跃的研究领域:
降低消息和持久性成本
可以减少工作量的特殊情况
广域交易
一致性降低,应用程序负担更大
筏式和两阶段提交解决了不同的问题!
使用Raft通过复制获得高可用性
即在某些服务器崩溃时能够运行
服务器都做同样的事情
每个下属做不同的事情时使用2PC
并且所有人必须尽自己的一份力量
2PC对可用性没有帮助
因为所有服务器都必须启动才能完成所有工作
Raft不能确保所有服务器都能做某事
因为只有大多数必须活着
如果您想要高可用性和原子提交怎么办?
这是一个计划。
[图表]
每个“服务器”都应该是Raft复制的服务
TC应该是筏复制的
在复制的服务之间运行两阶段提交
然后,您可以容忍失败,并继续进步
您将在实验室4中构建类似的内容来传输碎片
下次会议的FaRM采用不同的方法