前言

虽然一直说想写一篇关于Saga模式,在多次尝试后不得不承认这玩意儿的仿制代码真不是我一个菜鸟就能完成的,所以还是妥协般地引用现成的Eventuate Tram Saga框架(虽然我对它一直很反感)和Seata的Saga模式。有一说一,我极其不愿意采用这种封装好的框架和解决方案对知识进行讲解,因为庞大的架构和源码对读者来说跨度太大,可是如果想把它内部流程讲清又要花费很大的精力进行详解,而且太考验文章的叙述文字功底了。

这有违我一直提倡的:“架构”是一种理论思想而非具体百搭的代码段。鉴于此,此章我会竭尽所能把基于框架的Saga模式讲解清晰,希望读者能够反复阅读,有不解的地方尽情在评论区询问。

Saga是什么?

分布式事务的挑战

在分布式系统中会把一个应用系统拆分为可独立部署的多个服务,因此需要服务与服务之间远程协作才能完成事务操作,这种分布式系统环境下的保证事务特性(ACID)的机制运转称之为分布式事务。

多个服务、数据库、消息代理之间在很多时候是必须维持数据一致性。特别是涉及金融交易的模块,即使可能只是出现1的偏差,也会因为蝴蝶效应而导致巨大损失。最原始的分布式事务管理的实施标准是XA模式(Seata AT和XA模式),其采用了两段式提交来保证事务中所有参与者都是健康且同时完成提交,或则在事务失败时同时进行回滚。

但XA模式也衍生出相关的问题:1、许多新技术包括NoSQL、RabbitMq并不支持XA标准的分布式事务;2、其性能上因为采用阻塞性协议,所有参与节点都是事务阻塞型的。当参与者占有公共资源时,其他第三方节点访问公共资源不得不处于阻塞状态,这就容易出现响应时间增长、死锁等问题;3、由于分布式事务采用的时同步进程间通信(发送请求就必须收到应答,否则不进行下次传输),这导致一旦分布式事务中的一个参与方出现问题就会让整个系统无法正常使用,降低了系统总体的可用性。

Eric Brewer在著名的CAP理论中证明了系统只要在一致性、可用性、分区容错性中保证两个就行。

分区容错性:在分布式系统中因为网络通讯故障而导致整个整体被分割成一个个分区,而这些分区仍可以继续对外供满足一致性和可用性的服务。(必要的)

可用性:系统一直处于可用状态,能正常响应数据,但是不保证响应数据为最新数据。

一致性:数据在多个副本之间能够保持一致的特性(Mysql主从表)。

今天,大部分架构师会倾向保证系统可用性,而将数据强制一致性的要求降低至满足最终一致便可。Saga正是为了解决微服务架构下数据一致性的问题,构建在松耦合、异步服务之上的机制。

Saga的机制

当本地事务完成时服务就会发布消息(图中Create order完成时发布消息),然后触发Saga中的下一个步骤(触发库存服务中预减库存)。通过消息发送的方式可以确保所有参与服务之间的松耦合,也可以让整个业务流程实现异步通信,即使其中一些消息的接收方不可用,消息代理也会先进性缓存,直到消息被接受为止。

Saga的补偿式事务回滚

用八个字即可概括:有则继续,无则补偿。例如:当我从钱包中拿出5块钱跟柜员说从冰箱里拿一根巧克力甜筒,如果柜员发现冰箱中有巧克力甜筒,就把甜筒拿出给我,把钱放入收银柜中。如果柜员发现冰箱没有巧克力甜筒,则把钱返回给我放回钱包

以上例子中从 ①我拿出钱支付->②柜员接过钱确认有存货->③给我甜筒->④将钱放入收银柜中->⑤我离开店铺 是一整个活动流程。其中每一小段都是一个事务(不可分割),当没有存货时就把钱返还给我确保钱包的钱没有少,这里的还钱便是给钱的补偿事务。

