聊聊分布式事务

一、前言

为了更加系统、全面、完整的学习事务,也为了完善一下自己的知识框架。以本篇博客为契机,展开对事务的学习。

本篇内容包含

  • 数据库事务概念、特性
  • 分布式事务概念、特性
  • 分布式事务常见解决方案:二阶段提交、三阶段提交

二、数据库事务

1 何为数据库事务

事务(Transaction),一般是指要做的或者所做的事情。在计算机术语中,是指访问并可能更新数据库中各种数据项的一个程序执行单元(unit)。在计算机术语中,事务通常就是指数据库事务。

2 特性

一个数据库事务通常包含对数据库进行读或者写的操作序列。它的存在主要有以下两个目的:

1 为数据库操作提供一个从失败中恢复到正常的状态的方法,同时提供了数据库即使在异常状态下仍能保持一致性方法
2 当多个应用程序并发访问数据库时,并对数据库进行写操作,可以在这些应用程序之间提供一个隔离方法,以防止彼此的操作相互干扰。

当一个事务被提交给DBMS(数据库管理系统),则DBMS要确保事务执行完毕之后,事务中所有操作都被成功执行,而且这些操作产生的变更结果都要被永久保存在数据库中。倘若事务中有一个操作失败了,则整个事务中所有的操作,都要被回滚(rollback),即回归到事务执行之前的操作(要么全执行,要么全都不执行,怎么样,是不是跟Java里的关键字Synchronize的定义很像)。同时,该事务对数据库的其他事务执行是不影响的,好像每个事务只做好自己的 本职工作,对于其他事务都是无感知的。

所以,从上述概念中,可以总结出,数据库事务主要包含以下四个特性:原子性、一致性、隔离性以及持久性。

原子性(Atomicity):事务作为一个整体被执行,包含在其中的对数据库的操作,要么全部执行,要么全都不执行。
一致性(Consistency):事务应该确保数据库从一个一致性的状态到另一个一致性的状态。一致性的含义是数据库中的数据应当满足完整性约束。
隔离性(Isolation):多个事务并发执行时,一个事务的执行不会影响另一个事务的执行。
持久性(Durability):一个事务一旦提交,他对数据库的修改就应该永久保留在数据库中。

3 举例理解

说了这么多,让我们来举个日常生活中常见的例子,方便理解。比如AB转账问题。

  1. 从A账户中读取余额(500)
  2. 对A账户做减法操作(500-100)
  3. 将结果写回至A账户(400)
  4. 从B账户中把余额读取出来(500)
  5. 对B账户做加法(500 + 100 )
  6. 把结果写回B账户(600)

原子性
保证1~6操作,要么都执行,要么都不执行,一旦执行某一步骤的过程中发生问题,就需要执行回滚操作。比如执行到第二步时,突然遇到系统不可用,导致减法操作失败,那么数据库中A账户的余额仍然是500,而B账户却变成了600。所以原子性就保证了,当第二步失败时,之前所有操作都要回滚,即转账操作失败。

一致性
在转账之前,A和B的账户中共有500+500=1000元钱。在转账之后,A和B的账户中共有400+600=1000元。也就是说,数据的状态在执行该事务操作之后从一个状态改变到了另外一个状态。同时一致性还能保证账户余额不会变成负数等。

隔离性
在A向B转账的整个过程中,只要事务还没有提交(commit),查询A账户和B账户的时候,两个账户里面的钱的数量都不会有变化。
如果在A给B转账的同时,有另外一个事务执行了C给B转账的操作,那么当两个事务都结束的时候,B账户里面的钱应该是A转给B的钱加上C转给B的钱再加上自己原有的钱。

持久性
一旦转账成功(事务提交),两个账户的里面的钱就会真的发生变化(会把数据写入数据库做持久化保存)!

这里补充一点:原子性和一致性

一致性与原子性是密切相关的,原子性的破坏可能导致数据库的不一致,数据的一致性问题并不都和原子性有关。
比如刚刚的例子,在第五步的时候,对B账户做加法时只加了50元。那么该过程可以符合原子性,但是数据的一致性就出现了问题。

因此,事务的原子性与一致性缺一不可。

三、分布式事务

在介绍完数据库事务之后,结合具体的工作来看,其实单纯的数据库事务,只能保证单机事务,却不能在分布式环境中保证整体事务的一致性。但是为什么需要分布式事务?

随着大型网站的各种高并发访问,海量数据处理场景越来越多,如何实现网站的高可用、易伸缩、可扩展、安全等目标越来越重要。为了解决这样一系列问题,大型网站的架构也在不断发展。提高大型网站的高可用架构,不得不提的就是分布式。

