Spring Cloud Alibaba训练营 —— 分布式事务

1. 简介

分布式一致性是分布式系统亟需解决的关键问题之一,根据过去一年的调查问卷,在微服务的实践中分布式事务是用户遇到的最大痛点。目前市面缺少经过洪荒流量验证的分布式事务组件,Seata 在阿里经济体内部经过了漫长的孵化,承载了双11洪荒流量,实践证明 Seata 是一款解决分布式数据一致性的的优秀组件。Seata 于 2019 年正式对外开源,开源后就受到了大家的热情追捧,一度蝉联 GitHub 活跃排名榜首。Seata 除了提供了独创的 AT 事务模式外,还扩展了 TCC、Saga 和 XA 事务模式,满足大家对于不同业务场景中的需求。相关详细信息可参考其官网 Seata官网

2. 学习目标

  • 理解分布式事务在业务中的核心使用场景和常用解决方案
  • 理解 Seata AT 事务模式的核心原理
  • 掌握 Seata 作为分布式事务组件与 Spring Cloud 的整合
  • 如何扩展一个 RPC 框架
  • Seata 实战

3. 为什么需要分布式事务?

分布式事务不是在新架构下产生的新问题,即使在单体应用中同样存在着分布式事务问题,典型的场景是单体应用执行方法中含有多个数据源。X/OPEN 对于这一问题,提出了含有三种角色的 DTP(Distributed Transaction Processing)模型并形成了 XA 规范来解决此问题。各厂商针对 XA 规范做了具体的实现,也就是大家常说的 XA协议。在 Java 体系中基于 DTP 模型提出了 JTA规范(参考 JSR 907), 定义了分布式事务中的事务管理器(TM)与资源管理器(RM)、应用程序(AP)等的 Java 接口。在Java EE时代,应用服务器如weblogic 充当了 TM 的角色,而传统关系数据库通过实现 XA 协议充当了 RM 的角色。

随着互联网的高速发展,庞大的用户群体和快速的需求变化已经成为了传统架构的痛点。在这种情况下,如何从系统架构的角度出发,构建出灵活、易扩展的系统来快速响应需求的变化,同时,随着用户量的增加,如何保证系统的稳定性、高可用性、可伸缩性等等,成为了系统架构面临的挑战。微服务基于此背景应运而生,微服务架构越来越来越成为一种架构趋势,其本质是分布式去中心化。但微服务架构绝不是银弹,它不一定是一种能支撑未来一二十年的架构,引入微服务架构时需要我们根据业务场景,系统复杂性和团队规模有步骤的进行。微服务架构的引入使分布式数据一致性问题更为突出,由原来的单体应用拆分出来几十甚至上百个微服务,如何保证服务间的一致性?当在一条较长的微服务调用链中,位于中间位置的微服务节点出现异常,如何保证整个服务的数据一致性?

分布式一致性的引入,一定不可避免带来性能问题,如何更高效的解决分布式一致性问题,一直是我们致力于解决此问题的关键出发点。在“一切都正常”的情况下,我们可以认为我们并不需要分布式事务。但系统很难满足这种理想状态,系统可能因为一个非法的参数校验无法将服务链路继续向下调用下去,系统可能出现令人反感的超时问题,我们不清楚被调用的服务是否真正的执行了,被调用服务可能正在部署,网络抖动亦或者节点宕机导致接口无法继续调用。这些问题普遍存在于我们的系统中,业务的本质体现在数据上,数据不一致的直接后果是可能产生资损,更严重的是如果不一致的数据不能被及时发现,业务再次基于此数据的进行相关逻辑操作,会进一步导致数据错上加错,最终很难溯源。

4. 常见的分布式事务解决方案

从是否满足事务 ACID 特性上,我们可以将事务分为两大类:刚性事务和柔性事务。在常见解决方案中XA事务属于刚性事务解决方案,而其他的大多数解决方案如 TCC、Saga、消息最终一致性则属于柔性事务解决方案。以下将对几种常见的事务方案做简要的介绍:

