如何实现微服务架构下的分布式事务
什么是事务
事务(Transaction)及其ACID属性
事务是由一组SQL语句组成的逻辑处理单元,事务具有以下4个属性,通常简称为事务的ACID属性:
原子性(Atomicity):事务是一个原子操作单元,其对数据的修改,要么全都执行,要么全都不执行。
一致性(Consistent):在事务开始和完成时,数据都必须保持一致状态。这意味着所有相关的数据规则都必须应用于事务的修改,以保持数据的完整性;事务结束时,所有的内部数据结构(如B树索引或双向链表)也都必须是正确的。
隔离性(Isoation):数据库系统提供一定的隔离机制,保证事务在不受外部并发操作影响的“独立”环境执行。这意味着事务处理过程中的中间状态对外部是不可见的,反之亦然。
持久性(Durabe):事务完成之后,它对于数据的修改是永久性的,即使出现系统故障也能够保持。
什么是微服务
简而言之,微服务架构是一种将单应用程序作为一套小型服务开发的方法,每种应用程序都在其自己的进程中运行,并与轻量级机制(通常是HTTP资源的API)进行通信。这些服务是围绕业务功能构建的,可以通过全自动部署机制进行独立部署。这些服务的集中化管理已经是最少的,它们可以用不同的编程语言编写,并使用不同的数据存储技术。
微服务的优势
微服务落地存在的问题
虽然微服务现在如火如荼,但对其实践其实仍处于探索阶段。很多中小型互联网公司,鉴于经验、技术实力等问题,微服务落地比较困难。如著名架构师Chris Richardson所言,目前存在的主要困难有如下几方面:
1. 单体应用拆分为分布式系统后,进程间的通讯机制和故障处理措施变的更加复杂。
2. 系统微服务化后,一个看似简单的功能,内部可能需要调用多个服务并操作多个数据库实现,服务调用的分布式事务问题变的非常突出。
3. 微服务数量众多,其测试、部署、监控等都变的更加困难。
随着RPC框架的成熟,第一个问题已经逐渐得到解决。例如dubbo可以支持多种通讯协议,springcloud可以非常好的支持restful调用。对于第三个问题,随着docker、devops技术的发展以及各公有云paas平台自动化运维工具的推出,微服务的测试、部署与运维会变得越来越容易。
而对于第二个问题,现在还没有通用方案很好的解决微服务产生的事务问题。分布式事务已经成为微服务落地最大的阻碍,也是最具挑战性的一个技术难题。 为此,本文将深入和大家探讨微服务架构下,分布式事务的各种解决方案。
分布式事务典型场景:银行转账业务
以银行转账业务为例,通常包括以下三种情况:
A. 支行内转账:同一银行的相同支行内转账
B. 行内转账:同一银行的非同支行内转账
C. 跨行转账:不同银行的系统进行转账
对于传统集中式架构,A通常为本地事务,B可能为本地事务也可能为分布式事务,C为分布式事务。
业务微服务改造后,转入、转出通常为不同的微服务,同一个微服务也通常运行于不同的副本中。A可能变成一个分布式事务,也可能通过一些方法规避,在本地事务内完成。B和C很难规避,只能是分布式事务。
对于分布式事务,微服务最佳实践通常建议尽量规避,但是在很多业务场景是无法规避的,比如上面的B、C转账场景,没有好办法在一个微服务的本地事务内完成两个账户的数据更新。
分布式事务常用解决方案
为了解决分布式一致性问题,前人在性能和数据一致性的反反复复权衡过程中总结了许多典型的协议和算法。其中最常用的是两阶提交协议(2 Phase Commitment Protocol)。
两阶段提交方案
交易中间件与数据库通过 XA 接口规范,使用两阶段提交来完成一个全局事务, XA 规范的基础是两阶段提交协议。
第一阶段是表决阶段,所有参与者都将本事务能否成功的信息反馈发给协调者;第二阶段是执行阶段,协调者根据所有参与者的反馈,通知所有参与者,步调一致地在所有分支上提交或者回滚。
两阶段提交方案应用非常广泛,典型商用软件包括Oracle Tuxedo和IBM CICS。它的优点是对业务代码侵入较低,但缺点也很明显:
性能低下:由于 XA 协议自身的特点,它会造成事务资源长时间得不到释放,锁定周期长,而且在应用层上面无法干预,性能很差。
单点问题:协调者在整个两阶段提交过程中扮演着举足轻重的作用,一旦协调者所在服务器宕机,那么就会影响整个数据库集群的正常运行,比如在第二阶段中,如果协调者因为故障不能正常发送事务提交或回滚通知,那么参与者们将一直处于阻塞状态,整个数据库集群将无法提供服务。
同步阻塞:两阶段提交执行过程中,所有的参与者都需要听从协调者的统一调度,期间处于阻塞状态而不能从事其他操作,这样效率及其低下。
因此,两阶段提交方案在互联网项目中很少使用,很多业务中无法满足高并发需求。
为了这个弥补这种方案带来性能低的问题,大家又想出了很多种方案来解决,但这无一例外都需要通过在应用层做手脚,即入侵业务的方式,比如很出名的 TCC 方案,基于 TCC 也有很多成熟的框架,如 华为DTM,ByteTCC,tcc-transaction 等,以及基于可靠消息的最终一致性来实现。
TCC方案
TCC在电商、金融领域落地较多。TCC方案其实是两阶段提交的一种改进。其将整个业务逻辑的每个分支显式的分成了Try、Confirm、Cancel三个操作。Try部分完成业务的准备工作,confirm部分完成业务的提交,cancel部分完成事务的回滚。基本原理如下图所示。
事务开始时,业务应用会向事务协调器注册启动事务。之后业务应用会调用所有服务的try接口,完成一阶段准备。之后事务协调器会根据try接口返回情况,决定调用confirm接口或者cancel接口。如果接口调用失败,会进行重试。
TCC方案让应用自己定义数据库操作的粒度,使得降低锁冲突、提高吞吐量成为可能,比如华为分布式事务中间件DTM性能极高,普通配置服务器可以支持全局事务1万+ TPS,分支事务计算方式为3万+ TPS (阿里分布式事务中间件也是采用后者计算方式)。 当然TCC方案也有不足之处,集中表现在以下两个方面:
对应用的侵入性强。业务逻辑的每个分支都需要实现try、confirm、cancel三个操作,应用侵入性较强,改造成本高。
实现难度较大。需要按照网络状态、系统故障等不同的失败原因实现不同的回滚策略。为了满足一致性的要求,confirm和cancel接口必须实现幂等。
上述原因导致TCC方案大多被研发实力较强、有迫切需求的大公司所采用。微服务倡导服务的轻量化,而TCC方案中很多事务的处理逻辑需要应用自己编码实现,复杂且开发量大。
基于消息的最终一致性方案
消息一致性方案是通过消息中间件保证上、下游应用数据操作的[一致性。基本思路是将本地操作和发送消息放在一个事务中,保证本地操作和消息发送要么两者都成功或者都失败。下游应用向消息系统订阅该消息,收到消息后执行相应操作。
消息方案从本质上讲是将分布式事务转换为两个本地事务,然后依靠下游业务的重试机制达到最终一致性。基于消息的最终一致性方案对应用侵入性也很高,应用需要进行大量业务改造,成本非常高。
入侵代码的方案是基于现有情形“迫不得已”才推出的解决方案,实际上它们实现起来非常不优雅,比如TCC,一个事务的调用通常伴随而来的是对该事务接口增加一系列的反向操作,提交逻辑必然伴随着回滚的逻辑,这样的代码会使得项目非常臃肿,维护成本高。
针对上面所说的分布式事务解决方案的痛点,那很显然,我们理想的分布式事务解决方案肯定是性能要好而且要对业务无入侵,业务层上无需关心分布式事务机制的约束,也就是本文所重点推荐的非侵入事务,真正做到事务与业务分离。
非侵入事务方案
非侵入事务模式中,一个分布式事务称为一个全局事务,下面挂了若干个分支事务,而一个分支事务是一个满足 ACID 的本地事务。
非侵入事务中间件包含三个组件:
Transaction Coordinator (TC): 事务协调器,维护全局事务的运行状态,负责协调并驱动全局事务的提交或回滚。
Transaction Manager (TM): 控制全局事务的边界,负责开启一个全局事务,并最终发起全局提交或全局回滚的决议。
Resource Manager (RM): 控制分支事务,负责分支注册、状态汇报,并接收事务协调器的指令,驱动分支(本地)事务的提交和回滚。
其中,TM是一个分布式事务的发起者和终结者,TC负责维护分布式事务的运行状态,而RM则负责本地事务的运行。如下图所示:
简要说说整个全局事务的执行步骤:
TM 向 TC 申请开启一个全局事务,TC 创建全局事务后返回全局唯一的 XID,XID 会在全局事务的上下文中传播;
RM 向 TC 注册分支事务,该分支事务归属于拥有相同 XID 的全局事务;
TM 向 TC 发起全局提交或回滚;
TC 调度该全局事务的所有分支事务完成提交或者回滚。
非侵入事务实现原理相对复杂,我将在后续文章中跟大家探讨。
- 点赞
- 收藏
- 关注作者
评论(0)