文章

游戏服务器工程实践五:业务逻辑的事务性

游戏服务器工程实践五:业务逻辑的事务性

声明:gpt-6-sol 也参与了文章创作。

一、道具交易的事务性

游戏中的一些逻辑是需要有事务性的,否则会出现半完成状态。举个例子,玩家 A 用一件 X 换 B 的 100 金币。

如果是小规模数据,数据库事务的代价不高,那么数据库事务就可以解决问题了。但如果是大规模数据,数据库事务的代价太高,就要避免使用了。通常也不推荐使用数据库事务,往往一开始小规模的数据,到后面都可能变得庞大。这时候可以使用一种被称为 Saga 的分布式事务模型来实现事务。

Saga 并非什么缩写,而是来自于 1987 年 Hector Garcia-Molina 和 Kenneth Salem 提出的论文《Sagas》,名字借自北欧长篇英雄史诗。

Saga 将长事务分解为多个独立提交的子事务,并通过补偿处理失败情况[1]。在跨服务实现中,可以进一步区分可补偿步骤、不可取消的分界步骤和后续可重试步骤[2]。

Saga 属于”分布式一致性/分布式事务模型“,最初用于长事务,后来广泛应用于跨服务流程。它不是数据库事务,也不提供 ACID 的全局隔离;它通过状态机、幂等、重试和补偿,让多个独立服务最终收敛。

回到我们的例子,可以这么去设计:

步骤正向操作失败后的补偿
T1冻结A的一件X解除X的冻结
T2冻结B的100金币解除金币冻结
T3持久化交易提交决定提交后不再普通取消
T4结B入账一件X失败则幂等重试
T5给A入账100金币失败则幂等重试

如果 B 金币不足,则 T2 失败,就解除 A 的冻结,这是补偿恢复。
如果 T3 已经提交,而给 B 入账一件 X 时服务崩溃,就重试给 B 入账一件 X,不能因为一次超时就把交易取消,这是向前恢复。 给 A 入账金币同理。T3 在这个设计中是“不再取消”的分界点,常称为 pivot。[2]

需要说明的是,saga 不等于 “先冻结,再统一提交”,上面的例子额外采用了接近 TCC 的 “预留/确认/取消” 设计。saga 的核心是 “独立提交的步骤 + 业务补偿”;预留只是帮助这个交易避免中间状态被消费的一种办法,也就是避免后续可能发生的补偿失败。

T4 / T5 是可以并行执行的,T4 成功而 T5 未成功,不会影响 B 使用 X。同理 T4 未成功而 T5 成功不影响 A 使用 100 金币。

二、更接近 saga 的例子

可以用 “旅行套餐预订” 举例:用户一次购买机票、酒店和租车,但它们分属三个独立系统。假设机票和酒店都支持取消退款。

这笔 saga 依次执行:

步骤正向操作对应补偿
T1购买机票,完成扣款和出票取消机票,办理退款
T2预订酒店,完成扣款并生成有效订单取消酒店订单,办理退款
T3预订租车,生成有效订单取消租车订单

每一步完成时,业务已经真正发生。比如 T1 成功后,用户已经有了一张有效机票,不是在等待统一确认的预留状态。

假设机票、酒店都成功了,但租车系统明确返回“无车可用”,而这个套餐要求三项全部满足:

1
2
3
4
5
6
7
购买机票成功 -> 预订酒店成功 -> 租车失败
                               ↓
                           取消酒店并退款
                               ↓
                           取消机票并退款
                               ↓
                            套餐已取消

这里的 “取消、退款” 就是补偿。原来的扣款和订单记录仍然存在,只是多出了退款记录,订单状态变为已取消。它不会抹去已经发出的出票短信,也不会把其他用户期间产生的订单恢复到旧状态。

如果取消酒店时服务崩溃,系统应持久化 “正在补偿酒店” 的进度,恢复后继续。 若退款已经成功但响应丢失,按原退款请求号查询或幂等重试,不能再发起一笔新退款。