其中①-③被称为可补偿性事务,因为它们后面跟着的步骤可能失败,例如对①来说失败情况就是没有存货,那么补偿事务就是还钱;对③来说失败情况可能是给了假钞,那么补偿事务就是要回甜筒放回货柜里。第④步被称为关键性事务,因为后面跟着的是不可能失败的步骤(如果第④步已经成功完成,其实已经说明整个流程是成功的,因为我肯定能离开店铺)。对于最后的步骤成为可重复性事务,因为它们一定会成功。

Saga的协调模式

·协同式:把Saga的决策和决定执行顺序的逻辑分布在Saga的每一个参与方上,让它们各自通过交换事件的方式进行沟通。(各管各的)

·编排器:把Saga的决策和决定执行顺序的逻辑集中在一个Saga编排器类(理解成一个管理中心)上,Saga编排器发出命令式的消息给每一个参与方,指挥这些参与方完成具体操作。(中央调度)

协同式Saga

在下图中是简单的订单创建->支付成功->完成订单的流程,其中库存服务和支付服务通过订阅相关的消息通道接受事件消息,从而能够按顺序完成需要自己参与的事务工作。

以上是支付成功情况下,支付服务就发布PaySucceed事件,之后库存服务和订单服务就陆续完成减库存和完成订单的操作。

而出现支付失败情况下,支付服务就发布PayFail事件,库存服务会消费该事件并进行Inventory cancelReduction(减库存取消)并发布ReductionFail(减库存失败),订单服务就会消费该事件并进行Cancel order(取消订单)操作。

协同式的好处:

·松耦合:一眼看去就知道啦,参与方与订阅方除了消息事件的通信,并不会产生耦合;

·简单:只需要在创建、更新、删除业务对象时发布事件即可;

弊端:

·过度分散:代码中没有集中定义Saga的地方,因为分布在各个服务中,所以在进行编写和理解时难度较大;

·服务之间循环依赖:参考上图,参与方和订阅方身份一直在相互转换相互订阅,虽然不一定引发问题,但这种循环依赖是一种不好的设计风格;

编排式Saga

编排式时实现Saga的另一种方式,使用此方式时开发人员定义一个编排器类,这个类职责就是对外告诉Saga的参与方需要做什么。Saga编排器采用同步/异步方式相应方式进行通信。

上图很明显与协同式Saga相比,流程更加简洁明了。当然它仅仅只是描述了正常情况下整个运行流程,而涉及到失败情况需要进行实物补偿时我们就得借助状态机模型(有点类似流程图,成功时怎么样,失败了怎么样)对所有可能出现的场景进行判断描述。

上图就是状态机,例如当状态机从Inventory reduction(预锁库存)状态可以转化为Order Pay或者是Reject Inventory reduction状态,当它收到正常回复Inventory Reduction(锁库存成功),那状态转化为Order Pay。而如果收到Inventory Reduction Failed回复(锁库存失败),则状态转为Reject Inventory reduction。(注:状态、消息回复、命令都可以根据自己习惯定义)

出上面例举以外,阿里推出的分布式事务管理Seata中的Saga模式也是基于状态机引擎的Saga实现,此处我引用官网的图片简单讲解下(就按流程图来理解):

图中CompensateReduceInventory与CompensateReduceBalance是补偿式事务,当ReduceInventory在理想情况下到达ReduceBalance后触发了CompensationTrigger(补偿触发器)时,CompensateReduceInventory与CompensateReduceBalance两个补偿式事务就会分别对ReduceInventory和ReduceBalance进行补偿,并最终返回Fail的状态。

可以看到状态机的存在是为了让我们更直观地看清楚Saga整个运行流程,能够很好地帮助开发人员进行理解,简化了业务逻辑让我们的目光更加注重如何改善隔离性问题。并且编排式的Saga参与者之间的依赖关系更加简单,不存在以来循环的问题。

基于Eventuate Tram框架的Saga模式讲解

此小章节为了读者能够更好地理解Saga模式在实际落地的应用,本人摘选了《微服务架构设计模式》中的一部分代码,并对代码进行精简优化以便保证读者可以从业务流程带入到代码运行程的连贯性,如果希望了解详细代码各位可以去看原著,百度也有免费的电子书。

我们先看一副状态机:

