分布式事务通用解决方案

2018-08-26 09:37:39   最后更新: 2018-09-10 14:40:25   访问数量:113




对于一个分布式系统,分布式事务总是不得不涉及的一个技术,尤其是在当前流行的微服务架构中,分布式事务完全是无法避免的

那么,究竟有哪些手段可以保证分布式事务的执行呢?本文就来详细讲解一下

 

一切开始之前,我们先来介绍一下分布式事务是什么,那么,首先我们不得不说的就是事务是什么

所谓的事务就是若干个逻辑工作单元组成的一系列操作,事务保证了他们要么完全执行,要么完全不执行,对于每一个逻辑工作单元,事务必须满足所谓的 ACID 属性,那么什么是 ACID 属性呢?

 

Atomicity -- 原子性

事务必须是源自工作单元,对于数据修改,要么全都执行,要么全都不执行

在任何情况下,系统都必须处理所有操作或完全不处理,而不是逻辑工作单元的子集

 

Consistency -- 一致性

事务在完成时,必须使所有的数据都保持一致状态

事务结束时,所有的内部数据都必须是正确的

 

Isolation -- 隔离性

由并发事务所作的修改必须与任何其它并发事务所作的修改隔离

事务查看数据时数据所处的状态,要么是另一并发事务修改它之前的状态,要么是另一事务修改它之后的状态,事务不会查看中间状态的数据

 

Durability -- 持久性

事务完成之后,它对于系统的影响是永久性的。该修改即使出现致命的系统故障也将一直保持

 

在集群环境下,如果我们去追求 ACID 来保证事务的执行,会导致我们的系统性能大幅下降,复杂度急剧升高,加州大学伯克利分校Eric Brewer教授提出了分布式事务的三个属性 -- CAP 定理

 

Consistency -- 一致性

所有的分布式节点都能同时感知到所有数据

 

Availability -- 可用性

每个操作都必须以可预期的响应结束

 

Partition tolerance -- 分区容错性

即使出现单个组件无法可用,系统依然可以继续工作

 

在分布式系统中,在任何数据库设计中,一个Web应用至多只能同时支持上面的两个属性,显然任何横向扩展策略都要求即使部分分布式节点不可用,系统依然可以工作,因此设计人员必须在一致性与可用性之间做出选择

如果举一个 mysql 主从同步的例子就非常容易理解了,对于 mysql 主从同步我们有两种选择:

  1. 高可用 -- 只写主库,成功后立即返回,此后再通过 binlog 同步从库
  2. 强一致性 -- 每个请求都同时写入主库与从库,只有当主库、从库全部完成操作,才返回正确的结果

如果选择高可用模式,那么对于从成功写入主库,到 binlog 同步到从库之间有一段短暂的主从不一致,如果发生故障或宕机,这个不一致时间将会显著增加,从而降低了数据的安全性

而如果选择强一致性模式,就会造成在故障或宕机发生时,所有的写入操作都无法完成,从而降低了整个系统的可用性

这就是系统设计者必须权衡和考虑的抉择,但通常来说,可用性是系统更为关键的指标,牺牲强一致性来换取系统的高可用性是大部分场景的通用做法

 

在 CAP 理论的基础上,诞生了 BASE 理论,他是对 CAP 理论的眼神,他的思想是:面对大型高可用可扩展的分布式系统,即使无法做到强一致性,但每个应用都可以根据自身业务特点,采用适当的方式来使系统达到最终一致性

核心思想是:

 

Basically Available -- 基本可用

在系统发生故障时,允许系统损失部分可用性,来保证核心可用

 

Soft State -- 软状态

软状态是指允许系统存在中间状态,而该中间状态不会影响系统整体可用性

允许系统在不同节点的数据副本之间进行数据同步的过程存在延时

 

Eventual Consistency -- 最终一致性

当一段窗口期后,例如网络通信事件结束或故障解除后,系统最终可以保证其一致性

 

相当于 ACID 刚性事务而言,遵循 BASE 理论牺牲强一致性换取系统高可用的事务模型就是柔性事务模型

它有四种类型:

  1. 两阶段型 -- 分布式环境下事务处理的典型模式,如两阶段提交 2PC 协议
  2. 补偿型 -- 事务失败后通过补偿机制回滚操作保证事务的最终一致性
  3. 异步确保型 -- 这类事务将一个分布式事务切分成一个主事务与若干个小事务,主事务先行提交,一旦提交成功,则异步通知从事务提交,这种事务模型适用于从事务可以保证提交成功或其失败对于整个系统并无实际影响的场景
  4. 最大努力通知型 -- 这个模型比较简单,当分布式系统中的某个节点执行失败后,整个事务不会滚而是不断重试直到该节点完成事务的提交

 

执行流程

在分布式系统中,每个节点都可以知晓本服务的操作结果,但是却很难准确知道他所提来的其他节点的操作结果

因此,两阶段提交的分布式事务模型就诞生了,他的核心思想是引入一个协调者,协调者负责掌控全局,协调整个事务的进行

 

从图上,我们可以清楚的看到,2PC 整个流程分为两个阶段:

  1. 准备阶段 -- 协调者向所有参与者发送事务执行请求,等待反馈结果;各参与者执行请求并返回协调者执行结果
  2. 提交阶段 -- 如果所有参与者均执行成功,则向所有参与者发送事务提交请求

这个模型是建立在假定提交阶段一定能够保证成功的基础上的

 

异常情况

在第一阶段执行完成后,可能存在以下三种执行结果:

  1. 所有参与者返回执行成功的结果
  2. 一个或多个参与者返回执行失败
  3. 协调者等待结果超时