消息最终一致性

消息最终一致性方案是在Seata问世之前,市面上应用最广泛的一种解决方案。它本身具有削峰填谷,可异步化的优点,更多的适应于可异步化的末端链路消息通知场景。但是它本身也存在着一些缺点:需要依赖可靠消息组件,消息的可靠性很重要,大多数的原生消息组件故障时很难降级;实时性比较差,要经过多次网络IO开销和持久化,遇到队列积压情形实时性不可控;无法保证隔离性,在已发送消息和消息消费之前,中间数据对外可见,无法满足事务 isolate 特性;只能向前重试不可向后回滚,消息消费无法成功时无法回滚消息生产侧的数据;无法保证多条消息间的数据一致性。

XA

XA 标准提出后的20多年间未能得到持续的演进,在学术界有协议优化和日志协同处理等相关的研究,在工业界使用XA落地方案的相对较少,主要集中在应用服务器的场景。XA方案要求相关的厂商提供其具体协议的实现,目前大部分关系数据库支持了XA协议,但是支持程度不尽相同,例如,MySQL 在5.7 才对 xa_prepare 语义做了完整支持。XA 方案被人诟病的是其性能,其实更为严重的是对于连接资源的占用,导致在高并发未有足够的连接资源来响应请求成为系统的瓶颈。在微服务架构下 XA 事务方案随着微服务链路的扩展成为一种反伸缩模式,进一步加剧了资源的占用。另外 XA 事务方案要求事务链路中的resource全部实现XA协议方可使用,若其中某一资源不满足,那么就无法保证整个链路的数据一致性。

TCC

TCC 方案要求用户根据业务场景实现 try,confirm,cancel三个接口,由框架根据事务所处的事务阶段和决议来自动调用用户实现的三个接口。从概念上TCC框架可以认为是一种万能框架,但是其难点是业务对于这三个接口的实现,开发成本相对较高,有较多业务难以做资源预留相关的逻辑处理,以及是否需要在预留资源的同时从业务层面来保证隔离性。因此,这种模式比较适应于金融场景中易于做资源预留的扣减模型。

Saga

有了 TCC 解决方案为什么还需要 Saga 事务解决方案?上文提到了 TCC 方案中对业务的改造成本较大,对于内部系统可以自上而下大刀阔斧的推进系统的改造,但对于第三方的接口的调用往往很难推动第三方进行 TCC 的改造,让对方为了你这一个用户去改造 TCC 方案而其他用户并不需要,需求上明显也是不合理的。要求第三方业务接口提供正反接口比如扣款和退款,在异常场景下必要的数据冲正是合理的。另外,Saga 方案更加适应于工作流式的长事务方案并且可异步化。

上面提到了4种常用的分布式事务解决方案,Seata 集成了TCC、Saga 和 XA 方案。另外,Seata 还提供了独创的 AT 强一致分布式事务解决方案。下文将对 AT 方案进行简要的介绍。

5. AT事务模式

undefined

一个分布式事务有全局唯一的xid,由若干个分支事务构成,每个分支事务有全局唯一的branchId。上图展示了在一个分支事务中RM 与 TC 的交互过程。其中主要包含的交互动作如下: - branchRegister 分布式事务一阶段执行,分支事务在commit 之前与 TC 交互获取 全局锁 和返回 branchId。全局锁为Seata 应用锁等同于修改数据记录的行锁,若获取锁失败将会进行锁重试,此处提供了两种重试策略是否持有数据库连接重试全局锁,默认为释放数据库连接。若成功,则抢占全局锁并返回branchId,若重试到最大次数失败,则发起全局事务的回滚,对已完成的分支事务执行回滚。

  • branchReport 分布式事务一阶段执行,本地事务commit 之后与 TC 交互,上报本地事务已完成标识。目前 branchReport 动作已经在 1.0 版本做了相关的优化,本地事务commit 不上报,本地事务rollback 上报。经过优化分布式事务的整体性能在globalCommit 场景下最低提升25%,最高提升50%。本地事务rollback 上报可以帮助 TC 快速决策需要回滚的分支事务。
  • branchCommit 分布式事务二阶段执行,在形成globalCommit 决议后执行。AT模式中此步骤异步执行来提升其性能,可以认为分布式事务globalCommit决议提交到TC 释放完全局锁就已经完成了整个分布式事务的处理。branchCommit 在AT模式主要用于删除一阶段的undo_log,TC下发到RM后并不是立即执行,而是通过定时任务+sql 批量合并的方式来提升其处理性能。
  • branchRollback 分布式事务二阶段执行,在形成globalRollback 决议后执行。RM 收到 branchRollback 请求,取undo_log 表中对应的branchId 记录解析rollback_info 字段,对现有数据和undo log后镜像对比,对比不同则分支事务回滚失败。对比成功则根据前镜像构造sql并执行反向操作和删除undo log。