那么我们现在创建一个Saga编排器(CreateOrderSaga),用看图写作的思维,将整个状态机转化为一篇流水账式的代码:

public class CreateOrderSaga implements SimpleSaga<CreateOrderSagaState> {

  private Logger logger = LoggerFactory.getLogger(getClass());

  private SagaDefinition<CreateOrderSagaState> sagaDefinition;

  public CreateOrderSaga(OrderServiceProxy orderService, ConsumerServiceProxy consumerService, KitchenServiceProxy kitchenService,
AccountingServiceProxy accountingService) {
this.sagaDefinition =
step()
//补偿式事务,makeRejectOrderCommand创建“拒绝订单命令”,使用rderService.reject中配置的规则发送和回复
.withCompensation(orderService.reject, CreateOrderSagaState::makeRejectOrderCommand)
.step()
//这一步调用参与者,makeValidateOrderByConsumerCommand创建“根据消费者命令进行验证订单”的命令,使用consumerService.validateOrder中配置的规则发送和回复
.invokeParticipant(consumerService.validateOrder, CreateOrderSagaState::makeValidateOrderByConsumerCommand)
.step()
//这一步调用参与者,makeCreateTicketCommand创建“创建收据”的命令,使用kitchenService.create中配置的规则发送和回复
.invokeParticipant(kitchenService.create, CreateOrderSagaState::makeCreateTicketCommand)
//接受到上一步成功回复“CreateTicketReply”的命令后,执行handleCreateTicketReply的命令
.onReply(CreateTicketReply.class, CreateOrderSagaState::handleCreateTicketReply的命令)
//补偿式事务,makeCancelCreateTicketCommand创建“取消创建票据”的命令,使用kitchenService.cancel中的配置发送和回复
.withCompensation(kitchenService.cancel, CreateOrderSagaState::makeCancelCreateTicketCommand)
.step()
//这一步调用参与者,makeAuthorizeCommand创建“授权”命令,使用accountingService.authorize中的配置发送和回复
.invokeParticipant(accountingService.authorize, CreateOrderSagaState::makeAuthorizeCommand)
.step()
//这一步调用参与者,makeConfirmCreateTicketCommand创建“确认创建票”,使用kitchenService.confirmCreate中的配置发送和回复
.invokeParticipant(kitchenService.confirmCreate, CreateOrderSagaState::makeConfirmCreateTicketCommand)
.step()
//这一步调用参与者,makeApproveOrderCommand创建“批准订单”的命令,使用orderService.approve中的配置发送和回复
.invokeParticipant(orderService.approve, CreateOrderSagaState::makeApproveOrderCommand)
.build();
} @Override
public SagaDefinition<CreateOrderSagaState> getSagaDefinition() {
return sagaDefinition;
}
}

OrderServiceProxy适配器:

public class OrderServiceProxy {

  public final CommandEndpoint<RejectOrderCommand> reject = CommandEndpointBuilder
//要发送的“拒绝订单”的命令
.forCommand(RejectOrderCommand.class)
//发送命令到命令通讯通道:OrderServiceChannels.orderServiceChannel
.withChannel(OrderServiceChannels.orderServiceChannel)
//回复Saga编排器执行成功的应答
.withReply(Success.class)
.build(); public final CommandEndpoint<ApproveOrderCommand> approve = CommandEndpointBuilder
//执行批准订单的命令
.forCommand(ApproveOrderCommand.class)
.withChannel(OrderServiceChannels.orderServiceChannel)
.withReply(Success.class)
.build();
}

KitchenServiceProxy适配器:

public class KitchenServiceProxy {

  public final CommandEndpoint<CreateTicket> create = CommandEndpointBuilder
//要发送的“创建票据”的命令
.forCommand(CreateTicket.class)
//发送命令到命令通信通道:KitchenServiceChannels.kitchenServiceChannel
.withChannel(KitchenServiceChannels.kitchenServiceChannel)
//回复编排器CreateTicketReply的应答
.withReply(CreateTicketReply.class)
.build(); public final CommandEndpoint<ConfirmCreateTicket> confirmCreate = CommandEndpointBuilder
.forCommand(ConfirmCreateTicket.class)
.withChannel(KitchenServiceChannels.kitchenServiceChannel)
.withReply(Success.class)
.build();
public final CommandEndpoint<CancelCreateTicket> cancel = CommandEndpointBuilder
.forCommand(CancelCreateTicket.class)
.withChannel(KitchenServiceChannels.kitchenServiceChannel)
.withReply(Success.class)
.build(); }

