几乎所有的信息管理系统都会涉及到事务,事务的目的是为了保证数据的一致性,这里说的一致性是数据库状态的一致性。
说到数据库状态的一致性,相信大家都会想到 ACID :
在单体架构中,通常是一套程序对应一个数据库,事务基于数据库本身的能力,如果你在 .NET Core 中使用 dapper 或 sqlsugar ,可以很容易进行事务的处理,可以参考下面文档:
https://dapper-tutorial.net/transaction
https://www.donet5.com/Home/Doc?typeId=1183
但是,在微服务架构,分布式的场景中,事务的处理就会变得复杂,会存在多个节点,多个节点的同步、可用性等都是需要考虑的问题,在分布式中有一个著名的 CAP 理论:
我们在 CAP、ACID 中讨论的一致性称为「强一致性」(Strong Consistency),而把牺牲了 C 的 AP 系统,但又要保证最终的结果是一致的,称为「弱一致性」,也叫最终一致性。最终一致性的概念由 eBay 的系统架构师丹 · 普利切特(Dan Pritchett)在 2008 年发表于 ACM 的论文「Base: An Acid Alternative」中提出的。
本文主要说下保证一致性的几种方式:TCC、SAGA 和消息队列。
TCC 是 Try-Confirm-Cancel 的缩写,表示将整个过程分为了三个阶段:
在 .NET Core 中可以参考:
https://github.com/simpleway2016/JMS
在 Java 中可以使用 seata:
https://github.com/seata/seata https://seata.io/zh-cn/
因为在 TCC 中的第一步 Try 需要预留资源,进行检查和校验,但在某些场景下,资源不是我们所能控制的,比如支付中,余额是银行管理的,我们通常没有权限。所以这时就不太适合 TCC ,可以考虑用 SAGA 来代替 TCC。
SAGA 起源于 1987 年普林斯顿大学的赫克托 · 加西亚 · 莫利纳(Hector Garcia Molina)和肯尼斯 · 麦克米伦(Kenneth Salem)在 ACM 发表的一篇论文《SAGAS》。
SAGA 和 TCC 最大的区别是基于数据补偿机制来代替回滚。一个 SAGA 表示处理多个服务中数据的一系列操作,由一连串的本地事务组成,每个独立的本地事务中还是能够使用 ACID 。
SATA 由两部分组成:
在 ACID 中如果出现异常,可以很容易进行回滚,但 SAGA 没办法自己回滚,必须依赖补偿动作来进行回滚。
如果 T1、T2、T3 都提交成功了,整个事务 T 就提交成功,如果执行 T2 时出现异常,这时有两种方式进行处理:
正向(不断重试):不断对 T2 进行重试操作,直到成功(不排除人工干预),等 T2 重试成功后,继续执行后面的 T3;
反向(补偿):T2 出现异常时,执行对应的补偿 C2,C2 必须执行成功(不排除人工),然后执行 T1 对应的补偿动作 C1 。
在上面提到的 seata 中也同样可以支持 SAGA 模式。
除了 seata ,还有一个用 go 语言写的 DTM 分布式事务框架也不错:
https://dtm.pub/ https://github.com/dtm-labs/dtm
重要的是,DTM 支持 C# 客户端:
https://github.com/dtm-labs/dtmcli-csharp
消息队列相信大家都不陌生,我们零代码产品中调用外部接口的组件,会被用在一些复杂的业务逻辑编排中,对外部接口的调用就是使用消息队列,RabbitMQ 的延时队列加上死信队列可以来进行重试的操作,来保证数据的最终一致。
还有另一种方式就是使用事务消息表,比如有这样一个场景,在系统列表中删除一条流程数据,这时需要做:
1、列表服务中对数据进行删除;
2、文件服务对这条数据相关的附件进行删除;
3、流程服务对该业务数据的所有流程信息进行删除。
具体的步骤如下:
1、列表服务删除数据成功后,在数据库中创建一张事务消息表,该表中记录事务 ID、数据删除成功的状态、业务数据 ID、附件待删除的状态、流程信息待删除的状态等;
2、列表服务删除数据成功后,发送消息分别进行附件删除处理和流程信息删除处理;
3、消息被正确处理后,修改事务消息表的状态;
4、创建一个单独的消息服务程序,轮询扫描事务消息表,如果发现状态没有变成已完成,就重新发送一个新的消息,这样附件删除和流程信息删除就会进行多次执行,这也要求这些操作必须是幂等的。
RabbitMQ 本身不支持分布式事务,不过有一些消息中间件是支持的,例如:RocketMQ,原生就支持分布式事务操作,可以更方便进行事务处理。
本文是一些理论的梳理,要想更彻底地掌握,可以选择一个框架,找几个场景,写写代码演练一下。