1 何为分布式事务

分布式事务是指会涉及到操作多个数据库的事务。其实就是将对单一库事务的概念扩大到了对多个库的事务。目的是为了保证分布式系统中的数据一致性。分布式事务处理的关键就是必须有一种方法可以知道事务在任何地方所做的所有动作,提交或者回滚事务的决定必须产生同一结果(全部提交或者全部回滚)

在分布式系统中,各个节点之间在物理空间上,相互独立并通过网络进行沟通和协调。所以单从某个节点上来看,其实可以通过事务机制来保证单节点数据的一致性,即单机节点的数据操作满足ACID特性。但是从分布式系统上看,仅保证各个节点自身的数据一致性还是不够的,必须从全局角度上(所有节点)保证数据的一致性,即要么全部节点的数据进行写操作,要么全部都回滚不执行。

而分布式事务,要解决的问题,就是一台机器在执行完自己的本地事务之后,无法感知到其他机器节点的事务执行情况,从而导致该机器无法确定是否commit 或者 rollback。

所以,常用的解决办法(其实也是大多是分布式解决方案常见的解决办法),就是引入一个协调者,用来统一调度所有分布式节点执行事务。

2 XA规范

X/Open组织(Open Group)定义了分布式事务处理模型。X/Open DTP模型(1994)包括应用程序(AP,Application)、事务管理器(TM,Transaction Manger)、资源管理器(RM,Resource Manager)、通信资源管理器(CRM,Communication Resource Manager)四部分。

一般,常见的事务管理器(TM)会选择使用交易中间件,常见的资源管理器(RM)是数据库,而常见的通信资源管理器(CRM)则是消息中间件。

通常把一个数据库内部的事务处理,比如多表操作视作为一个本地事务(单机事务)来看待,数据库的事务处理对象即为本地事务,而分布式事务处理的对象则是全局事务

所谓的全局事务,就是指分布式事务处理环境中,多个数据库可能需要共同完成一个工作,这个工作即为全局事务。

例如,一个事务中可能更新几个不同的数据库。对数据库的操作发生在系统的各处但必须全部被提交或回滚。此时一个数据库对自己内部所做操作的提交不仅依赖本身操作是否成功,还要依赖与全局事务相关的其它数据库的操作是否成功,如果任一数据库的任一操作失败,则参与此事务的所有数据库所做的所有操作都必须回滚。

一般情况下,某一数据库无法知道其它数据库在做什么,因此,在一个 DTP 环境中,交易中间件是必需的,由它通知和协调相关数据库的提交或回滚。而一个数据库只将其自己所做的操作(可恢复)影射到全局事务中。

XA 就是 X/Open DTP 定义的交易中间件与数据库之间的接口规范(即接口函数),交易中间件用它来通知数据库事务的开始、结束以及提交、回滚等。 XA 接口函数由数据库厂商提供。

3 举例理解

这里想稍微解释一下,为什么事务管理器(TM)通常会选择交易中间件。

其实从业务角度上说,交易是整个电商链路中最为核心的环节,其核心就是订单的正向履约(用户从正常下单到库存发货的一系列流程)以及订单逆向操作(退款退货等)。

正向:即从用户下单开始,需要依次扣商品库存、用户津贴、优惠券、红包,然后到发货、用户签收,直至订单完成。

逆向:即用户要求退货退款,这里涉及到归还商品库存、归还用户津贴、红包、优惠券等。

所以,再整个分布式系统中,可能各个扣减操作会发生在不同的应用服务中,比如商品扣减库存,在商品域服务中完成,而津贴、红包等等则可能在营销域服务中完成,所以,此时交易中间件就充当了事务管理器这一角色,进行全局事务的管控。

四、分布式事务解决方案

在介绍完分布式事务概念以及其必要性之后,还还需要介绍当前分布式方事务常见的解决方案。主要包括二阶提交协议(2PC)、三阶提交协议(3PC)以及TCC。

1 二阶提交协议(2PC)

二阶段提交其实就是实现XA分布式事务的关键,两阶段的提交方式保证了事务的原子性,要么全做要么全不做。

二阶段提交(Two-phaseCommit)是指,在计算机网络以及数据库领域内,为了使基于分布式系统架构下的所有节点在进行事务提交时保持一致性而设计的一种算法。通常,二阶段提交也被成为一种协议(Protocol)。在分布式系统中,每个节点虽然可以知晓自己的操作成功或者失败,却无法知道其他节点的操作的成功失败与否。当一个事务跨越多个节点时,为了保持事务的ACID特性,需要引入一个座位协调者的组件来统一掌控所有节点(称之为参与者)的操作结果并最终指示这些节点是否需要把操作结果进行真正的提交。因此,二阶段提交的算法思路可以概括为:参与者将操作成败通知协调者,再有协调者根据所有参与者的反馈情报决定参与者是否需要提交操作还是中止操作。

