Post

Saga分布式事务处理

Saga

解耦和一致性

目前大量的系统采用SOA或者MicroServices的架构,其目的在于应用的解耦,和组织结构上的敏捷和灵活。结果就是服务、数据库的分离等技术呈现形态。当一个应用变成一系列解耦的服务之后,其要完成的目标和原来一体化的应用结构并没有不同。一个典型的服务化架构如下

MicroService

因此产生的一个最大的问题,就在于事务的处理。单体应用,我们可以通过数据库来实现ACID的保证。那么如何在多个分离的系统、甚至是分离的数据库实例进行一致性的保证。
一个典型的应用场景是下单应用:电商在确认订单的同时,需要进行支付的确认,订单的确认和支付的确认,必须保持一致。如果在单体应用中,使用单一的数据库,这非常容易得到保证。但是如果转出和转入分属两个不同的服务,独立使用自己的数据库,那如何保证两个步骤的一致?

  1. 一种做法通过直接调用和锁机制来保证也就是2PC两阶段提交的分布式事务方式:两个步骤在一个事务里,统一成功或回滚,然而如果支付时间很长,那么就会导致第一步,即下单这里所占用的资源被长时间锁定,可能会对系统可用性造成影响,特别是在性能方面,2PC在最少的情况下完成一次事务操作,需要(2 * participants)次的通信。并且因为长事务的影响,大量的计算资源被无谓占用。如果有更多的参与方加入到系统的情况下,各个参与方之间状态的保证则更加复杂,通过锁来进行控制非常容易引起连锁反应,加入资源管理器之后,则存在单点故障的问题。特别是在复杂的网络情况下,以及处理长时间事务的时候,2PC的复杂性和可靠性都会遇到非常多的挑战。
  2. 另一种则是转而使用BASE最终一致性的处理方案,使用最终一致性来代替强一致性,从而提高整体系统的性能和可用性,也就是柔性事务机制。

SAGA 事务处理

Saga是来自于1987年Hector GM和Kenneth Salem论文,Saga的原理比较简单,就是将一个长的分布式事务,拆分成一个个独立的小的事务,通过异步的事务补偿机制,达到最终一致性

  1. 客户端发出订单创建createOrder()
  2. OrderService会在内部事务进行Order数据库操作,此时订单状态是待确认状态
  3. AccountService会在其本地事务对信用卡进行授权操作,检查订单金额是否超过信用卡额度 与2PC事务相比,其过程是由一个个小的异步事务组成,而非一个分布式事务。

saga-transaction

使用补偿事务的机制,带来的影响主要在开发和设计时增加的复杂度。ACID的事务回滚依赖于数据库即可;而SAGA事务,需要更加复杂的设计,每一个Forwad Transaction必须编写符合应用一致性原则的Corresponding Transaction逻辑,一般为rollback操作。其中正向的请求事务需要时幂等,并且是可以取消的,取消操作就是对应的补偿事务,而补偿事务是不可取消的,并且必须能够最终执行。这是提供最终一致性的保证

transaction

OrderService.createOrder() => AccountService.reserveCredit()(FAILS) => OrderService.rejectOrder()

Sagas事务的一个复杂设计在系统API接口的设计

  1. 返回时机
    1. Saga所有事务完成之后,再返回,好处就是返回的结果是确定的执行结果,坏处是降低了可用性,如果一个服务需要长时间执行,则整个服务都处于等待,这种模式和2PC比较接近。
    2. saga创建之后,立刻返回结果,这会提高整个系统的可用性和整体性能,代价是返回的结果,并非确定的结果,客户端必须有通讯或者接受通知的能力。这种做法是更加推荐的做法。
  2. 返回接口createOrder()返回Order生成的ID,此时只是创建了订单,并没有完成所有的校验;getOrder(id)调用之前创建的订单,返回的订单通过了校验。
  3. UI隐藏异步的用户API接口;Saga执行效率一般小于100ms返回,如果长时间没有返回,UI显示”正在执行”弹出框;服务端可以推送提示给UI。
  4. 异步的订单创建,整个过程不再是原子化的。这就会涉及一个中间状态PENDING,那如果客户在这个中间状态尝试进行订单取消,那我们要如何处理?
    1. 不能取消,抛出CanNotCancellableException,这可能会影响用户体验
    2. 中断saga订单创建,设置订单的状态为CANCELLED,导致订单创建回滚,但这可能会导致一些复杂的中间状态处理,并且在订单创建的服务需要知道整个订单取消需要做的事情。
    3. 等待订单的saga执行完成之后,再进行取消。这会导致类似2PC的分布式锁,但是由于saga执行的效率较高,这是可行的。

