Pavel Simonov

Город IT · 2024 · Pavel Simonov

保证一致性的分布式事务:神话还是现实?

本文根据演示文稿、提纲、预告和源代码重建。没有可用的演讲录像,因此并非逐字记录。内容顺序对应2024年演讲的23张幻灯片。

原始幻灯片图片保留俄语。文字稿可按所选语言阅读;代码和 API 标识符不翻译。

目录

1 / 23分布式事务

原始幻灯片 1: 分布式事务
原始幻灯片 · RU

重建文本

大家好!我是Citeck的系统架构师Pavel Simonov。今天介绍我们如何实现分布式事务,会遇到哪些困难,以及如何处理。

先从两阶段提交讲起,再介绍事务动作和事件监听器。目标是让跨微服务的操作更容易编写,同时保持数据一致。

2 / 23事务边界

原始幻灯片 2: 事务边界
原始幻灯片 · RU

重建文本

大家已经习惯了普通事务。在方法上加一个注解,就期待所有修改一起保存,出错时全部回滚。只有一个数据库时,这很直观。

图中我们启动事务,执行几次修改,最后Commit。这些修改需要一起保存。

3 / 23回滚

原始幻灯片 3: 回滚
原始幻灯片 · RU

重建文本

如果中途出错呢?比如前两次修改成功,第三次失败。执行Rollback,数据库就回到这些修改之前的状态。我们不必自己判断哪些写入成功,再逐一撤销。

不过回滚也有边界。邮件已经发出,数据库回滚无法把它收回来。后面会再讲这类动作。

4 / 23多个数据库

原始幻灯片 4: 多个数据库
原始幻灯片 · RU

重建文本

现在假设数据在两个微服务中,各有自己的数据库。我们在DB1和DB2启动事务、完成修改。接下来怎么办?

依次提交时,第一个数据库可能成功,第二个却报错。第一个Commit已经完成,普通Rollback无法撤销它,数据就对不上了。

5 / 23回到单体?

原始幻灯片 5: 回到单体?
原始幻灯片 · RU

重建文本

可以保留一个数据库,回到单体。这样所有修改又能放进同一个事务,和前面的图一样。

对某些任务,这很合适。但如果操作已经涉及多个独立服务,我们仍要约定怎样把修改一起保存。

6 / 23Saga与补偿

原始幻灯片 6: Saga与补偿
原始幻灯片 · RU

重建文本

另一种选择是Saga。各服务分别保存修改。如果后续步骤失败,就执行补偿:用另一个操作修正前一步的结果。

补偿逻辑需要自己写,取决于业务:撤销创建的文档、恢复状态、处理其他影响。长流程可以用这种办法,但这里希望保留完整执行或回滚修改的便利。

7 / 23实际场景

原始幻灯片 7: 实际场景
原始幻灯片 · RU

重建文本

哪些场景需要它?比如Camunda管理流程,文档的业务数据在另一个服务。完成任务时还要修改文档,我们希望两项修改一起保存。

集成或使用平台业务数据的自定义微服务也有同样问题。用户看到的是一次操作,一条请求却可能涉及多个数据库。

8 / 232PC参与者

原始幻灯片 8: 2PC参与者
原始幻灯片 · RU

重建文本

为此我们使用两阶段提交,也就是2PC。XA标准描述怎样与事务资源交互。先看看参与者。

事务管理器TM管理事务。XA资源能准备修改,再提交或回滚,例如支持XA的数据库。还需要TM Repo,保存协调信息,以便故障后知道怎样处理未完成事务。

9 / 23准备与提交

原始幻灯片 9: 准备与提交
原始幻灯片 · RU

重建文本

先修改两个资源。结束时不立即提交,而是询问每个资源:能保存你这一部分吗?这就是Prepare阶段。

只要有一个没准备好,就回滚。全部准备好,则保存提交决定,再让各资源Commit。完成后清理协调信息。

故障时,关键是提交决定是否已经保存。保存后,恢复必须完成这个决定,不能随意改成回滚。等待期间,已准备资源可能持有锁,需要考虑这个代价。

10 / 23ecos-model中的协调器

原始幻灯片 10: ecos-model中的协调器
原始幻灯片 · RU

重建文本

来看我们的服务。请求通过ecos-gateway进入,涉及ecos-process和ecos-model,各自有保存业务数据的PostgreSQL。

gateway把提交协调交给ecos-model,事务管理器的仓库也在那里。ecos-model负责判断哪些资源参与、是否就绪,以及应执行什么决定。

11 / 23HTTP请求上下文

原始幻灯片 11: HTTP请求上下文
原始幻灯片 · RU

重建文本

其他服务怎么知道调用处于事务中?HTTP请求会携带事务标识,让服务知道这次调用属于哪项工作。

还会传递实例路由。图中emodel对应wbag7dya54yq,eproc对应za7iyygilrr4。服务有多个实例时,后续调用必须到达已经保存本事务状态的那个实例。

12 / 23路由传播

