1. 引言

在典型的微服务架构中,单个业务用例往往跨越多个微服务,每个服务都有自己的本地数据存储和本地事务。当涉及多个事务且微服务数量庞大时,就需要处理跨服务的事务管理问题。Saga模式正是为解决这类分布式事务问题而生的。该模式最早由Hector Garcia Molina和Kenneth Salems在1987年提出,定义为一系列可相互交织的事务序列。

本文将深入探讨分布式事务管理的挑战、基于编排的Saga模式如何解决这些问题,并使用Spring Boot 3和Orkes Conductor(开源编排平台Conductor OSS的企业版)实现一个Saga模式示例。

2. 分布式事务管理的挑战

如果实现不当,分布式事务会带来诸多挑战。在分布式事务中,每个微服务都有独立的本地数据库,这种模式通常称为"每个服务一个数据库"(Database per Service)。

例如:

  • 某个微服务可能因MySQL的性能特性选择它
  • 另一个微服务可能因PostgreSQL的优势选择它

在这种模型中,每个服务执行自己的本地事务来完成整个应用事务。这个整体事务被称为分布式事务。

分布式事务有多种处理方式,传统方法包括两阶段提交(2PC)和ACID事务(原子性、一致性、隔离性、持久性),但它们都存在多语言持久化、最终一致性、延迟等挑战

3. 理解Saga模式

Saga模式是一种架构模式,用于实现一系列本地事务,帮助维护不同微服务间的数据一致性。

每个本地事务更新自己的数据库后,通过发布消息或事件触发下一个事务。如果某个本地事务失败,saga会执行一系列补偿事务来回滚之前事务所做的更改。这确保了即使事务失败,系统仍能保持一致性

以订单管理系统为例,流程包含从下单到配送的多个步骤: 订单处理流程

流程包括:

  1. 用户下单
  2. 库存检查
  3. 支付处理
  4. 配送服务
  5. 通知服务

如果支付失败,系统必须执行补偿事务回滚之前步骤(如撤销支付、取消订单)。Saga模式能在任何阶段处理失败并补偿已完成的事务

Saga模式有两种实现方式:

编排模式(Choreography)

  • 各微服务消费事件、执行活动并将事件传递给下一个服务
  • 没有中央协调器,服务间通信更复杂 编排模式图

协同模式(Orchestration)

  • 所有微服务连接到中央协调器
  • 协调器按预定义顺序协调服务,完成应用流程
  • 便于可见性、监控和错误处理 协同模式图

4. 为什么选择基于协同的Saga模式?

编排模式的去中心化特性使服务交互的管理和监控更具挑战性。缺乏中央协调和可见性会增加复杂性,使应用更难维护。

4.1. 编排模式的局限性

构建分布式应用时,编排模式存在明显缺陷:

  • 紧耦合:服务直接连接,任何服务变更可能影响所有连接服务
  • 分布式事实源:跨微服务维护应用状态使流程跟踪复杂化,可能需要额外系统整合状态信息
  • 故障排查困难:流程分散在不同服务中,定位和修复问题耗时更长
  • 测试环境复杂:微服务相互连接使开发者测试困难
  • 维护成本高:服务演进时引入新版本需添加条件逻辑,导致"分布式单体"问题

4.2. 协同模式的优势

协同模式在构建分布式应用时具有显著优势:

  • 分布式系统内协调事务:中央协调器按预定义方式管理微服务执行,确保应用一致性
  • 补偿事务Saga模式在失败时能执行补偿事务,回滚已完成事务
  • 异步处理:微服务可独立处理活动,协调器管理异步操作的通信和排序
  • 可扩展性:通过添加或修改服务即可调整应用,不影响整体架构
  • 增强可见性和监控提供跨分布式应用的集中可见性,快速识别和解决问题
  • 加速上市时间:简化服务重连和新流程创建,支持快速适应变化

总结:基于协同的Saga模式为微服务架构提供了协调、一致且可扩展的分布式事务实现方式,通过补偿事务处理故障,是构建健壮分布式应用的强大模式。

5. 使用Orkes Conductor实现Saga协同模式

下面通过一个实际示例展示如何使用Orkes Conductor实现Saga模式。考虑一个包含以下服务的订单管理系统:

  • OrderService:处理初始订单创建(添加购物车商品、指定数量、初始化结账)
  • InventoryService:检查并确认商品可用性
  • PaymentService:安全处理支付流程(支持多种支付方式)
  • ShipmentService:准备商品配送(打包、生成运单、启动配送)
  • NotificationService:向用户发送订单更新通知

