几乎所有的信息管理系统都会涉及到事务,事务的目的是为了保证数据的一致性,这里说的一致性是数据库状态的一致性。
说到数据库状态的一致性,相信大家都会想到 ACID :
- 原子性(Atomic):在一个事件的多个数据库操作中,要么同时成功,要么同时失败,例如:转账业务。
- 隔离性(Isolation):不同的业务之间处理数据相互独立,互不影响。
- 持久性(Durability):正常提交的数据能够被持久化,不丢失数据,比如 mysql 天然就能持久化,redis 、 rabbitmq 也能通过设置进行持久化。
- 一致性(Consistency):最终的数据正确,所以说是通过 AID 这些手段来保证了 C 。
在单体架构中,通常是一套程序对应一个数据库,事务基于数据库本身的能力,如果你在 .NET Core 中使用 dapper 或 sqlsugar ,可以很容易进行事务的处理,可以参考下面文档:
https://dapper-tutorial.net/transaction 。
https://www.donet5.com/Home/Doc?typeId=1183。
但是,在微服务架构,分布式的场景中,事务的处理就会变得复杂,会存在多个节点,多个节点的同步、可用性等都是需要考虑的问题,在分布式中有一个著名的 CAP 理论:
- C:数据一致性(Consisitency):分布式中存在多个节点,对某个指定的客户端来说,从任一节点读取的数据保证获取到的是最新写入的数据。
- A:可用性(Acailability),非故障节点在合理的时间内返回合理的响应(不是错误和超时的响应)。
- P:分区容错性(Partition Tolerance),节点之间的数据传递是基于网络的,由于网络本身不是 100% 可靠,极端情况下会出现网络不可用的情况,进而将网络两端的节点分隔开来,这就是所谓的「网络分区」现象。在出现网络分区时,两部分的数据是不一致的,如果要保证数据的一致性,就必须要让没有及时同步数据的节点变为不可用,这就牺牲了可用性,否则就会牺牲一致性,所以在 P 一定存在的情况下,需要在 C 和 A 中间做取舍。
我们在 CAP、ACID 中讨论的一致性称为「强一致性」(Strong Consistency),而把牺牲了 C 的 AP 系统,但又要保证最终的结果是一致的,称为「弱一致性」,也叫最终一致性。最终一致性的概念由 eBay 的系统架构师丹 · 普利切特(Dan Pritchett)在 2008 年发表于 ACM 的论文「Base: An Acid Alternative」中提出的。
本文主要说下保证一致性的几种方式:TCC、SAGA 和消息队列。
TCC
TCC 是 Try-Confirm-Cancel 的缩写,表示将整个过程分为了三个阶段:
- Try:一个请求涉及到多个服务,多个服务会同时进行 Try,这个阶段为尝试执行阶段,在这个阶段中会进行数据的校验、检查,保障一致性,并准备资源,都成功会进入到 Confirm 阶段。
- Confirm:确认执行阶段,不进行任何业务检查,多个服务的 Try 都执行成功了,多个服务都进入到 Confirm 阶段,在这个阶段直接使用 Try 阶段准备的资源来完成业务处理。注意,Confirm 阶段可能会重复执行,因此需要满足幂等性。
- Cancel:如果在 Try 阶段有一个服务没有成功,那么所有的服务都进入到 Cancel 阶段,在该阶段,释放 Try 阶段预留的业务资源。注意,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
SAGA 起源于 1987 年普林斯顿大学的赫克托 · 加西亚 · 莫利纳(Hector Garcia Molina)和肯尼斯 · 麦克米伦(Kenneth Salem)在 ACM 发表的一篇论文《SAGAS》。
SAGA 和 TCC 最大的区别是基于数据补偿机制来代替回滚。一个 SAGA 表示处理多个服务中数据的一系列操作,由一连串的本地事务组成,每个独立的本地事务中还是能够使用 ACID 。
SATA 由两部分组成:
- 将一个大的事务拆分成的若干个小的事务,比如一个大的事务 T ,拆分成 T1、T2、T3。
- 每一个子事务有对应的补偿动作,例如对应上面的 T1、T2、T3 有 C1、C2、C3 的补偿动作。
在 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,原生就支持分布式事务操作,可以更方便进行事务处理。
本文是一些理论的梳理,要想更彻底地掌握,可以选择一个框架,找几个场景,写写代码演练一下。