内容总结自《微服务架构设计模式》
使用Saga管理事务
- 一、XA解决方案存在问题
- 二、使用Saga管理事务
- Saga是什么
- 补偿事务是什么
- Saga协调模式
- 协同式Saga
- 编排式Saga
- 隔离性
- Saga结构
- 三、总结
一、XA解决方案存在问题
在多个服务、数据库和消息代理之间维持数据一致性的传统方式是采用分布式事务。分布式事务管理的事实标准是X/Open Distributed Transaction Processing (DTP) Model。XA采用了两阶段提交(two phase commit,2PC)来保证事务中的所有参与方同时完成提交,或者在失败时同时回滚。应用程序的整个技术栈需要满足XA标准,包括符合XA要求的数据库、消息代理、数据库驱动、消息API,以及用来传播XA全局事务ID的进程间通信机制。市面上绝大多数的SQL数据库和一部分消息代理满足XA标准。例如,JaveEE应用程序可以使用JTA来完成分布式事务。
分布式事务并不像听起来这么简单,而是有着许多问题。其中一个问题是,许多新技术,包括NoSQL数据库(例如 MongoDB和 Cassandra),并不支持XA标准的分布式事务。同样,一些流行的消息代理如RabbitMQ和Apache Kafka也不支持分布式事务。因此,如果你坚持在微服务架构中使用分布式事务,那么不得不放弃使用这些流行的数据库或消息代理。
二、使用Saga管理事务
Saga是什么
传统的分布式事务管理方法对于现代应用程序来说不是一个好的选择。跨服务的操作必须使用所谓的Saga(一种消息驱动的本地事务序列)来维护数据一致性,而不是ACID事务。Saga的一个挑战在于只满足ACD(原子性、一致性和持久性)特性,而缺乏传统ACID事务的隔离性。因此,应用程序必须使用所谓的对策(countermeasure),找到办法来防止或减少由于缺乏隔离而导致的并发异常。
简单来说就是:通过使用异步消息来协调一系列本地事务,从而维护多个服务之间的数据一致性。
补偿事务是什么
传统ACID事务的一个重要特性是:如果业务逻辑检测到违反业务规则,可以轻松回滚事务。通过执行ROLLBACK语句,数据库可以撤销(回滚)目前为止所做的所有更改。遗憾的是,Saga无法自动回滚,因为每个步骤都会将其更改提交到本地数据库。这意味着,如果一个流程有六个步骤,如果第四个步失败,则应用程序必须明确撤销前三个步骤所做的更改。即必须编写所谓的补偿事务。
假设一个Saga的第n+1个事务失败了。必须撤销前n个事务的影响。从概念上讲,每个步骤T,都有一个相应的补偿事务C,它可以撤销T的影响。要撤销前n个步骤的影响,Saga必须以相反的顺序执行每个C。步骤顺序如图
Saga协调模式
Saga的实现包含协调Saga步骤的逻辑。当通过系统命令启动Saga时,协调逻辑必须选择并通知第一个Saga参与方执行本地事务。一旦该事务完成,Saga协调选择并调用下一个Saga参与方。这个过程一直持续到Saga执行完所有步骤。如果任何本地事务失败,则 Saga必须以相反的顺序执行补偿事务。以下几种不同的方法可用来构建Saga的协调逻辑。
- 协同式 ( choreography):把Saga 的决策和执行顺序逻辑分布在Saga的每一个参与方中,它们通过交换事件的方式来进行沟通。
- 编排式(orchestration):把 Saga的决策和执行顺序逻辑集中在一个Saga编排器类中。Saga编排器发出命令式消息给各个Saga参与方,指示这些参与方服务完成具体操作(本地事务)。
协同式Saga
使用协同时,没有一个中央协调器会告诉Saga参与方该做什么。相反,Saga参与方订阅彼此的事件并做出相应的响应。为了展示基于协同的Saga如何运作。
好处:
- 简单:服务在创建、更新或删除业务对象时发布事件。
- 松耦合:参与方订阅事件并且彼此之间不会因此而产生耦合。
弊端:
- 更难理解:与编排式不同,代码中没有一个单一地方定义了Saga。相反,协调式Saga的逻辑分布在每个服务的实现中。因此,开发人员有时很难理解特定的Saga是如何工作的。
- 服务之间的循环依赖关系:Saga参与方订阅彼此的事件,这通常会导致循环依赖关系,比如A服务->B服务->A服务。虽然这并不一定是个问题,但循环依赖性被认为是一种不好的设计风格。
- 紧耦合的风险:每个Saga参与方都需要订阅所有影响它们的事件。
编排式Saga
编排式是实现Saga的另外一种方式。当使用编排式Saga时,开发人员定义一个编排器类,这个类的唯一职责就是告诉Saga的参与方该做什么事情。Saga编排器使用命令/异步响应方式与Saga的参与方服务通信。为了完成Saga中的一个环节,编排器对某个参与方发出一个命令式的消息,告诉这个参与方该做什么操作。当参与方服务完成操作后,会给编排器发送一个答复消息。编排器处理这个消息,并决定Saga 的下一步操作是什么。
基于编排的Saga的每个步骤都包括一个更新数据库和发布消息的服务。一个 Saga参与方通过更新其数据库并发送回复消息,作为对编排器的响应。
好处:
- 更简单的依赖关系:编排的一个好处是它不会引人循环依赖关系。Saga编排器调用Saga参与方,但参与方不会调用编排器。因此,编排器依赖于参与方,但反之则不然,因此没有循环依赖。
- 较少的耦合:每个服务实现供编排器调用的API,因此它不需要知道Saga参与方发布的事件。
- 改善关注点隔离,简化业务逻辑:Saga的协调逻辑本地化在Saga编排器中。领域对象更简单,并且不需要了解它们参与的Saga。
弊端
- 在编排器中存在集中过多业务逻辑的风险,为Saga实现协调逻辑只是你需要解决的设计问题之一
- 处理缺乏隔离的问题也许是你在使用Saga时面临的最大挑战。
隔离性
缺乏隔离可能导致数据库异常( anomaly)。异常是一个数据库术语,指多个事务以某种方式(往往是并行)读取或写入数据产生的结果,与多个事务按顺序执行时的结果不同。当异常发生时,并行执行多个Saga的结果与串行执行的结果不同。
从表面上看,缺乏隔离听起来是个大问题。但实际上,开发人员往往在减少隔离性和获得更高性能之间权衡。关系型数据库允许你为每个事务指定隔离级别。默认的隔离级别通常是一个比完全隔离要弱一些的隔离级别,也称为可序列化事务。现实世界中的数据库事务通常与教科书中的ACID事务定义不同。
缺乏隔离性会导致的问题:
- 丢失更新
- 脏读
- 模糊或不可重复读
解决策略:
- 语义锁:应用程序级的锁。
- 交换式更新:把更新操作设计成可以按任何顺序执行。
- 悲观视图:重新排序Saga的步骤,以最大限度地降低业务风险。
- 重读值:通过重写数据来防止脏写,以在覆盖数据之前验证它是否保持不变。版本文件:将更新记录下来,以便可以对它们重新排序。
- 业务风险评级(by value):使用每个请求的业务风险来动态选择并发机制。
Saga结构
Saga模型中,一个Saga包含三种类型的事务
- 可补偿性事务:可以使用补偿事务回滚的事务。
- 关键性事务:Saga执行过程的关键点。如果关键性事务成功,则Saga将一直运行到完成。关键性事务不见得是一个可补偿性事务,或者可重复性事务。但是它可以是最后一个可补偿的事务或第一个可重复的事务。
- 可重复性事务:在关键性事务之后的事务,保证成功。
三、总结
-
某些系统操作需要更新分散在多个服务中的数据。传统的基于XA/2PC的分布式事务不适合现代应用。更好的方法是使用Saga模式。Saga是使用消息机制协调的一组本地事务序列。每个本地事务都在单个服务中更新数据。由于每个本地事务都会提交更改,因此如果由于违反业务规则而导致Saga必须回滚,则必须执行补偿事务以显式撤销更改。
-
可以使用协同或编排来协调Saga的步骤。在基于协同的Saga 中,本地事务发布触发其他参与方执行本地事务的事件**。在基于编排的Saga中,集中式Saga编排器向参与方发送命令式消息,告诉它们执行本地事务。可以通过将Saga编排器建模为状态机来简化开发和测试。简单的Saga可以使用协同式,但编排式通常是复杂Saga的更好选择。**
-
设计基于Saga 的业务逻辑可能具有挑战性,因为与ACID事务不同,Saga不是彼此孤立的。你必须经常使用各种对策,即防止ACD事务模型引起的并发异常的设计策略。应用甚至可能需要使用锁来简化业务逻辑,即使这会导致死锁。