所谓的两个阶段:第一阶段:准备阶段(投票阶段)第二阶段:提交阶段(执行阶段)

准备阶段
事务协调者(事务管理器)给每个参与者(资源管理器)发送Prepare消息,每个参与者要么直接返回失败(如权限验证失败),要么在本地执行事务,写本地的redo和undo日志,但不提交,到达一种“万事俱备,只欠东风”的状态。

可以进一步将准备阶段分为以下三个步骤:

  • 1)协调者节点向所有参与者节点询问是否可以执行提交操作(vote),并开始等待各参与者节点的响应。
  • 2)参与者节点执行询问发起为止的所有事务操作,并将Undo信息和Redo信息写入日志。(注意:若成功这里其实每个参与者已经执行了事务操作)
  • 3)各参与者节点响应协调者节点发起的询问。如果参与者节点的事务操作实际执行成功,则它返回一个”同意”消息;如果参与者节点的事务操作实际执行失败,则它返回一个”中止”消息。

提交阶段
如果协调者收到了参与者的失败消息或者超时,则直接给每个参与者发送回滚(rollback)消息;否则,发送提交(commit)消息;参与者根据协调者的指令执行提交或者回滚操作,释放所有事务处理过程中使用的锁资源,而且必须是在最后阶段才释放锁资源。

具体的提交过程如下所示:

  1. 协调者向所有参与者节点发出“commit”请求
  2. 参与者节点正式完成操作,并释放整个事务期间所占用资源
  3. 参与者节点向协调者节点发送“完成”消息
  4. 协调者节点收到所有参与者节点的“完成”反馈后,完成全局事务

若任何参与者在协调者发出commit请求之后,返回失败消息,或者返回消息超时,那协调者将向所有参与者发出回滚指令,对事务操作进行回滚。
在这里插入图片描述

  1. 协调者节点向所有参与者节点发出“回滚操作”的请求
  2. 参与者节点利用之前写入的Undo信息执行回滚,同时释放事务期间所占用的资源
  3. 参与者节点向协调者节点发送“回滚完成”的消息
  4. 协调者节点收到所有参与者节点反馈的“回滚完成”消息后,取消事务。

无论最后事务是否执行成功,第二阶段都会结束当前事务。

二阶段提交看起来确实能够提供原子性的操作,但是不幸的是,二阶段提交依然存在以下缺陷:

1 单点故障:由于整个二阶段提交解决方案的核心在于协调者,一旦协调者发生故障,那么参与者会一直处于阻塞状态,事务执行期间所占用的资源也不能及时释放,从而无法完成事务操作。尤其是在第二阶段,当所有参与者向协调者发送操作完成消息之后,却得不到协调者反馈,那么参与者将一直占用事务资源,且无法完成事务提交操作。(如果协调者挂了,可以选举一个新的协调者,但是新的协调者依然无法感知到因原协调者宕机而处于阻塞状态的参与者)

2 同步阻塞问题。二阶段提交执行过程中,所有参与者节点都是事务阻塞型的,换句话说,从参与者接受到协调者执行消息到执行完毕之后,返回给协调者消息的这个过程中,都是处于事务阻塞状态。性能不佳(第三方节点访问公共资源会因此也处于阻塞状态)

3 数据不一致。在二阶段提交的阶段二中,当协调者发送commit请求后,发生局部网络异常或者发送commit请求过程中协调者发送故障,则有可能只有一部分参与者接收到了commit请求,就可能导致接受到commit请求的参与者进行事务提交,而未接受到commit消息的参与者无法提交事务,导致了整个分布式系统出现数据不一致的情况

4 状态丢失问题。 当协调者发送commit请求过程中出现宕机,恰好唯一收到这条消息的参与者也宕机,那么即使新推举出的协调者,也无法知晓宕机的参与者的状态,从而无法知道这个参与者的事务是否已经被提交。

由于二阶段提交存在的一些缺陷,研究者们在此基础上提出了三阶段提交。

2 三阶提交协议(3PC)

三阶段提交(Three-phase commit),也叫三阶段提交协议(Three-phase commit protocol),是二阶段提交(2PC)的改进版本。
在这里插入图片描述

与两阶段提交不同的是,三阶段提交有两个改动点。

  1. 引入超时机制,同时在协调者和参与者中引入超时机制
  2. 在一阶段和二阶段中插入一个准备阶段,保证了在最后提交阶段之前各个参与者节点是一致的。

