Saga 模式服务设计

基于 Saga 模式分布式事务的多年实践,本文提供了在 Saga 模式下服务设计的一些最佳实践与经验。

服务执行与补偿

Saga 模式是 SEATA 提供的长事务解决方案。Saga 模式下,分布式事务内存在多个参与者,每一个参与者都是一个冲正补偿服务,用户需要根据业务场景实现其正向操作(原服务)和逆向回滚操作(补偿服务)。在事务执行过程中,首先会依次执行各参与者的正向操作,如所有正向操作执行成功,则事务提交。如任一正向操作执行失败,则事务会执行之前各参与者的逆向回滚操作,回滚已提交的参与者,直至事务退回至其初始状态。

允许服务空补偿

空补偿,指的是原服务未执行,补偿服务已执行。大致场景如下:

  1. 由于丢包,原服务未执行。

  2. 然后,Saga 状态机执行补偿操作,事务触发回滚。

  3. 此时,收到补偿请求,未收到原服务请求。

针对该问题,在服务设计时,需要允许空补偿,即在没有找到要补偿的业务主键时,返回补偿成功,并将原业务主键记录下来,标记该业务流水已补偿成功。

服务防悬挂控制

悬挂,指的是补偿服务比原服务先执行。大致场景如下:

  1. 由于网络拥堵,原服务超时未执行;

  2. 然后,Saga 状态机触发回滚,服务被定义为已回滚,向客户端返回补偿成功;

  3. 最后经过一段网络拥堵后,原服务最终又到达了。

此时,需要检查当前业务主键是否已经在空补偿记录下来的业务主键中存在,如果存在则要拒绝执行该笔服务,以免造成数据不一致。

服务幂等控制

在分布式事务执行过程中,原服务与补偿服务都需要保证幂等性。由于网络连接可能超时,可以设置重试策略,重试发生时要通过幂等控制避免业务数据重复更新。

判断服务状态

Saga 发起方作为服务方,需要根据状态机的执行状态,返回服务状态给调用方。可以参考以下代码示例:

  StateMachineInstance inst = stateMachineEngine.startWithBusinessKey("testTransferBySaga",null, businessKey, params);
    if(ExecutionStatus.SU.equals(inst.getStatus()) && inst.getCompensationStatus()==null){
        //正向状态为成功, 补偿状态为空(没有触发回滚),则交易成功,返回 SU
    }else if(ExecutionStatus.SU.equals(inst.getCompensationStatus())){
        //补偿状态为成功,则交易回滚成功,返回失败 FA
        //如果有多级调用,可以给调用方返回失败 FA,这样上层编排则不再进行补偿该服务
    }else if(ExecutionStatus.FA.equals(inst.getStatus()) && inst.getCompensationStatus()==null){
         //正向状态为失败, 补偿状态为空(没为触发回滚),则交易失败且没有数据不一致,返回FA
    }else{
        //其它情况说明是正向或补偿结果未知,返回未知UN给调用方,需要进行重试
    }

应对隔离性问题

Saga 模式由于一阶段已经提交本地数据库事务,且没有进行预留动作,因此无法保证隔离性。在极端情况下,可能由于脏写无法完成回滚操作。例如,在分布式事务中,需要先给用户 A 充值,然后给用户 B 扣减余额。如果在给用户 A 充值成功,在事务提交以前,用户 A 已经把余额消费掉了,如果事务发生回滚,此时则没有办法进行补偿了。

以上是一个极端场景下隔离性缺乏造成的典型问题。在实践中,一般可以采取以下方法应对该问题:

  • 业务流程设计时,始终遵循“宁可长款,不可短款”的原则。长款指的是客户少了钱机构多了钱,以机构信誉可以给客户退款;反之则是短款,少的钱可能追不回来了。所以在业务流程设计上,一定是先扣款。

  • 有些业务场景可以允许让业务最终成功,在回滚不了的情况下,可以继续重试完成后面的流程。所以状态机引擎除了提供“回滚”能力还需要提供“向前”恢复上下文继续执行的能力,让业务最终执行成功,达到最终一致性的目的。