它与 TCC 的区别在于:

  • 这个 saga:先真正出票、真正订房;后续失败,再取消这些已经失效的业务。
  • TCC:先预留机票、房间和车辆;全部预留成功后,再分别确认,否则释放预留。

saga 因而特别依赖补偿是否在业务上可行。如果机票不可退,就不能把它放在前面,然后假定后面失败时总能撤销;需要调整步骤顺序,或明确接受退款损失、人工处理等结果。

三、再谈 saga

微服务架构中,各个服务的数据库往往是独立的,涉及到跨服务的数据一致时,是没法直接使用数据库的事务,就需要用到 saga。

saga 相当于将事务拆分为一系列的本地事务来管理[2]。

从设计和落地的角度,要实施 saga,需要有如下的前提:

前提具体要求
1.业务能够拆分能拆成多个独立提交的本地步骤;每一步成功后都有明确、可持久化的结果
2.撤销路径可行对需要撤销的步骤,业务接受其补偿结果。例如退款,而非要求捐款从未发生
3.能接受中间状态整体完成前,可能出现“机票已买,酒店未订”。若这些状态会被其他操作利用,必须增加冻结、版本检查等并发控制。
4.不可逆步骤有后续策略一旦跨过不可取消的分界点,剩余步骤必须有可靠的完成路径,不能再遇到普通业务失败就要求整体撤销。
5.执行进度可恢复保存流程、步骤及补偿所需的信息,例如订单号、退款金额。协调者崩溃后,其他实例能接着处理。
6.重试不会重复产生副作用正向操作和补偿都要支持幂等,或提供等价的去重、结果查询机制。超时不能直接当作失败。
7.有持续恢复能力临时故障后能继续重试;补偿失败也有人处理。永久故障或无法自动解决的情况,要有告警、对账与人工处置路径。

前3个决定了能否使用 saga,后4个决定了能否可靠的实现。

四、总结

Saga 是一种处理长事务的事务模型,由 Hector Garcia-Molina 和 Kenneth Salem 在 1987 年的 《Sagas》中提出。其核心思想是:将长事务拆成一系列可以独立提交、与其他事务交错执行的小事务,从而避免长期占用数据库资源;如果流程中途放弃,则通过预先定义的补偿事务,在业务语义上撤销已经提交的步骤,而不是将数据库恢复到原来的快照。原论文讨论了两种恢复方向:backward recovery(向后恢复),即补偿已执行的步骤;forward recovery(向前恢复),即继续执行尚未完成的步骤,并进一步讨论了二者结合的恢复方式。[1]

现代微服务架构将这一思想用于跨服务业务流程:各服务独立提交本地事务,通过消息或调用推进流程,并借助持久化进度、幂等操作和重试机制应对故障。常见实现方式是由协调器(orchestrator)统一推进的编排式(Orchestration),以及服务通过事件相互触发的编舞式(Choreography)。微软的架构指南还将流程中的步骤区分为可补偿事务、pivot transaction(不可回退的分界步骤) 和可重试事务:在 pivot 成功之前,流程仍可通过补偿退出;一旦 pivot 成功,后续步骤就应通过重试和恢复推进完成。Pivot 是现代 Saga 设计中组织恢复边界的一种方式,并非原始 Saga 定义要求必须存在的全局提交点。Saga 通常不提供跨步骤的全局隔离,也不意味着所有数据同时以身试法;其正确性依赖业务上可行的补偿、可靠的恢复机制,以及对中间状态和并发操作的明确约束。[2]

saga 论文链接: https://www.cs.princeton.edu/techreports/1987/070.pdf 。

五、参考

[1] GARCIA-MOLINA H, SALEM K. Sagas[C]//Proceedings of the 1987 ACM SIGMOD International Conference on Management of Data. New York: ACM, 1987: 249-259. DOI: 10.1145/38713.38742.
[2] MICROSOFT. Saga distributed transactions pattern[EB/OL]. [2026-09-26]. https://learn.microsoft.com/en-us/azure/architecture/patterns/saga.