「尺有所短,寸有所长;不忘初心,方得始终」。
一、什么是分布式事务
「什么是分布式系统」
部署在不同结点上的系统通过网络交互来完成协同工作的系统 比如:充值加积分的业务,用户在充值系统向自己的账户充钱,在积分系统中自己积分相应的增加。充值系统和积分系统是两个不同的系统,一次充值加积分的业务就需要这两个系统协同工作来完成。
「什么是事务」
事务是指由一组操作组成的一个工作单元,这个工作单元具有原子性(atomicity)、一致性(consistency)、隔离性(isolation)和持久性(durability)。
「原子性(atomicity)」
执行单元中的操作要么全部执行成功,要么全部失败。如果有一部分成功一部分失败那么成功的操作要全部回滚到执行前的状态。
「一致性(consistency)」
执行一次事务会使用数据从一个正确的状态转换到另一个正确的状态,执行前后数据都是完整的。
「隔离性(isolation)」
在该事务执行的过程中,任何数据的改变只存在于该事务之中,对其他事物没有影响,事务与事务之间是完全的隔离的。只有事务提交后其它事务才可以查询到最新的数据。
「持久性(durability)」
事务完成后对数据的改变会永久性的存储起来,即使发生断电宕机数据依然在。
「什么是本地事务」
本地事务就是用关系数据库来控制事务,关系数据库通常都具有ACID特性,传统的单体应用通常会将数据全部存储在一个数据库中,会借助关系数据库来完成事务控制。
「什么是分布式事务」
在分布式系统中一次操作由多个系统协同完成,这种「一次事务操作涉及多个系统通过网络协同完成的过程称为分布式事务」。这里强调的是多个系统通过网络协同完成一个事务的过程,并不强调多个系统访问了不同的数据库,即使多个系统访问的是同一个数据库也是分布式事务,
如下图:
另外一种分布式事务的表现是,一个应用程序使用了多个数据源连接了不同的数据库,当一次事务需要操作多个数据源,此时也属于分布式事务,当系统作了数据库拆分后会出现此种情况
上面两种分布式事务表现形式第一种用的最多
二、分布式事务的应用场景
分布式事务的应用场景
电商的订单和库存系统
一般大型电商系统中,下单过程会被分为订单和库存会分为两个系统协作完成。
金融系统的银行充值扣款
在众多的金融系统(支付宝/微信等)平台通过银行卡充值扣款,会由金融系统与银行系统协作完成。
SNS系统的消息推送
应用系统发送邮件或者短信,需要应用系统与通信系统(运营商)协作完成。
三、实现分布式事务解决方案
常见的分布式事务解决方案大致分为以下几种:
两阶段提交(2PC) 三阶段提交(3PC) 补偿事务(TCC) 本地消息表(异步确保) MQ 事务消息 Sagas 事务模型
3.1. 一致性协议 两阶段提交(2PC)
2PC即两段提交协议,是将整个事务提交分成两个阶段来完成:
P-准备阶段( Prepare phase )
C-提交阶段( commit phase )
「两阶段提交由协调者和参与者组成,共经过两个阶段和三个操作」,部分关系数据库如Oracle、MySQL支持两阶段提交协议。
「二阶段提交的算法思路」:
「参与者将操作成败通知协调者,再由协调者根据所有参与者的反馈情况决定各参与者是否要提交操作还是中止操作。」
3.1.1 成功执行事务 执行过程
「阶段一 准备阶段」
「ACK 确认字符,在数据通信中,接收站发给发送站的一种传输类控制字符。表示发来的数据已确认接收无误。只有当所有参与者返回yes时,才会进行阶段二」。
「第一步:事务询问」
协调者向各个参与者发出事务请求,询问各个参与者是否可以执行事务提交操作,并开始等待各参与者的回复。
「第二步:执行事务」
写本地的undo和redo (此时还没有提交事务)。
「第三步:各参与者向协调者反馈事务询问结果。」
「阶段二:提交阶段」
「第一步:发送提交请求」
协调者向所有参与者发送commit请求。
「第二步:事务提交」
参与者接收到commit请求后,会正式执行事务提交操作,并在完成事务提交之后释放所占用的所有资源。
「第三步:反馈事务提交结果」
完成「事务提交」操作,参与者向协调者发送Ack信息。
「第四步:完成事务」
事务协调者接收到所有参与者发送的Ack信息之后,完成事务操作。
3.1.2 中断事务 执行过程
假设「参与者中任何一个向协调者发送了No响应或者在等待超时」之后,协调者无法接收到所有参与者返回的结果,那么就会中断事务
「阶段一 准备阶段」
这一阶段与上述成功执行事务的准备阶段是一样的。
「ACK 确认字符,在数据通信中,接收站发给发送站的一种传输类控制字符。表示发来的数据已确认接收无误。只有当所有参与者返回yes时,才会进行阶段二」。
「第一步:事务询问」
协调者向各个参与者发出事务请求,询问各个参与者是否可以执行事务提交操作,并开始等待各参与者的回复。
「第一步:执行事务」
写本地的undo和redo (此时还还没有提交事务)。
「第一步:各参与者向协调者反馈事务询问结果。」
「阶段二:提交阶段」
「第一步:发送回滚请求」
协调者向所有参与者发出 Rollback 请求。
「第二步:事务回滚」
参与者接收到 Rollback 请求后,会利用其在阶段一中记录的 Undo 信息来执行事务回滚操作,并在完成
回滚之后释放在整个事务执行期间占用的资源。
「第三步:反馈事务回滚结果」
参与者在「完成事务回滚」之后,向协调者发送 Ack 信息。
「第四步:中断事务」
协调者接收到所有参与者反馈的 Ack 信息后,完成事务中断。
3.1.3 2PC优缺点
「优点」
原理简单、实现方便 一定程度上保证了数据的强一致(无法100%保证强一致),适合对数据强一致要求很高的关键领域。 「缺点」
「同步阻塞」
所有事务参与者在等待其它参与者响应的时候都处于同步阻塞状态,无法进行其它操作。
「单点问题」
协调者在 2PC 二阶段中用来「控制事物的提交与回滚,发生故障将会造成很大影响」。特别是在阶段二发生
故障,所有参与者将会处于一直锁定事务资源的状态中,无法完成其它操作。
「存在数据不一致问题」
当提交阶段协调者向所有的参与者发送 commit 请求之后,「由于某些局部网络异常或者是协调者在尚未发」
「送完所有 commit 请求之前自身发生了崩溃,导致最终只有部分参与者收到了 commit请求」,将导致数据不一致问题。
3.2 一致性协议 三阶段提交(3PC)
3PC是2PC的改进版,将2PC的准备阶段一分为二,j将事物执行过程分为三个阶段,以此保证在提交阶段之前,各
参与者节点的状态都一致:
「准备阶段(CanCommit)」 「预提交阶段(PreCommit)」 「提交阶段(DoCommit)」
同时在协调者和参与者中都引入「超时机制,当参与者
各种原因未收到协调者
的commit请求后,会对本地事务进行commit,不会一直阻塞等待,解决了2PC
的单点故障问题」,但3PC
还是没能从根本上解决数据一致性的问题。
3.2.1 三阶段提交执行流程
「阶段一 CanCommit阶段」
协调者向所有参与者发出包含事务内容的CanCommit请求,询问是否可以提交事务,并等待所有参与者答复。
参与者收到CanCommit请求后,如果认为可以执行事务操作,则反馈YES并进入预备状态,否则反馈NO。
「阶段二 PreCommit阶段」
此阶段分两种情况:
「事务预提交」
「所有参与者在CanCommit阶段均反馈YES,即执行事务预提交」。
「中断事务」
「任何一个在参与者CanCommit阶段反馈NO,或者等待超时后协调者尚无法收到所有参与者的反馈,即执行中断事务」。
协调者向所有参与者发出abort请求。 无论收到协调者发出的abort请求,或者在「等待协调者请求过程」中出现超时,参与者均会中断事务。 协调者向所有参与者发出PreCommit请求,进入预提交阶段。 参与者收到PreCommit请求后,执行事务操作,将Undo和Redo信息写入事务日志中(「不提交事务」)。 各参与者向协调者反馈Ack响应或No响应,并等待最终指令。 「阶段三 DoCommit阶段」
此阶段也存在两种情况:
「进入阶段三后,无论协调者是否出现问题,或者协调者与参与者网络出现问题,都会导致参与者无法接收到协调者发出的do Commit请求或abort请求。此时,参与者都会在等待超时之后,继续执行事务提交」。
「提交事务」
所有参与者均反馈Ack响应,即执行真正的事务提交。
「中断事务:」
任何一个参与者反馈NO,或者等待超时后协调者尚无法收到所有参与者的反馈,即中断事务。
如果协调者处于工作状态,向所有参与者发出abort请求。 参与者使用阶段1中的Undo信息执行回滚操作,并释放整个事务期间占用的资源。 各参与者向协调者反馈Ack完成的消息。 协调者收到所有参与者反馈的Ack消息后,即完成事务中断。 如果协调者处于工作状态,则向所有参与者发出do Commit请求。 参与者收到do Commit请求后,会正式执行事务提交,并释放整个事务期间占用的资源。 各参与者向协调者反馈Ack完成的消息。 协调者收到所有参与者反馈的Ack消息后,即完成事务提交。
3.2.2 3PC的优缺点
「优点」
降低了阻塞范围,在等待超时后协调者或参与者会中断事务。
避免了协调者单点问题,阶段3中协调者出现问题时,参与者会继续提交事务。
「缺陷」
脑裂问题依然存在,即在参与者收到PreCommit请求后等待最终指令,如果此时协调者无法与参与者正常通信,会导致参与者继续提交事务,造成数据不一致。
3.3. 补偿事务(TCC)
「TCC 其实就是采用的补偿机制,其核心思想是:针对每个操作,都要注册一个与其对应的确认和补偿(撤销)操作」。它分为三个阶段:
Try 阶段主要是对业务系统做检测及资源预留
Confirm 阶段主要是对业务系统做确认提交,对try阶段预留的资源正式执行,默认 Confirm阶段是不会出错的。即:只要Try成功,Confirm一定成功。
Cancel 阶段主要是在业务执行错误,需要回滚的状态下执行的业务取消,预留资源释放。
3.3.1 TCC应用实例
下边用一个下单减库存的业务为例来说明
「Try阶段」
下单业务由订单服务和库存服务协同完成,在try阶段订单服务和库存服务完成检查和预留资源。
订单服务检查当前是否满足提交订单的条件(比如:当前存在未完成订单的不允许提交新订单)。
库存服务检查当前是否有充足的库存,并锁定资源。
「Confirm阶段」
订单服务和库存服务成功完成Try后开始正式执行资源操作。
订单服务向订单写一条订单信息。
库存服务减去库存。
「Cancel阶段」
如果订单服务和库存服务有一方出现失败则全部取消操作。
订单服务需要删除新增的订单信息。
库存服务将减去的库存再还原。
3.3.2 3PC的优缺点
「优点」:
最终保证数据的一致性,在业务层实现事务控制,灵活性好。
「缺点」:
应用侵入性强:TCC由于基于在业务层面,导致每个操作都需要有
try
、confirm
、cancel
三个接口。开发难度大:代码开发量很大,要保证数据一致性
confirm
和cancel
接口还必须实现幂等性。注意:TCC的try/confirm/cancel接口都要实现幂等性,在为在try、confirm、cancel失败后要不断重试。
3.3.3 幂等性
「幂等性是指同一个操作无论请求多少次,其结果都相同。」
幂等操作实现方式有:
操作之前在业务方法进行判断如果执行过了就不再执行。
缓存所有请求和处理的结果,已经处理的请求则直接返回结果。
在数据库表中加一个状态字段(未处理,已处理),数据操作时判断未处理时再处理。
3.4. 本地消息表(异步确保)
本地消息表这种实现方式应该是业界使用最多的,其「核心思想是将分布式事务拆分成本地事务进行处理」,这种思路是来源于ebay。我们可以从下面的流程图中看出其中的一些细节:
基本思路就是
「消息生产方」
需要额外建一个消息表,并记录消息发送状态。消息表和业务数据要在一个事务里提交,也就是说他们要在一个数据库里面。然后消息会经过MQ发送到消息的消费方。如果消息发送失败,会进行重试发送。
「消息消费方」
需要处理这个消息,并完成自己的业务逻辑。此时如果本地事务处理成功,表明已经处理成功了,如果处理失败,那么就会重试执行。如果是业务上面的失败,可以给生产方发送一个业务补偿消息,通知生产方进行回滚等操作。
「生产方和消费方定时扫描本地消息表」
把还没处理完成的消息或者失败的消息再发送一遍。如果有靠谱的自动对账补账逻辑,这种方案还是非常实用的。
这种方案「遵循BASE理论,采用的是最终一致性」,比较适合实际业务场景的,即不会出现像2PC那样复杂的实现(当调用链很长的时候,2PC的可用性是非常低的),也不会像TCC那样可能出现确认或者回滚不了的情况。
「优点」:一种非常经典的实现,避免了分布式事务,实现了最终一致性。在 .NET中 有现成的解决方案。
「缺点」:消息表会耦合到业务系统中,如果没有封装好的解决方案,会有很多杂活需要处理。
3.5. MQ 事务消息
有一些第三方的MQ是支持事务消息的,比如RocketMQ,他们支持事务消息的方式也是类似于采用的二阶段提交,但是市面上一些主流的MQ都是不支持事务消息的,比如 RabbitMQ 和 Kafka 都不支持。
以阿里的 RocketMQ 中间件为例,其思路大致为:
第一阶段Prepared消息,会拿到消息的地址。
第二阶段执行本地事务,
第三阶段通过第一阶段拿到的地址去访问消息,并修改状态。
「在业务方法内要想消息队列提交两次请求,一次发送消息和一次确认消息」。如果确认消息发送失败了RocketMQ会定期扫描消息集群中的事务消息,这时候发现了Prepared消息,它会向消息发送者确认,所以生产方需要实现一个check接口,RocketMQ会根据发送端设置的策略来决定是回滚还是继续发送确认消息。这样就保证了消息发送与本地事务同时成功或同时失败。
「优点」:实现了最终一致性,不需要依赖本地数据库事务。
「缺点」:实现难度大,主流MQ不支持,没有.NET客户端,RocketMQ事务消息部分代码也未开源
3.6. Sagas 事务模型
「什么是Saga」
Saga事务模型又叫做长时间运行的事务(Long-running-transaction), 它是由普林斯顿大学的H.Garcia-Molina等人提出,它「描述的是另外一种在没有两阶段提交的的情况下解决分布式系统中复杂的业务事务问题」。
该「模型其核心思想就是拆分分布式系统中的长事务为多个短事务,或者叫多个本地事务,然后由 Sagas 工作流引擎负责协调」,如果整个流程正常结束,那么就算是业务成功完成,如果在这过程中实现失败,每个本地事务有相应的执行模块和补偿模块,那么Sagas工作流引擎就会以相反的顺序调用补偿操作,重新进行业务回滚,达到事务的最终一致性。
它与2PC不同,2PC是同步的,而Saga模式是异步和反应性的。在Saga模式中,分布式事务由所有相关微服务上的异步本地事务完成。微服务通过事件总线相互通信。
「示例1」
比如我们一次关于购买旅游套餐业务操作涉及到三个操作,他们分别是预定车辆,预定宾馆,预定机票,他们分别属于三个不同的远程接口。可能从我们程序的角度来说他们不属于一个事务,但是从业务角度来说是属于同一个事务的。
他们的执行顺序如上图所示,所以当发生失败时,会依次进行取消的补偿操作。
「示例2」
下面是以客户订单为例的Saga模式图:
在上面的示例中,OrderMicroservice接收下订单的请求。它首先启动本地事务以创建订单,然后发出OrderCreated事件。CustomerMicroservice接收此事件后,将处理事件并更新客户资金。如果从账户成功扣除,CustomerFundUpdated则会发出一个事件,在此示例中表示交易结束。
如果有某个微服务无法完成其本地事务,则其他微服务将运行补偿事务以回滚更改。以下是补偿交易的Saga模式图:
在上面的例子中,UpdateCustomerFund由于某种原因失败了,然后它发出了一个CustomerFundUpdateFailed事件。在OrderMicroservice监听到该事件并启动其补偿事务恢复所创建的订单。
因为长事务被拆分了很多个业务流,所以 「Sagas 事务模型最重要的一个部件就是工作流或者也可以叫流程管理器(Process Manager)」,工作流引擎和Process Manager虽然不是同一个东西,但是在这里,他们的职责是相同的。在选择工作流引擎之后,最终的代码也许看起来是这样的
SagaBuilder saga = SagaBuilder.newSaga("trip")
.activity("Reserve car", ReserveCarAdapter.class)
.compensationActivity("Cancel car", CancelCarAdapter.class)
.activity("Book hotel", BookHotelAdapter.class)
.compensationActivity("Cancel hotel", CancelHotelAdapter.class)
.activity("Book flight", BookFlightAdapter.class)
.compensationActivity("Cancel flight", CancelFlightAdapter.class)
.end()
.triggerCompensationOnAnyError();
camunda.getRepositoryService().createDeployment()
.addModelInstance(saga.getModel())
.deploy();
「Saga模式的优点」
Saga模式的一大优势是它支持长事务。因为每个微服务仅关注其自己的本地原子事务,所以如果微服务运行很长时间,则不会阻止其他微服务。这也允许事务继续等待用户输入。此外,由于所有本地事务都是并行发生的,因此任何对象都没有锁定。
「Saga模式的缺点」
为了解决Saga模式的复杂性问题,「将流程管理器添加为协调器是很正常的。流程管理器负责监听事件和触发端点」。
Saga模式很难调试,特别是涉及许多微服务时。此外,如果系统变得复杂,事件消息可能变得难以维护。
Saga模式的另一个缺点是它没有读取隔离。
例如,客户可以看到正在创建的订单,但在下一秒,订单将因补偿交易而被删除。
「结论」
Saga模式是解决基于微服务的体系结构的分布式事务问题的优选方式。
但是它还引入了一些新的问题,例如
1.如何以原子方式更新数据库并发出事件。
2.采用Saga模式需要改变开发和测试的思维方式。