命令传输的通信通道:

public class OrderServiceChannels {
public static final String orderServiceChannel = "orderService";
} public class KitchenServiceChannels {
public static final String kitchenServiceChannel = "kitchenService";
}

OrderCommandHandlers定义了接受到命令消息后的具体操作:

public class OrderCommandHandlers {

  @Autowired
private OrderService orderService; public CommandHandlers commandHandlers() {
return SagaCommandHandlersBuilder
//从此命令通道中获取命令消息
.fromChannel("orderService")
//接受到批准订单命令后执行approveOrder方法
.onMessage(ApproveOrderCommand.class, this::approveOrder)
//接受到拒绝订单命令后执行rejectOrder方法,后面都是如此,自己理解
.onMessage(RejectOrderCommand.class, this::rejectOrder)
.onMessage(BeginCancelCommand.class, this::beginCancel)
......
.build(); } public Message approveOrder(CommandMessage<ApproveOrderCommand> cm) {
long orderId = cm.getCommand().getOrderId();
orderService.approveOrder(orderId);
return withSuccess();
} public Message rejectOrder(CommandMessage<RejectOrderCommand> cm) {
long orderId = cm.getCommand().getOrderId();
orderService.rejectOrder(orderId);
return withSuccess();
}
.........
}

至此,整个关于Saga基础内容讲解完毕,大家可以根据上面的状态机和代码试着画一下Saga编排器的设计图,看看是否真地对Saga内容有进一步的了解。

结语

本文内容结构参照了《微服务架构设计模式》,是在对此书阅读以及结合网络上其他的相关案例总结成连贯,翻译更准确的知识点,其中因为个人水平问题可能存在歧义或疏漏,希望各位读者存在疑问或觉得文章中有需要改正的地方尽情指出。之后将在Saga的基础上讲解事件溯源和CQRS,敬请期待。

领域驱动模型DDD(三)——使用Saga管理事务的更多相关文章

  1. 领域驱动模型DDD(一)——服务拆分策略

    前言 领域驱动模型设计在业界也喊了几年口号了,但是对于很多"务实"的程序员来说,纸上谈"术"远比敲代码难得太多太多.本人能力有限,在拜读相关作品时既要隐忍书中晦 ...

  2. 领域驱动模型DDD(二)——领域事件的订阅/发布实践

    前言 凭良心来说,<微服务架构设计模式>此书什么都好,就是选用的业务过于庞大而导致代码连贯性太差,我作为读者来说对于其中采用的自研框架看起来味同嚼蜡,需要花费的学习成本实在是过于庞大,不仅 ...

  3. DDD 领域驱动设计-三个问题思考实体和值对象(续)

    上一篇:DDD 领域驱动设计-三个问题思考实体和值对象 说实话,整理现在这一篇博文的想法,在上一篇发布出来的时候就有了,但到现在才动起笔来,而且写之前又反复读了上一篇博文的内容及评论,然后去收集资料, ...

  4. 【tornado】系列项目(二)基于领域驱动模型的区域后台管理+前端easyui实现

    本项目是一个系列项目,最终的目的是开发出一个类似京东商城的网站.本文主要介绍后台管理中的区域管理,以及前端基于easyui插件的使用.本次增删改查因数据量少,因此采用模态对话框方式进行,关于数据量大采 ...

  5. 【tornado】系列项目(一)之基于领域驱动模型架构设计的京东用户管理后台

    本博文将一步步揭秘京东等大型网站的领域驱动模型,致力于让读者完全掌握这种网络架构中的“高富帅”. 一.预备知识: 1.接口: python中并没有类似java等其它语言中的接口类型,但是python中 ...

  6. 领域驱动设计(DDD)

    领域驱动设计(DDD)实现之路 2004年,当Eric Evans的那本<领域驱动设计——软件核心复杂性应对之道>(后文简称<领域驱动设计>)出版时,我还在念高中,接触到领域驱 ...

  7. 领域驱动设计(DDD:Domain-Driven Design)

    领域驱动设计(DDD:Domain-Driven Design) Eric Evans的"Domain-Driven Design领域驱动设计"简称DDD,Evans DDD是一套 ...

  8. python 全栈开发,Day116(可迭代对象,type创建动态类,偏函数,面向对象的封装,获取外键数据,组合搜索,领域驱动设计(DDD))

    昨日内容回顾 1. 三个类 ChangeList,封装列表页面需要的所有数据. StarkConfig,生成URL和视图对应关系 + 默认配置 AdminSite,用于保存 数据库类 和 处理该类的对 ...

  9. 关于领域驱动设计 DDD(Domain-Driven Design)

    以下旨在 理解DDD. 1.     什么是领域? 妈妈好是做母婴新零售的产品,应该属于电商平台,那么电商平台就是一个领域. 同一个领域的系统都有相同的核心业务. eg: 电商领域都有:商品浏览.购物 ...