我们将使用Orkes Conductor和Spring Boot 3复制这个流程。

5.1. 外卖配送应用

使用Saga模式构建的外卖应用在Conductor UI中如下所示: 外卖配送工作流
在Playground中查看

工作流进展:

  1. 用户下单触发一系列工作器任务
    • 添加商品到购物车(order_food
    • 检查餐厅商品可用性(check_inventory
    • 支付处理(make_payment
    • 配送处理(ship_food
  2. 进入fork-join任务处理通知服务:
    • 通知配送员
    • 通知用户

5.2. 运行应用

前置条件

Orkes Conductor设置选项

本示例使用Playground。核心代码片段如下:

@AllArgsConstructor
@Component
@ComponentScan(basePackages = {"io.orkes"})
public class ConductorWorkers {
    
    @WorkerTask(value = "order_food", threadCount = 3, pollingInterval = 300)
    public TaskResult orderFoodTask(OrderRequest orderRequest) {
        String orderId = OrderService.createOrder(orderRequest);
        TaskResult result = new TaskResult();
        Map<String, Object> output = new HashMap<>();

        if(orderId != null) {
            output.put("orderId", orderId);
            result.setOutputData(output);
            result.setStatus(TaskResult.Status.COMPLETED);
        } else {
            output.put("orderId", null);
            result.setStatus(TaskResult.Status.FAILED);
        }

        return result;
    }
}

运行步骤

  1. 克隆项目

  2. 更新application.properties文件(使用生成的访问密钥):

    conductor.server.url=https://play.orkes.io/api
    conductor.security.client.key-id=<key>
    conductor.security.client.secret=<secret>
    
    • 使用Playground时conductor.server.url保持不变
    • 替换key-idsecret为实际密钥
    • 为工作器添加访问工作流和任务的权限
    • 默认conductor.worker.all.domain为'saga',建议修改为其他名称避免冲突
  3. 运行应用:

    gradle bootRun
    
  4. 调用API创建订单:

    curl --location 'http://localhost:8081/triggerFoodDeliveryFlow' \
     --header 'Content-Type: application/json' \
     --data '{
         "customerEmail": "[email protected]",
         "customerName": "Tester QA",
         "customerContact": "+1(605)123-5674",
         "address": "350 East 62nd Street, NY 10065",
         "restaurantId": 2,
         "foodItems": [
             {
                 "item": "Chicken with Broccoli",
                 "quantity": 1
             },
             {
                 "item": "Veggie Fried Rice",
                 "quantity": 1
             },
             {
                 "item": "Egg Drop Soup",
                 "quantity": 2
             }
         ],
         "additionalNotes": [
             "Do not put spice.",
             "Send cutlery."
         ],
         "paymentMethod" : {
             "type": "Credit Card",
             "details": {
                 "number": "1234 4567 3325 1345",
                 "cvv": "123",
                 "expiry": "05/2022"
             }
         },
         "paymentAmount": 45.34,
         "deliveryInstructions": "Leave at the door!"
      }'
    

成功后返回工作流ID,使用该ID可在Conductor UI中可视化应用流程(导航至"Executions > Workflow"搜索ID)。

示例执行结果: 完成的工作流执行

5.3. 补偿流程

外卖应用的补偿事务可视化: 补偿事务图

在Orkes Conductor中定义工作流时,可通过failureWorkflow指定主应用失败时触发的补偿工作流:

"failureWorkflow": "<失败时运行的工作流名称>",

补偿工作流在失败时回滚更改: Conductor中的补偿工作流
在Playground中查看

当主应用中任何服务失败时触发此工作流。例如支付失败(余额不足)时: 支付失败时的补偿流程

系统会:

  1. 取消支付
  2. 取消订单
  3. 向用户发送失败通知

这就是使用Orkes Conductor回滚已完成事务的方式,确保应用一致性。遇到Conductor相关问题可加入Slack社区咨询。

6. 结论

本文成功使用Orkes Conductor和Java Spring Boot 3开发了一个订单管理应用,实现了Saga模式。Orkes Conductor支持所有主流云平台:AWS、Azure和GCP。

本文源代码可在GitHub获取


原始标题:Saga Pattern in a Microservices Architecture | Baeldung