Saga事务与ACID

  1. 原子性:Saga通过确保所有的事务被执行或者补偿事务全部被执行来保证。如果事务中的一个步骤失败,可以通过调用应用服务的回滚接口来实现撤销。
  2. 一致性:单个服务的一致性通过本地的数据库来保证,跨服务的一致性,通过正规应用来保证,这一点和原子性原理一致。
  3. 持久性:各个服务将事务处理中的数据保存在本地数据库来实现持久化。
  4. 隔离性:隔离性是为了保证事务独立进行,不相互干扰,与并发控制相关。隔离性会导致脏读、数据重复读、更新丢失等问题。
    1. 丢失更新
      • Ti 读 -> 其他事务写 -> Tj(或者Ci)写
      • 其他进程写入的数据丢失
    2. 脏读
      • Ti 写 -> 其他事务读 -> Ci 写
      • 其他进程读到的数据是过期的数据
    3. 不可重复读/幻读
      • Ti 读 -> 其他事务写 -> Tj 读
      • Ti 在一个事务内,读取相同的数据是不一致的
  5. Saga减少隔离性缺失的影响,可以采用的措施
    1. 幂等的请求操作:一个相同的请求,执行多次之后,结果是一样的,因为在故障发生的时候我们需要通过重试来达成最终一致,幂等性增加重试方法的安全性。
    2. Commutative Updates:符合交换律的Update,例如账户的存款和付款,存入100,取出200,和取出200,再存入100。存入和取出两个更新操作,无论什么顺序执行,结果都是一样的。这在实现上需要结合幂等操作进行处理。
    3. Version file:版本文件
      • 记录所有的更新历史
      • 使用更新记录,在读取的时候,根据过往的Event实时构建订单的状态。这类似Event Sourcing的概念
      • 在使用版本文件恢复领域模型状态的时候,需要处理Commutative Updates的问题。因为记录了之前的所有Event,使这个功能较容易实现
      • 例如:
        1. 用户先点击一次创建订单(T1),此时网络超时
        2. 用户再次点击创建订单(T2),并完成创建
        3. 用户点击取消订单(C1),并完成
        4. (T1)请求到达服务,此时应该保留原来已经取消的订单,而不应该创建一个新的订单

Saga实现细节

  1. 对Saga事务进行排序,当Ti事务完成之后,需要决定下一步要怎么进行。如果成功执行T(i+1)分支,如果失败,则执行C(i-1)分支。这类似一个工作流,或者状态机的概念。 这时候有两种选择,一种通过集中式的逻辑编排Orchestration,一种是基于事件的分布式的逻辑编排Choreography
  2. Choreography

choreography

各个参与者之间通过事件/消息进行通知,事件或者消息通过MQ或者网络在各个参与者之间交互
1. 优点:简单,特别是使用Event Sourcing技术的时候,各个参与者之间相对解耦,如果只有三四个参与者使用这种方式可以很好地解决问题 2. 缺点:循环依赖,每个服务都要监听其他服务的事件;领域模型例如订单和账户,需要知道更多的和本模型无关的信息,比如当订单发出订单已经创建后,需要发出订单已经创建的事件,而账户服务需要知道如何处理订单已经创建这个事件,例如进行账户扣款等,过多的监听和事件的处理,很容易使得程序陷入混乱和难以管理的境地;某种程度上,事件和也是一种直接调用。

  1. Orchestration

orchestration

在整个系统中新增一个Saga(Orchestrator)的调度组角色,其会跟踪和保存引用中的状态,并根据各个参与者的反馈,调用其他参与者。每一个参与者只需要和Saga实现交互即可。
1. 优点:更容易被理解,多服务之间的协调引入一个协调者使得整个逻辑更加清晰;同时使服务提供者之间减少对彼此逻辑的关注;避免了循环依赖 2. 缺点:存在单点故障,Orchestrator出现故障会导致整个服务不可用,需要对服务进行高可用处理;逻辑集中在sagas上,服务本身的逻辑无法完整表达应用的逻辑,本身也是对微服务和领域驱动原则的一个破坏。

  1. Saga 需要保证在参与者出现短暂故障的时候,可以完成参与者的调用,一般可以采用异步的消息中间件来实现。

messaging

  1. 按照上图的架构,Saga还需要保证Service中,对数据库的操作和消息的发布必须要原子化
    1. 使用数据库MESSAGE表作为消息队列,使用一个本地数据库事务来保证原子性,同时轮询MESSAGE表,并发送到对应的参与者,执行完成之后将消息删除,或者Tail数据库的binlog来读取事件。
    2. 使用Event Sourcing,将事件表作为一个消息队列
    3. 使用支持JMS规范的消息中间件,比如ActiveMQ可以在内部使用JtaTransactionManager进行统一的事务管理
    4. 设计可靠的消息处理机制,当消息投递出现异常的时候,进行异常的消息处理

    mqprocess

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    
      对于上面的MQ正向流程处理
         
      a) 首先业务发起方在进行业务处理之前,先发送一个预发送消息给到消息中间件
         
      b) 消息中间件收到这个消息之后,进行存储状态设置为"待确认"
         
      c) 返回业务发起方执行结果,如果消息保持则进行业务逻辑处理,否则终止流程或者由业务发起方从新发起请求
         
      d) 业务处理方处理完业务流程之后,将处理结果返回给消息中间件,如果处理成功则消息中间件将消息状态修改为待发送,如果处理失败则删除原来的消息;
         
      待发送的消息,则可以投递给下一个流程进行处理
      在整个正向的消息处理流程中,因为夸机器夸网络的关系,可能出现故障导致如下的情况  
      * 消息未保存,业务未执行
      * 消息已保存(待确认),业务未执行或者执行失败,可能原因有:主动方没有收到MQ返回的消息保存确认信息;业务处理失败后,发送给MQ的消息,未顺利送达,或者在处理消息存储的时候出错
      * 消息以保存(待确认),业务执行成功,可能的原因有:和上面的类似
      为了解决消息的可靠性问题,需要引入一个补偿机制  
         
      ![mqCompensation](/assets/img/saga/mqCompensation.png)  
         
      在消息框架内,引入一个异常恢复的机制,通过定时器定时回查业务处理结果,根据业务处理结果的返回值,判断对于待确认的消息,是进行删除处理或者修改状态进行消息投递。这样即可达到消息最终和业务系统一致的保证。  
      > 异常处理流程,如果出现故障,只需要再重复执行即可,不需要对其进行特殊的处理
    
This post is licensed under CC BY 4.0 by the author.