随机推荐

  1. 修复ST-LINK V2下载器 | ST-LINK V2下载器烧录DAPLink固件

    前言 某宝上的STLINK V2下载器偶尔会坏掉,我们尝试修复一下 1.材料 (1)完好的STLINK V2下载器和坏掉的下载器各1个: (2)固件:https://gitee.com/Cai-Zi/ ...

  2. 四旋翼中的PID调节方法 | betaflight固件如何调节PID

    roll横滚,pitch俯仰,yaw航向 一.PID的作用概述 1.P产生响应速度和力度,是I和D的基础 过小响应慢(虽然无震荡) 过大会产生振荡且不断发散 2.D抑制过冲和振荡,抵抗外界的突发干扰, ...

  3. ESP8266 NodeMCU引脚说明,CH340和CP2102两款

    开发ESP8266 NodeMCU GPIO功能时,虽然知道ESP8266 NodeMCU丝印引脚编号如下图所示(CP2102款),但是和实际对应的GPIO编号完全不一样.   CP2102款 引脚说 ...

  4. 不会DRF?源码都分析透了确定不来看?

    目录 不会DRF?源码都分析透了确定不来看? 快速使用DRF写出接口 序列化和反序列化 drf快速使用 views.py serializer.py urls.py 在settings的app中注册 ...

  5. redis 持久化有几种方式?

    面试题 redis 的持久化有哪几种方式?不同的持久化机制都有什么优缺点?持久化机制具体底层是如何实现的? 面试官心理分析 redis 如果仅仅只是将数据缓存在内存里面,如果 redis 宕机了再重启 ...

  6. CopyOnWriteArrayList 可以用于什么应用场景?

    CopyOnWriteArrayList(免锁容器)的好处之一是当多个迭代器同时遍历和修改这 个列表时,不会抛出 ConcurrentModificationException.在 CopyOnWri ...

  7. java常用方法集合

    1.获取当前日期 // 获取当前日期 public Date getDate(int num) { Calendar cal = new GregorianCalendar(); cal.setTim ...

  8. memcached 如何处理容错的?

    不处理! 在 memcached 节点失效的情况下,集群没有必要做任何容错处理.如 果发生了节点失效,应对的措施完全取决于用户.节点失效时,下面列出几种方 案供您选择: 忽略它! 在失效节点被恢复或替 ...

  9. Container 在微服务中的用途是什么?

    容器是管理基于微服务的应用程序以便单独开发和部署它们的好方法.您可以将 微服务封装在容器映像及其依赖项中,然后可以使用它来滚动按需实例的微服务, 而无需任何额外的工作.

  10. Spring常用的注入方式有哪些?

    Spring通过DI(依赖注入)实现IOC(控制反转),常用的注入方式主要有三种:构造方法注入,setter注入,基于注解的注入. 参考链接:Spring常用的三种注入方式