详细处理过程和原理,可参考官网文档关于AT模式的介绍:seata.io/zh-cn/docs/…

6. Seata 与 Spring Cloud 集成

undefined

如上图,Seata 与 Spring Cloud Alibaba 集成代码结构如上图所示。从代码上可以分为三大部分:rest、feign 和 web。AutoConfiguration 结尾的类是 @Configuration 类被 spring.factories 加载,负责创建 package 中所属的 bean。

rest

对应restTemplate调用,实现 ClientHttpRequestInterceptor 接口,将当前事务上下文包装到HttpRequest header中,加入到拦截器列表中。

feign

对应openFeign调用,这部分实现了事务上下文传递,与 Hystrix、Sentinel 、Ribbon 组件集成功能。需要特别注意是Hystrix中跨线程的事务上下文传递。这部分代码大量使用了Builder、Wrapper模式,有兴趣的同学可深入阅读。

web

对应Spring Web Servlet中的处理,实现了 HandlerInterceptor接口。在 preHandle 预处理中取 httpRequest header中 Seata的事务上下文并使用 API 绑定到当前线程的事务处理上下文中,这种后续的数据源操作就自动加入到了分布式事务的链路中;在 afterCompletion 中做了当前线程事务上下文的清除,防止事务上下文在线程中污染。

7. 如何扩展一个RPC框架?

在上一章节,我们讲到了Seata 是如何与 Spring Cloud 相集成的。Seata 目前已经集成了 Spring Cloud、Alibaba Dubbo、Apache Dubbo、Motan、gRPC 和 sofa-RPC 等微服务调用。结合上一节与Spring Cloud 的集成,扩展一个RPC 框架我们需要做哪些工作呢 ?主要可以分为两大部分:

事务上下文传递

Seata 的事务上下文目前主要包含:xid和调用服务的分支事务类型。xid 为一个分布式事务的全局唯一标识,类似于Tracing中的 traceId,只有将 xid 传递下去才可能加入到分布式事务的链路中。调用服务的分支事务类型主要用于非 AT模式,例如在一个 TCC 分支事务中不能再嵌套 AT 分支事务。主流的RPC框架大多是基于TCP协议之上的私有协议封装或者是基于HTTP协议。Seata 的事务上下文标识都是简单的字符串,序列化由RPC框架直接完成,大多数RPC 框架实现了 filter 或者 interceptor 接口,通过将Seata的事务上下文填充到协议中的attachment字段或者http header中,就可以简单的完成事务上下文的传递。

事务上下文绑定和清除

在 RPC 的 provider 端收到 consumer 的请求,将事务上下文取出,通过 API 绑定到当前的执行线程中,这样后续的业务处理都纳入到了Seata 的分布式事务链路中。执行完业务处理后,需要对绑定到当前线程的事务上下文清除掉,防止产生线程事务上下文污染。

具体 API 可参考官网文档:seata.io/zh-cn/docs/… 请大家思考一下如何去与Dubbo 集成的呢?github.com/seata/seata…