即三阶段提交,将原先二阶段提交的准备阶段细分为两个阶段,所以三阶段提交就有以下三个阶段:CanCommit、PreCommit、DoCommit。

CanCommit阶段

3PC的CanCommit,与二阶段提交里的准备阶段一样,协调者向参与者发送请求提交的消息,询问各个参与者是否准备完毕,是返回YSE,否则返回NO。

1 事务询问 协调者向参与者发送CanCommit请求。询问是否可以执行事务提交操作。然后开始等待参与者响应
2 响应反馈 参与者接到CanCommit请求之后,正常情况下,如果其自身认为可以顺利执行事务,则返回Yes响应,并进入预备状态。否则反馈No

PreCommit阶段
协调者根据参与者的反应情况来决定是否可以进行事务的PreCommit操作。根据参与者的响应情况,存在以下两种可能。

假如所有参与者给协调者响应都是Yes,那么就会进行事务的预执行。

1 发送预执行提交请求。协调者向参与者发送PreCommit请求,并进入Prepared阶段
2 事务预提交。参与者收到PreCommit后,会执行事务操作,并将undo和redo信息记录至事务日志之中。
3 响应反馈。如果参与者成功执行事务操作,则返回ACK,同时开始等待最终指令

假如有任何一个参与者向协调者发送了No响应,或者等待超时之后,协调者都没有接到参与者的响应,那么就执行事务的中断。

发送中断请求协调者向所有参与者发送abort请求
中断事务 参与者收到来自协调者的abort请求之后(或者超时之后,仍未收到协调者请求),执行事务中断。

doCommit
该阶段进行真正的事务提交,也可以分以下两种情况。

执行提交

1 发送提交请求 协调者接收到参与者发送的ACK响应,那么他将从预提交进入到提交状态,并向所有参与者发送doCommit请求。
2 事务提交 参与者接收到doCommit请求之后,执行正式的事务提交。并在完成事务提交之后释放所有事务资源。
3.反馈结果 参与者完成事务回滚之后,向协调者发送ACK消息。
4.中断事务 协调者接收到参与者反馈的ACK消息之后,执行事务的中断。

中断事务 协调者没有收到参与者发送的ACK响应(可能接受者发送的不是ACK响应,也可能是响应超时),那么就会执行中断事务。

1 发送中断请求 协调者向所有参与者发送abort请求
2 事务回滚 参与者收到abort请求之后,利用其在二阶段记录的undo信息来执行事务的回滚操作,并在完成回滚之后释放所有的事务资源。
3 反馈结果 参与者完成事务回滚之后,向协调者发送ACK消息。
4 中断事务 协调者接收到参与者反馈的ACK消息后,执行事务中断。

这里需要注意的是:

doCommit阶段,如果参与者无法及时接收到来自协调者的doCommit或者rebort请求时,会在等待超时之后,会继续进行事务的提交。(其实这个应该是基于概率来决定的,当进入第三阶段时,说明参与者在第二阶段已经收到了PreCommit请求,那么协调者产生PreCommit请求的前提条件是他在第二阶段开始之前,收到所有参与者的CanCommit响应都是Yes。(一旦参与者收到了PreCommit,意味他知道大家其实都同意修改了)所以,一句话概括就是,当进入第三阶段时,由于网络超时等原因,虽然参与者没有收到commit或者abort响应,但是他有理由相信:成功提交的几率很大。 )

3 2PC与3PC的区别

相对于2PC,3PC主要解决的单点故障问题,并减少阻塞,因为一旦参与者无法及时收到来自协调者的信息之后,他会默认执行commit。而不会一直持有事务资源并处于阻塞状态。但是这种机制也会导致数据一致性问题,因为,由于网络原因,协调者发送的abort响应没有及时被参与者接收到,那么参与者在等待超时之后执行了commit操作。这样就和其他接到abort命令并执行回滚的参与者之间存在数据不一致的情况。

五、总结

了解了2PC和3PC之后,我们可以发现,无论是二阶段提交还是三阶段提交都无法彻底解决分布式的一致性问题。Google Chubby的作者Mike Burrows说过, there is only one consensus protocol, and that’s Paxos” – all other approaches are just broken versions of Paxos. 意即世上只有一种一致性算法,那就是Paxos,所有其他一致性算法都是Paxos算法的不完整版。后面的文章会介绍这个公认为难于理解但是行之有效的Paxos算法。

六、参考资料

关于分布式事务、两阶段提交协议、三阶提交协议

分布式协议之两阶段提交协议(2PC)和改进三阶段提交协议(3PC)

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值