What's it?
事务, 基本上要求 多个sql操作 就向一个sql操作一样。(是一个系统,是一个整体,但对外表现就是像一个不可切割的个体。)
特别需要注意的是,一般我们说事务的时候,一般就是指那种Read committed或 Repeatable read 隔离级别的事务,是需要保证 一定的隔离级别的, 否则如果允许脏读 随机发生, 那事情就太简单了!!
分布式系统,基本上要求 多个节点 就向一个节点一样对外 提供服务 。
它是什么? 当我们把事务的ACID 特性和 分布式理论的CAP、BASE 统一 结合 起来的时候,其实就是 所谓的 分布式事务 !!!
分布式事务是企业集成中的一个技术难点,也是每一个分布式系统架构中都会涉及到的一个东西,特别是在微服务架构中,几乎可以说是无法避免
总的来说,BASE理论面向的是大型高可用可扩展的分布式系统,和传统的事物ACID特性是相反的,它完全不同于ACID的强一致性模型,而是通过牺牲强一致性来获得可用性,并允许数据在一段时间内是不一致的,但最终达到一致状态。但同时,在实际的分布式场景中,不同业务单元和组件对数据一致性的要求是不同的,因此在具体的分布式系统架构设计过程中,ACID特性和BASE理论往往又会结合在一起。
ACID 是面向单机的事务特性,如果mysql 数据库做了集群呢?ACID 是根本没有考虑网络问题的,而一旦涉及集群或者分布式系统,那么,P相关问题不可避免,那么我们可能需要结合 ACID ,CAP,甚至BASE。
AP还是CP还是混合 还是BASE?
分布式的系统,当然只能选择AP、CP咯!?其实我们不需要严格的AP,因为它会导致系统事情所有的C;或者CP,因为它失去所有的A。 所以,单纯的AP、CP都不是我们想要的。那么我们混合一下吧,其实可以大致这么认为,P+部分A+部分C 就是BASE。实际上,A不是 非0即1,C也不是 非0即1。 我们可以取值 0到1 之间。达到一个平衡的姿态即可。
一般认为, 保留A是比较好的,当然了不再需要保留全部A,否则就失去了所有C,前面说过这不是我们想要的。所以呢,我们会牺牲小部分的C保留大部分C, 但是,我们需要保证 最终一致性。这也大概是 为什么最终一致性 这么受关注和讨论 的原因吧。
分布式事务的实现
在分布式系统中,要实现分布式事务,无外乎那几种解决方案。
一、两阶段提交(2PC)
和上一节中提到的数据库XA事务一样,两阶段提交就是使用XA协议的原理,我们可以从下面这个图的流程来很容易的看出中间的一些比如commit和abort的细节。
两阶段提交这种解决方案属于牺牲了一部分可用性来换取的一致性。这个这么理解呢? 可以这么说,如果Phase1出现了网络分区,那么必然会导致协调者 收不到所有的 反馈,即使实际上发送来的是 All Yes,但是协调者会把网络分区导致的收不到认为是 延迟(协调者并不清楚是否发生了 网络分区),从而发生 abort 指令,从而使得所有的 和协调者处于同一分区的节点 回滚, 也就导致了 这些节点 永远不会提交事务,从而不会进行修改, 从而失去了所有的 A !!没错,虽然所有节点都没有成功提交(大部分是成功仍然回滚了), 但是因为没有了A,却保证了C。 ( 但是, 这里有另外一个不可忽略的问题,就是,协调者处于不同分区的 节点会一直 阻塞。。。)可以想象的是,3pc 也有类似问题,保证了C,失去了A,(然而如果是3pc的phase3 发生网络分区,那个 事务却是 可以成功的,但是之后的事务,无法通过phase1和2)。如果仅仅是网络分区问题,可以保证完美的C,但是如果是节点挂掉,那么C也无法完美保证了。
在实现方面,在 .NET 中,可以借助 TransactionScop 提供的 API 来编程实现分布式系统中的两阶段提交,比如WCF中就有实现这部分功能。不过在多服务器之间,需要依赖于DTC来完成事务一致性,Windows下微软搞的有MSDTC服务,Linux下就比较悲剧了。
另外说一句,TransactionScop 默认不能用于异步方法之间事务一致,因为事务上下文是存储于当前线程中的,所以如果是在异步方法,需要显式的传递事务上下文。
优点: 尽量保证了数据的强一致,适合对数据强一致要求很高的关键领域。(其实也不能100%保证强一致)
缺点: 实现复杂,牺牲了可用性,对性能影响较大,不适合高并发高性能场景,如果分布式系统跨接口调用,目前 .NET 界还没有实现方案。
二、补偿事务(TCC)
TCC 其实就是采用的补偿机制,其核心思想是:针对每个操作,都要注册一个与其对应的确认和补偿(撤销)操作。它分为三个阶段:
-
Try 阶段主要是对业务系统做检测及资源预留
-
Confirm 阶段主要是对业务系统做确认提交,Try阶段执行成功并开始执行 Confirm阶段时,默认 Confirm阶段是不会出错的。即:只要Try成功,Confirm一定成功。
-
Cancel 阶段主要是在业务执行错误,需要回滚的状态下执行的业务取消,预留资源释放。
举个例子,假入 Bob 要向 Smith 转账,思路大概是:
我们有一个本地方法,里面依次调用
1、首先在 Try 阶段,要先调用远程接口把 Smith 和 Bob 的钱给冻结起来。
2、在 Confirm 阶段,执行远程调用的转账的操作,转账成功进行解冻。
3、如果第2步执行成功,那么转账成功,如果第二步执行失败,则调用远程冻结接口对应的解冻方法 (Cancel)。
优点: 跟2PC比起来,实现以及流程相对简单了一些,但数据的一致性比2PC也要差一些
缺点: 缺点还是比较明显的,在2,3步中都有可能失败。TCC属于应用层的一种补偿方式,所以需要程序员在实现的时候多写很多补偿的代码,在一些场景中,一些业务流程可能用TCC不太好定义及处理。
三、本地消息表(异步确保)
本地消息表这种实现方式应该是业界使用最多的,其核心思想是将分布式事务拆分成本地事务进行处理,这种思路是来源于ebay。我们可以从下面的流程图中看出其中的一些细节:
基本思路就是:
消息生产方,需要额外建一个消息表,并记录消息发送状态。消息表和业务数据要在一个事务里提交,也就是说他们要在一个数据库里面。然后消息会经过MQ发送到消息的消费方。如果消息发送失败,会进行重试发送。
消息消费方,需要处理这个消息,并完成自己的业务逻辑。此时如果本地事务处理成功,表明已经处理成功了,如果处理失败,那么就会重试执行。如果是业务上面的失败,可以给生产方发送一个业务补偿消息,通知生产方进行回滚等操作。
生产方和消费方定时扫描本地消息表,把还没处理完成的消息或者失败的消息再发送一遍。如果有靠谱的自动对账补账逻辑,这种方案还是非常实用的。
这种方案遵循BASE理论,采用的是最终一致性,笔者认为是这几种方案里面比较适合实际业务场景的,即不会出现像2PC那样复杂的实现(当调用链很长的时候,2PC的可用性是非常低的),也不会像TCC那样可能出现确认或者回滚不了的情况。
优点: 一种非常经典的实现,避免了分布式事务,实现了最终一致性。在 .NET中 有现成的解决方案。
缺点: 消息表会耦合到业务系统中,如果没有封装好的解决方案,会有很多杂活需要处理。
四、MQ 事务消息
所谓的 事务消息, 其实我认为可以理解为 “可靠消息”。
有一些第三方的MQ是支持事务消息的,比如RocketMQ,他们支持事务消息的方式也是类似于采用的二阶段提交,但是市面上一些主流的MQ都是不支持事务消息的,比如 RabbitMQ 和 Kafka 都不支持。
以阿里的 RocketMQ 中间件为例,其思路大致为:
第一阶段Prepared消息,会拿到消息的地址。
第二阶段执行本地事务,第三阶段通过第一阶段拿到的地址去访问消息,并修改状态。
也就是说在业务方法内要想消息队列提交两次请求,一次发送消息和一次确认消息。如果确认消息发送失败了RocketMQ会定期扫描消息集群中的事务消息,这时候发现了Prepared消息,它会向消息发送者确认,所以生产方需要实现一个check接口,RocketMQ会根据发送端设置的策略来决定是回滚还是继续发送确认消息。这样就保证了消息发送与本地事务同时成功或同时失败。
遗憾的是,RocketMQ并没有 .NET 客户端。有关 RocketMQ的更多消息,大家可以查看这篇博客
优点: 实现了最终一致性,不需要依赖本地数据库事务。
缺点: 实现难度大,主流MQ不支持,没有.NET客户端,RocketMQ事务消息部分代码也未开源。
五、Sagas 事务模型
Saga事务模型又叫做长时间运行的事务(Long-running-transaction), 它是由普林斯顿大学的H.Garcia-Molina等人提出,它描述的是另外一种在没有两阶段提交的的情况下解决分布式系统中复杂的业务事务问题。你可以在这里看到 Sagas 相关论文。
我们这里说的是一种基于 Sagas 机制的工作流事务模型,这个模型的相关理论目前来说还是比较新的,以至于百度上几乎没有什么相关资料。
该模型其核心思想就是拆分分布式系统中的长事务为多个短事务,或者叫多个本地事务,然后由 Sagas 工作流引擎负责协调,如果整个流程正常结束,那么就算是业务成功完成,如果在这过程中实现失败,那么Sagas工作流引擎就会以相反的顺序调用补偿操作,重新进行业务回滚。
比如我们一次关于购买旅游套餐业务操作涉及到三个操作,他们分别是预定车辆,预定宾馆,预定机票,他们分别属于三个不同的远程接口。可能从我们程序的角度来说他们不属于一个事务,但是从业务角度来说是属于同一个事务的。
他们的执行顺序如上图所示,所以当发生失败时,会依次进行取消的补偿操作。
因为长事务被拆分了很多个业务流,所以 Sagas 事务模型最重要的一个部件就是工作流或者你也可以叫流程管理器(Process Manager),工作流引擎和Process Manager虽然不是同一个东西,但是在这里,他们的职责是相同的。在选择工作流引擎之后,最终的代码也许看起来是这样的
SagaBuilder saga = SagaBuilder.newSaga("trip")
.activity("Reserve car", ReserveCarAdapter.class)
.compensationActivity("Cancel car", CancelCarAdapter.class)
.activity("Book hotel", BookHotelAdapter.class)
.compensationActivity("Cancel hotel", CancelHotelAdapter.class)
.activity("Book flight", BookFlightAdapter.class)
.compensationActivity("Cancel flight", CancelFlightAdapter.class)
.end()
.triggerCompensationOnAnyError();
camunda.getRepositoryService().createDeployment()
.addModelInstance(saga.getModel())
.deploy();
这里有一个 C# 相关示例,有兴趣的同学可以看一下。
优缺点这里我们就不说了,因为这个理论比较新,目前市面上还没有什么解决方案,即使是 Java 领域,我也没有搜索的太多有用的信息。
分布式事务的最终一致性
分布式事务呢,除了2pc/3pc、tcc 这2种强一致性的 解决方案,其他的方案 都是 最终一致性的。
强一致性和最终一致性的事务,一个区别在于,
对于强一致性事务,我们可以立即拿到最后一个事务的结果,而最终一致性而需要等待,等待的时间不确定,如果最后的事务失败了,那么就是等待了许久但是等到了一个失败的结果。
另外,强一致性事务,我们拿最后一个事务的结果,是主动的拿,可以把这个结果进行各种加工,然后返回。 但是最终一致性,只能给被动的拿,也就是必须等待 最后一个事务 结束之后, 程序来进行通知 调用方。另外一个区别在于,强一致性事务,事务处理的具体的结果的传递,是自然而然的,就是直接方法内 引用传递; 而最终一致性而需要把结果序列化后,然后封装到消息里面发送给消息中间件。
如果某些服务非要 立即拿到另外的服务的结果,以便做进一步的处理, 那么也不适合采用 最终一致性。
参考:
https://www.cnblogs.com/savorboard/p/distributed-system-transaction-consistency.html
https://www.cnblogs.com/bigben0123/p/9453830.html