8. Seata 实战

本章节将通过一个基于Spring Cloud的订单和库存服务进行Seata 实战。 我们在沙箱环境里准备好了一套seata的实际应用案例,服务调用结构如下所示: undefined 如图所示,服务链路中包含三个微服务:business(微服务入口)、order(订单服务)和 storage(库存服务)。

业务逻辑 通过url 调用 business 服务,business 服务会通过 openFeign 的方式分别调用 storage 和 order 服务。每个服务调用后会根据正常(ok)或异常(fail)的返回值决定是否抛出异常,若返回结果不为 ok 那么 business 将抛出异常,触发整个事务回滚,预期数据至 business 方法执行前的数据并且库存总量和与初始值一致。当order 和 storage 两个服务调用正常,用户可以根据 url 中的 mockException=true 或 false 来注入一个 mock 异常,当注入异常后,期望数据回滚至 business 方法执行前的数据并且库存总量和与初始值一致。

异常模拟 \1. 在 bussiness 中请求超过现有库存,通过参数 count 来指定要扣减的库存数量,当超出库存,将抛出异常进行事务回滚。 \2. 在 bussiness 中请求中加入 mockException=true 参数,触发事务回滚。

启动步骤 以下请求 url 中的 localhost 请根据实际值进行替换。 \1. 启动order 和 storage 服务,最后启动 business 服务。服务的注册和发现,Seata服务注册和发现 在这里使用了 Nacos 启动成功后,可以通过Nacos 控制台的sandbox-seata namespace 看到如下服务: undefined

  1. 访问 business 的url : http://localhost:8080/seata/check 来检查初始化数据 undefined
  2. 通过访问 business 的url:http://localhost:8080/seata/feign?count=5&mockException=true 来触发业务逻辑: undefined 其中count 代表扣减库存的数量,当库存不足将触发事务回滚;mockException代表是否触发业务异常,设置为true 会触发业务异常,并通过分布式事务实现回滚。
  3. 每次操作完business 请求后,访问 business 的url : http://localhost:8080/seata/check 来检查操作后数据。
  4. 重复以上3(根据实际需要请求,不必每次请求相同),4步骤,最后再次通过访问 business 的url : http://localhost:8080/seata/check 来检查结果数据: undefined

使用总结 \1. 引入依赖。com.alibaba.cloud:spring-cloud-starter-alibaba-seata 中包含了 io.seata:seata-spring-boot-starter 依赖。若于期望依赖 seata 的版本不一致,可手动排除重引入。com.alibaba.cloud 原生 版本说明

parent:
<dependencyManagement>
            <dependency>
                <groupId>com.alibaba.cloud</groupId>
                <artifactId>spring-cloud-alibaba-dependencies</artifactId>
                <version>${alibaba.cloud.version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
    </dependencyManagement>
	
	module:
	 <dependencies>
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-seata</artifactId>
        </dependency>
    </dependencies>
  1. 添加配置文件,具体配置项说明参考此处
seata:
   enabled: true
   application-id: business
   tx-service-group: my_test_tx_group
   config:
      type: nacos
      nacos:
         namespace: "sandbox-seata"
         serverAddr: 139.196.203.133:8848
         group: SEATA_GROUP
         username: xxx
         password: xxx
   registry:
      type: nacos
      nacos:
         application: seata-server
         serverAddr: 139.196.203.133:8848
         group: SEATA_GROUP
         namespace: "sandbox-seata"
         username: xxx
         password: xxx
  1. 在所有业务库中创建 undo_log 表,不同的数据库类型脚本参考此处 ,示例中已自动完成创建。
  2. 在需要纳入分布式事务链路的入口service 方法(保证可使 Spring 切面生效的位置亦可)上添加 @GlobalTransactional 注解。

9. 其他

Seata 更多sample样例 ,请参考 样例 Seata 社区联系方式,请参见 联系我们 欢迎大家试用阿里云商业化产品 GTS,更高性能,更稳定,全面兼容 Seata。