后两种情况就是执行的异常情况,在这样的情况下,协调者需要通知所有参与者执行回滚操作

 

存在的问题

两阶段提交可以在一定程度上保证分布式事务的执行,但仍然存在以下问题:

  1. 单点问题 -- 协调者保证了分布式事务的成功执行,但是谁来保证协调者的正常运行呢?一旦协调者宕机,整个系统将无法继续运行,尤其是如果参与者一直停留在等待协调者通知提交或回滚操作时,整个集群将无法继续提供服务
  2. 同步阻塞 -- 对于完成准备阶段的参与者,他将处于阻塞等待协调者调度提交或回滚的状态下,从而无法从事其他工作,造成阻塞导致整个系统效率的降低
  3. 数据不一致 -- 如上所述,2PC 是建立在保证提交阶段能够成功的前提下的分布式事务模型,如果提交阶段出现了网络问题,参与者没有收到提交或回滚请求,那么就会一直停留在阻塞状态,从而导致了数据的不一致

虽然上述问题都可以通过一些手段去解决,但是那又会很大程度上增加系统的复杂度

两阶段提交最大的缺点是性能比较差,不适合高并发和高性能要求的场景

 

基于两阶段提交的事务模型,阿里巴巴提出了补偿事务模型 -- TCC,他将资源层的 2PC 模型放到了服务层,实现了更高的性能

 

执行流程

TCC 得名于整个事务过程分为三个阶段:try、confirm、cancel 阶段

整个系统中,存在三个角色:

  1. 主业务服务 -- 整个事务的发起方,负责通知事务管理器事务的参与者与参与顺序,以及通知所有事务参与者预留资源
  2. 业务活动管理器 -- 管理控制整个业务活动,记录维护TCC全局事务的事务状态和每个从业务服务的子事务状态,并在业务活动提交时确认所有的TCC型操作的confirm操作,在业务活动取消时调用所有TCC型操作的cancel操作
  3. 从业务服务 -- 提供TCC业务操作,是整个业务活动的操作方,必须实现 try、confirm、cancel 三个接口,且 confirm 和 cancel 两个接口必须的是幂等的

 

 

优缺点

TCC 的优势非常明显,那就是与 2PC 相比,流程相对简单,但是相对的,业务服务的设计难度也就相应的提升了,包括 try、confirm、cancel 接口都必须幂等,而对于很多业务来说,幂等的补偿逻辑是难处理的

 

本地消息表是目前被广泛使用的一种分布式事务模型,其核心思想是将分布式事务拆分成本地事务进行处理

 

 

其基本思路是:

  • 消息生产方 -- 需要额外建一个消息表,并记录消息发送状态,消息表和业务数据要在一个事务里提交,也就是说他们要在一个数据库里面,消息会经过MQ发送到消息的消费方,消息生产方通过重试等方式保证消息的成功发送
  • 消息消费方 -- 处理消息,完成业务逻辑,如果业务执行失败,可以给生产方发送一个业务补偿消息,通知生产方进行回滚等操作,否则更新消息表中该条消息的状态

 

这是一种比较经典通用的分布式事务模型,但如果没有进行一定的封装,就需要处理很多额外的问题

 

目前大部分消息队列组件是不支持事务消息的,阿里开源的消息队列中间件 RocketMQ 提供了事务机制,其原理就是将我们上面说的本地消息表集成在了消息队列中间件中

 

事务消息的发送总共分为三个阶段:发送Prepared消息、执行本地事务、发送确认消息

RocketMQ 第一阶段发送 Prepared 消息时,会拿到消息的地址,第二阶段执行本地事物,第三阶段通过第一阶段拿到的地址去访问消息,并修改消息的状态

如果数据没有发送到broker,导致事务消息的 状态更新失败,broker会有回查线程定时(默认1分钟)扫描每个存储事务状态的表格文件,如果是已经提交或者回滚的消息直接跳过,如果是prepared状态则会向Producer发起CheckTransaction请求,Producer会调用DefaultMQProducerImpl.checkTransactionState()方法来处理broker的定时回调请求,而checkTransactionState会调用我们的事务设置的决断方法来决定是回滚事务还是继续执行,最后调用endTransactionOneway让broker来更新消息的最终状态

 

这样基本上可以解决消费端超时问题,但是如果消费失败应该如何处理呢?目前 RocketMQ 的事务模型中并不包含业务失败后的处理方法,这是业务具体的实现者来考虑的

 

Sagas 事务模型又被称为长时间运行的事务,他的核心思想是拆分分布式系统中的长事务为多个短事务,或者叫多个本地事务,然后由 Sagas 工作流引擎负责协调

如果整个流程正常结束,那么就算是业务成功完成,如果在这过程中实现失败,那么Sagas工作流引擎就会以相反的顺序调用补偿操作,重新进行业务回滚

下图展示了一次关于购买旅游套餐业务操作涉及到三个操作,他们分别是预定车辆,预定宾馆,预定机票,他们分别属于三个不同的远程接口,事务管理器保证他们的执行顺序以及回滚顺序:

 

 

https://wenku.baidu.com/view/be946bec0975f46527d3e104.html

https://help.aliyun.com/document_detail/43348.html?spm=5176.doc51338.6.567.x2IZub

https://github.com/changmingxie/tcc-transaction

https://www.cnblogs.com/savorboard/p/distributed-system-transaction-consistency.html

https://www.jianshu.com/p/453c6e7ff81c

 






技术帖      技术分享      transaction      事务      分布式事务      acid      cap      base      2pc      tcc     


京ICP备15018585号