原始幻灯片 12: 路由传播
原始幻灯片 · RU

重建文本

响应也要传播路由。假设App A-0调用App B-0,B再调用App C-1。A还不知道C的哪个实例参与了操作。

所以B-0返回App C → C-1。A之后在同一事务里调用C,就能找到正确实例。通过负载均衡随意选一个,可能选到完全不了解这个事务的实例。

13 / 23响应元数据

原始幻灯片 13: 响应元数据
原始幻灯片 · RU

重建文本

响应返回三项信息:事务资源、路由、事务动作。路由已经讲过,资源列表告诉调用方,结束事务时要把谁包括进来。

动作让服务提出:这段代码要在特定时刻执行,比如提交前或提交后。接下来看看为什么需要它。

14 / 23为何需要动作

原始幻灯片 14: 为何需要动作
原始幻灯片 · RU

重建文本

保存前可能要重新计算权限,保存后要发布事件或发邮件。如果立即发邮件,而事务随后回滚,收件人就会收到一个最终没发生的变更通知。

因此先注册动作,在合适阶段执行:提交前计算权限,成功提交后发送通知。

15 / 23动作描述

原始幻灯片 15: 动作描述
原始幻灯片 · RU

重建文本

描述一个动作需要什么?注册它的应用、动作标识、执行时机和顺序。

时机有三种:before commit、after commit、after rollback。这样可以收集多个服务的动作,按所需顺序执行。

16 / 23线程复用

原始幻灯片 16: 线程复用
原始幻灯片 · RU

重建文本

另一个难点是线程。A调用B后等待响应,但B处理时又可能同步回调A,而且仍在同一个事务中。

A已经在工作,有被占用的线程及其上下文。特别是回到同一个业务流程时,必须考虑这些状态。来看一个例子。

为此我们使用WebApiThreadChain。原始线程等待HTTP响应时,可以处理队列中的嵌套调用。同一事务中的回调到达A后,可以交给这个等待中的线程。它使用已经打开的流程上下文完成工作,收到B的响应后再继续原来的调用。

17 / 23BPMN示例

原始幻灯片 17: BPMN示例
原始幻灯片 · RU

重建文本

这里有两个子流程。第一个接手任务,设置“一级支持处理中”,随后取消SLA 1通知并结束SLA 1。

但状态变化也启动第二条分支:检查转移条件、通知发起人、处理请求。一次状态修改,在第一条分支结束前就把我们带回流程引擎。

18 / 23回调与转移条件

原始幻灯片 18: 回调与转移条件
原始幻灯片 · RU

重建文本

按服务来看,ecos-process调用ecos-model设置状态。ecos-model发送“状态已变更”事件,我们回到ecos-process,在同一线程继续流程。

图中转移条件检查失败。这时需要知道哪个流程正在执行、属于哪个事务。如果丢失流程执行上下文,仅有事务标识还不够。

19 / 23事务事件监听器

原始幻灯片 19: 事务事件监听器
原始幻灯片 · RU

重建文本

这些情况需要事务事件监听器。另一个服务若必须在当前操作中响应数据创建、修改或删除,我们就在当前事务内通过HTTP(S)同步调用它。

为了让发送方知道该调用谁,我们在Zookeeper中维护订阅注册表。应用注册需要的事件、过滤条件和处理器所需的属性,并注明监听器是否参与事务。服务监听注册表的变化,及时更新接收方列表。

处理器可在事务结束前完成自己的工作。确认全部数据已保存的通知,则通过提交后的独立动作发送。

20 / 23负载测试条件

原始幻灯片 20: 负载测试条件
原始幻灯片 · RU

重建文本

再看负载测试。使用JMeter,非200响应或执行超过10秒都算错误,每个线程使用独立用户。

系统预先有100 000用户和10 000 000合同,以10 000用户身份持续施加一小时负载。

21 / 23负载场景

原始幻灯片 21: 负载场景
原始幻灯片 · RU

重建文本

用户执行不同操作:登录并打开首页20%,查看合同列表30%,打开带全部组件的合同页面30%。

还包括活动任务列表30%、创建文档20%、启动流程10%、执行任务50%。这些是参与各场景的用户比例,一个用户可以执行多种操作,所以无需相加为100%。

22 / 23测试结果

原始幻灯片 22: 测试结果
原始幻灯片 · RU

重建文本

一小时共20 447 914请求,221次错误或超过时限。幻灯片舍入为0.00%,更精确是约0.00108%。

平均响应时间21.88毫秒,中位数6毫秒,90%请求低于18毫秒。

23 / 23源码与结束

原始幻灯片 23: 源码与结束
原始幻灯片 · RU

重建文本

想看代码如何实现,可以通过链接查看分布式事务库的源码。

起点很简单:多个服务的修改要一起保存。为此需要2PC、传播上下文和路由,再配合动作和监听器。这让跨微服务代码也能获得单个数据库带来的便利。谢谢大家!

https://github.com/Citeck/ecos-txn-lib