Java 中的 Saga 模式:掌握分布式系统中的长时运行事务
Saga 设计模式的意图
以容错和可靠的方式管理和协调跨多个服务的分布式事务。
Saga 模式的详细解释以及真实世界的例子
真实世界的例子
想象一家旅行社为客户协调一个度假套餐。该套餐包括预订航班、预订酒店房间和租车。每个预订都由不同的服务提供商管理。如果航班预订成功,但酒店已满,旅行社需要取消航班并通知客户。这样可以确保客户不会只得到一个部分的度假套餐。在 Saga 模式中,这种情况通过一系列协调的事务来管理,其中包括补偿操作(例如取消航班)以保持一致性。
通俗地说
Java 中的 Saga 模式使用一系列事件和补偿操作来协调跨微服务的分布式事务,以确保数据一致性和容错性。
维基百科说
长时运行事务(也称为 Saga 交互模式)是计算机数据库事务,它避免对非本地资源进行锁定,使用补偿来处理故障,可能聚合更小的 ACID 事务(也称为原子事务),并且通常使用协调器来完成或中止事务。与 ACID 事务中的回滚相反,补偿恢复原始状态或等效状态,并且是特定于业务的。例如,预订酒店的补偿操作是取消该预订。
Java 中 Saga 模式的编程示例
Saga 设计模式是一系列本地事务,其中每个事务更新单个服务中的数据。它在微服务架构中特别有用,其中每个服务都有自己的数据库。Saga 模式确保跨服务的數據一致性和容错性。以下是 Saga 模式的关键组件
Saga:Saga 是一系列本地事务,每个事务称为一个章节。Saga 管理这些事务的顺序,确保每个事务按正确的顺序执行,并在事务失败时回滚 Saga。
章节:Saga 中的每个章节代表一个本地事务。一个章节有一个名称,一个结果(可以是
INIT
、SUCCESS
或ROLLBACK
),以及一个输入值。Chapter
类提供方法来获取和设置这些属性。服务:服务执行一个本地事务。它处理章节的输入值,并返回一个
ChapterResult
。如果事务失败,它将章节的状态设置为ROLLBACK
。服务发现:此组件负责发现可用的服务并执行 Saga。它按顺序处理 Saga 中的每个章节。如果一个章节失败,Saga 将被回滚。
Saga 结果:Saga 的结果可以是
PROGRESS
、FINISHED
或ROLLBACKED
。这由Saga
类的getResult
方法确定。
在真实世界的应用中,Service
类将包含执行本地事务和处理故障的逻辑。Saga
类将管理本地事务的顺序,确保每个事务按正确的顺序执行,并在事务失败时回滚 Saga。
代码片段 1:创建 Saga
使用 Saga 模式的第一步是创建一个 Saga。Saga 是一系列章节,每个章节代表一个本地事务。Saga
类提供方法来添加章节,以及检查是否存在某个章节。
// Create a new Saga
Saga saga = Saga.create();
代码片段 2:向 Saga 中添加章节
Saga 中的每个章节代表一个本地事务。我们可以使用 chapter
方法将章节添加到 Saga 中。
// Add chapters to the Saga
saga.chapter("init an order");
saga.chapter("booking a Fly");
saga.chapter("booking a Hotel");
saga.chapter("withdrawing Money");
代码片段 3:为章节设置输入值
Saga 中的每个章节都可以有一个输入值。我们可以使用 setInValue
方法为最后一个添加的章节设置输入值。
// Set input values for the chapters
saga.chapter("init an order").setInValue("good_order");
代码片段 4:执行 Saga
我们可以使用服务来执行 Saga。该服务将按顺序处理 Saga 中的每个章节。如果一个章节失败,Saga 将被回滚。
// Execute the Saga
var service = sd.findAny();
var goodOrderSaga = service.execute(saga);
代码片段 5:检查 Saga 的结果
我们可以使用 getResult
方法检查 Saga 的结果。该方法返回 Saga 的结果,可以是 PROGRESS
、FINISHED
或 ROLLBACKED
。
// Check the result of the Saga
SagaResult result = goodOrderSaga.getResult();
SagaApplication
类有一个用于运行示例的 main
方法。
@Slf4j
public class SagaApplication {
public static void main(String[] args) {
var sd = serviceDiscovery();
var service = sd.findAny();
var goodOrderSaga = service.execute(newSaga("good_order"));
var badOrderSaga = service.execute(newSaga("bad_order"));
LOGGER.info("orders: goodOrder is {}, badOrder is {}",
goodOrderSaga.getResult(), badOrderSaga.getResult());
}
private static Saga newSaga(Object value) {
return Saga
.create()
.chapter("init an order").setInValue(value)
.chapter("booking a Fly")
.chapter("booking a Hotel")
.chapter("withdrawing Money");
}
private static ServiceDiscoveryService serviceDiscovery() {
var sd = new ServiceDiscoveryService();
return sd
.discover(new OrderService(sd))
.discover(new FlyBookingService(sd))
.discover(new HotelBookingService(sd))
.discover(new WithdrawMoneyService(sd));
}
}
- Saga:
SagaApplication
使用Saga.create()
方法创建一个新的 Saga。然后,它使用chapter
方法向 Saga 添加章节,并使用setInValue
方法为每个章节设置输入值。 - 服务:
SagaApplication
使用服务来执行 Saga 中的章节。每个服务代表一个本地事务。SagaApplication
使用ServiceDiscoveryService
来发现可用的服务并执行 Saga。 - 服务发现:
ServiceDiscoveryService
用于发现可用的服务。SagaApplication
使用它来找到一个服务并执行 Saga。 - Saga 执行:
SagaApplication
使用服务的execute
方法来执行 Saga。它创建两个 Saga,一个用于正常订单,另一个用于错误订单,并执行它们。 - Saga 结果:
SagaApplication
使用getResult
方法检查 Saga 的结果。它记录了正常订单 Saga 和错误订单 Saga 的结果。
总而言之,SagaApplication
创建一个 Saga,向其中添加章节,为每个章节设置输入值,发现服务,使用服务执行 Saga,并检查 Saga 的结果。
运行示例会生成以下控制台输出
11:32:17.779 [main] INFO com.iluwatar.saga.choreography.Service -- The chapter 'init an order' has been started. The data good_order has been stored or calculated successfully
11:32:17.782 [main] INFO com.iluwatar.saga.choreography.Service -- The chapter 'booking a Fly' has been started. The data good_order has been stored or calculated successfully
11:32:17.782 [main] INFO com.iluwatar.saga.choreography.Service -- The chapter 'booking a Hotel' has been started. The data good_order has been stored or calculated successfully
11:32:17.782 [main] INFO com.iluwatar.saga.choreography.Service -- The chapter 'withdrawing Money' has been started. The data good_order has been stored or calculated successfully
11:32:17.782 [main] INFO com.iluwatar.saga.choreography.Service -- the saga has been finished with FINISHED status
11:32:17.782 [main] INFO com.iluwatar.saga.choreography.Service -- The chapter 'init an order' has been started. The data bad_order has been stored or calculated successfully
11:32:17.782 [main] INFO com.iluwatar.saga.choreography.Service -- The chapter 'booking a Fly' has been started. The data bad_order has been stored or calculated successfully
11:32:17.782 [main] INFO com.iluwatar.saga.choreography.Service -- The chapter 'booking a Hotel' has been started. The data bad_order has been stored or calculated successfully
11:32:17.782 [main] INFO com.iluwatar.saga.choreography.Service -- The chapter 'withdrawing Money' has been started. But the exception has been raised.The rollback is about to start
11:32:17.782 [main] INFO com.iluwatar.saga.choreography.Service -- The Rollback for a chapter 'booking a Hotel' has been started. The data bad_order has been rollbacked successfully
11:32:17.782 [main] INFO com.iluwatar.saga.choreography.Service -- The Rollback for a chapter 'booking a Fly' has been started. The data bad_order has been rollbacked successfully
11:32:17.782 [main] INFO com.iluwatar.saga.choreography.Service -- The Rollback for a chapter 'init an order' has been started. The data bad_order has been rollbacked successfully
11:32:17.782 [main] INFO com.iluwatar.saga.choreography.Service -- the saga has been finished with ROLLBACKED status
11:32:17.782 [main] INFO com.iluwatar.saga.choreography.SagaApplication -- orders: goodOrder is FINISHED, badOrder is ROLLBACKED
这是一个关于如何使用 Saga 设计模式的基本示例。在真实世界的应用中,Saga
类将管理本地事务的顺序,确保每个事务按正确的顺序执行,并在事务失败时回滚 Saga。
何时在 Java 中使用 Saga 模式
- 当您有一个跨多个微服务的复杂事务时。
- 当您需要确保跨服务的數據一致性,而无需使用传统的两阶段提交时。
- 当您需要以异步方式处理长时运行事务时。
Java 中 Saga 模式的真实世界应用
- 管理订单、库存和支付服务的电子商务平台。
- 在跨多个服务的账户借记和贷记之间进行协调的银行系统。
- 协调航班、酒店和租车的旅行预订系统。
Saga 模式的优缺点
优点
- 提高容错性和可靠性。
- 由于服务解耦,可扩展性得到提高。
- 处理长时运行事务的灵活性。
权衡
- 处理补偿事务的复杂性增加。
- 需要精心设计,以处理部分故障和回滚场景。
- 由于异步特性,可能会出现延迟。
相关的 Java 设计模式
- 事件溯源:用于将状态更改捕获为一系列事件,这些事件可以通过提供状态更改的历史记录来补充 Saga 模式。
- 命令查询责任分离 (CQRS):可以与 Saga 模式结合使用,将命令和查询责任分离,从而提高可扩展性和容错性。