工作纪实_14-Seata分布式事务框架调研【dubbo/cloud和AT/TCC模式源码提供】

这里写目录标题

一、简介

背景

分布式事务是跨系统、跨机器之间的事务,由于其不满足单机的ACID特性,所以较普通事务来说复杂了很多

而对微服务而言,其实就是微服务接口调用不同的微服务时,涉及到跨库的事务数据一致性的问题,尤其是在服务

调用过程中,针对异常,对数据一致性的要求尤为苛刻

二、理论依据

CAP原则

CAP原则又称CAP定理,指的是在一个分布式系统中, Consistency(一致性)、 Availability(可用性)、Partition tolerance(分区容错性),三者不可得兼

分布式系统的CAP理论:理论首先把分布式系统中的三个特性进行了如下归纳:

  • 一致性(C):在分布式系统中的所有数据备份,在同一时刻是否同样的值。(等同于所有节点访问同一份最新的数据副本)
  • 可用性(A):在集群中一部分节点故障后,集群整体是否还能响应客户端的读写请求。(对数据更新具备高可用性)
  • 分区容忍性(P):以实际效果而言,分区相当于对通信的时限要求。系统如果不能在时限内达成数据一致性,就意味着发生了分区的情况,必须就当前操作在C和A之间做出选择

按照CAP理论,我们选择分布式事务的解决方案时,必然要在CPAP之间做选择,剩下的一个我们则尽量去保证,这就需要根据不同业务场景来选择。

BASE理论

BASEBasically Available(基本可用)、Soft state(软状态)和Eventually consistent(最终一致性)三个短语的简写,BASE是对CAP中一致性和可用性权衡的结果,其来源于对大规模互联网系统分布式实践的结论,是基于CAP定理逐步演化而来的,其核心思想是即使无法做到强一致性(Strong consistency),但每个应用都可以根据自身的业务特点,采用适当的方式来使系统达到最终一致性(Eventual consistency)。接下来我们着重对BASE中的三要素进行详细讲解。

基本可用

基本可用是指分布式系统在出现不可预知故障的时候,允许损失部分可用性——但请注意,这绝不等价于系统不可用,以下两个就是“基本可用”的典型例子。

  • 响应时间上的损失:正常情况下,一个在线搜索引擎需要0.5秒内返回给用户相应的查询结果,但由于出现异常(比如系统部分机房发生断电或断网故障),查询结果的响应时间增加到了1~2秒。
  • 功能上的损失:正常情况下,在一个电子商务网站上进行购物,消费者几乎能够顺利地完成每一笔订单,但是在一些节日大促购物高峰的时候,由于消费者的购物行为激增,为了保护购物系统的稳定性,部分消费者可能会被引导到一个降级页面。

弱状态也称为软状态,和硬状态相对,是指允许系统中的数据存在中间状态,并认为该中间状态的存在不会影响系统的整体可用性,即允许系统在不同节点的数据副本之间进行数据听不的过程存在延时。

最终一致性【有延迟】

最终一致性强调的是系统中所有的数据副本,在经过一段时间的同步后,最终能够达到数据一致的状态。

因此,最终一致性的本质是需要系统保证最终数据能够达到一致,而不需要实时保证系统数据的强一致性。

三、柔性事务解决方案

柔性事务满足BASE理论(基本可用,最终一致),刚性事务满足ACID理论

一、两阶段型

1.1 XA两段式【基础】
阶段一:提交事务请求

1、事务询问。协调者向所有参与者发送事务内容,询问是否可以进行事务提交操作,然后就开始等待参与者的响应。

2、执行事务。各参与者节点执行事务操作,并将UndoRedo信息记入事务日志中。

3、各参与者向协调者反馈事务询问的响应。

阶段二:执行事务提交

假如协调者从所有的参与者获得的反馈都是Yes响应,那么就会执行事务提交。

1、发送提交请求。

2、事务提交。

3、反馈事务提交结果。参与者在完成事务提交之后,会向协调者发送Ack消息。

4、完成事务。

中断事务

1、发送回滚请求。协调者向参与者发出rollback请求。

2、事务回滚。参与者接收到Roolback请求利用阶段一种记录的Undo信息来执行事务回滚动作。

3、反馈事务回滚结果。

4、中断事务。

总结

优点:原理简单,实现方便;

缺点:同步阻塞、单点问题【协调者故障】、参与者断网无法提交事务问题、数据不一致。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-kgTAXg1L-1596016393692)(C:\Users\dell\AppData\Roaming\Typora\typora-user-images\1594713098053.png)]

1.2 三段式【变种】

在这里插入图片描述

阶段一:CanCommit

1、事务询问。

2、各参与者向协调者反馈事务询问的响应。

阶段二:PreCommit

假设协调者从所有的参与者获得的都是Yes响应,那么将执行事务预提交。

​ 1、发送预提交请求。协调者向所有参与者节点发出preCommit请求,进入prepared阶段。

​ 2、事务预提交。参与者接收到preCommt请求,执行事务操作后,将UndoRedo信息记录到事务日志中。

​ 3、各参与者向协调者反馈事务提交的响应。

假设任何一个参与者向协调者反馈了No反应,活着在等待超时之后,协调者无法获得所有参与者的响应,那么将执行事务的中断。

​ 1、发送终端请求。协调者向所有参与者发出abort请求。

​ 2、中断事务。无论接到abort请求还是等待协调者请求过程出现超时情况,参与者都会中断事务。

阶段三:doCommit

该阶段将进行真正的事务提交

执行提交

​ 1、发送提交请求。进入这一阶段,假设协调者从正常的工作状态,并且接收到所有的参与者的ack响应,它将从预提交状态转换到提交状态,向所有参与者发送doCommit请求。

​ 2、事务提交。参与者接收到doCommit请求后,正式执行事务提交操作。并在提交后释放在整个事务执行期间占用的事务资源。

​ 3、反馈事务提交结果。参与者完成事务提交之后,向协调者发送Ack消息。

​ 4、完成事务。协调者接收到所有参与者的Ack消息,完成事务。

中断事务

中断事务的4步操作与提交事务完全一致,只不过从提交事务变成了事务回滚。

总结

三段式降低了参与者的阻塞范围,两段式在第一阶段就阻塞,三段式在第二阶段阻塞

三段式解决了两段式的单点阻塞问题,因为一旦参与者无法及时收到来自协调者的信息之后,会由于超时机制而默认执行commit,但如果协调者发送的是abort,而其中一个参与者由于网络问题没有收到,最终执行了commit,就会导致这个参与者与其他执行了abort的参与者数据不一致。

二、TCC补偿型

全称:Try-Confirm-Cancel, 在电商、金融领域落地较多。TCC方案其实是两阶段提交的一种改进

Try 阶段

完成所有业务检查,预留业务资源, 负责持久化记录存储消息数据 ,灵活选择业务资源的锁力度

Confirm 阶段

确认执行业务操作【可以空代码】,不做任何业务检查,只使用Try阶段预留的业务资源,满足操作幂等性

Cancel 阶段

取消Try阶段预留的业务资源,删除提交的事务数据,满足操作幂等性

特点:由业务活动来保证数据的最终一致性,本地事务补偿性代码,通过本地事务回滚机制来保证ACID特性,由于是多个独立的本地事务组成,所以不会对资源一直加锁

缺点:代码开发成本高,维护复杂,对每一类组合的事务活动需要开发相应阶段的事务补偿代码

主流开源框架: https://github.com/changmingxie/tcc-transaction/wiki/%E4%BD%BF%E7%94%A8%E6%8C%87%E5%8D%971.2.x

三、异步确保型

不常用,需要建立本地消息表,并且依赖MQ或者类似的中间件,极度依赖数据库的消息表【需要自己维护】来管理事务,在高并发情况下难以扩展,较为繁琐

四、MQ事物消息

基于MQ来实现事务,不再用本地的消息表,目前阿里云的RocketMq支持事务消息,也需要手动回滚、补偿事务,对于rabbitmqkafka都不支持事务消息,付费的RocketMq变种Ons比免费更强大。

五、主流分布式事务中间件

分布式事务中间件其本身并不创建事务,而是基于对本地事务的协调从而达到事务一致性的效果
在这里插入图片描述

5.1 SEATA【推荐】
简介

源自阿里云的全局事务服务【GTS】的框架,高性能且易于使用,旨在实现简单并快速的事务提交、回滚。

已为用户提供了ATTCCSAGA三种事务模式,为用户打造一站式的分布式解决方案.2014年面世到现在

已经更新了6年时间。

源码: https://github.com/seata/seata

文档: http://seata.io/zh-cn/

特点:最终一致性,可能存在脏读

事务组成
TC-事务协调者

维护全局和分支事务的状态,驱动全局事务提交或回滚。

TM-事务管理器

定义全局事务的范围:开始全局事务、提交或回滚全局事务。

RM-资源管理器

管理分支事务处理的资源,与TC交谈以注册分支事务和报告分支事务的状态,并驱动分支事务提交或回滚。

5.2 TX-LCN

四、方案比较

核心RocketMqTX-LCNSEATA
兼容性不存在兼容性问题SpringCloudDubboSpringCloudDubbo
高可用支持集群组件支持集群支持集群化
事务机制ack、事务消息机制TXC-代理转本地事务处理AT-根据undo_logsql
模式MQ需要支持事务消息支持TCCTXC支持ATTCCSAGAXA
扩展性接入RocketMq即可
CAP理论最终一致性CP】强一致性,可能死锁AP】最终一致性,可能脏读
界面管理
性能一般
开发难度复杂,更关注MQ的特性一般一般
文献资料
开源情况部分开源,收费版ONS更强大已停止更新全面开源
整合难度一般TCC模式复杂,其他易TCC模式复杂,其他易

五、SEATA

依赖的基础环境

  • 微服务【TM/RM】必须和seata-serverTC】同属一个注册中心,不能拆分
  • seata基本上涵盖了市面上所有的注册中心,配置即可
  • seata客户端服务【TM/RM】必须要按照规则导入部分sql表,供seata对分布式事务进行协调
  • seata客户端服务【TM/RM】的全局事务组的配置,必须要在seata-server【TC】中有所对应
  • seata是分布式事务的协调者,不涉及全局事务的控制

1.整体机制

一阶段

业务数据和回滚日志记录在同一个本地事务中提交,释放本地锁和连接资源。

二阶段

2.1 提交异步化,非常快速地完成。

2.2 回滚通过一阶段的回滚日志进行反向补偿。

2.写隔离

  • 一阶段本地事务提交前,需要确保先拿到 全局锁
  • 拿不到 全局锁 ,不能提交本地事务。
  • 全局锁 的尝试被限制在一定范围内,超出范围将放弃,并回滚本地事务,释放本地锁。

在回滚、提交本地事务之前,各个分布式事务都必须拿到全局锁才行,超时则直接回归本地事务,

由于在任何情况下,全局锁都只被一个服务拿到锁,所以不会出现脏写的问题

3.读隔离

在数据库本地事务隔离级别 读已提交(Read Committed) 或以上的基础上,Seata(AT 模式)的默认全局隔离级别是 读未提交(Read Uncommitted)

4.AT模式

使用前提
  • 基于支持本地 ACID 事务的关系型数据库
  • Java应用,通过 JDBC访问数据库
底层实现

利用undo_log的逆读写能力回滚事务

数据库日志文件说明

- undo log记录更新前数据,用于保证事务原子性

- redo log记录更新后数据,用于保证事务的持久性

行为阶段
  • 一阶段 prepare 行为:在本地事务中,一并提交业务数据更新和相应回滚日志记录。
  • 二阶段 commit 行为:马上成功结束,自动 异步批量清理回滚日志。
  • 二阶段 rollback 行为:通过回滚日志,自动 生成补偿操作,完成数据回滚。

基于支持本地ACID事务的关系型数据库,利用各服务的本地事务来保证事务数据一致性

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-sbz2dFac-1596016393698)(C:\Users\dell\AppData\Roaming\Typora\typora-user-images\1595570963791.png)]

事务的管理流程

img

优点
  • 代码侵入性最低,简单的加注解方式即可实现分布式事务
  • 无阻塞,数据源代理的方式通过本地事务做提交和日志逆读写来实现回滚,效率高
  • 开发方便,学习成本较低,只需要缕清楚谁是TM的角色再加上注解即可
缺点
  • 依赖于数据库的底层事务支持
  • 对复杂业务的回滚,仅仅支持到数据库级别,非数据库级别的回滚【redismq】操作无法回滚【和传统的本地事务一样,这部分操作不可逆】

5.TCC模式

使用前提
  • 基于手动编码的方式保证事务的一致性,适合复杂业务场景
  • 对事务的一致性要求全靠人为把控,cancel阶段的异常需要额外处理做数据兜底
  • 参与全局事务的RM需要针对每个方法写单独的confirmcancel接口
行为阶段
  • 一阶段 prepare 行为:调用 自定义 的 prepare 逻辑。
  • 二阶段 commit 行为:调用 自定义 的 commit 逻辑。
  • 二阶段 rollback 行为:调用 自定义 的 rollback 逻辑。

支持把自定义的分支事务纳入到全局事务管理中,自己设计、编排业务代码的补偿机制

注意事项

TCC模式下注意允许空回滚幂等校验悬挂处理

1.允许空回滚

try未执行,Cancel执行了,导致根本就没有相应的业务数据进行回滚,出现此情况,要允许空回滚

场景

  1. try超时(丢包)

  2. 分布式事务回滚触发cancel

  3. 未收到try而收到Cancel

2.幂等校验

对于同一个分布式事务的同一个分支事务,重复去调用该分支事务的第二阶段接口,因此,要求 TCC 的二阶段 ConfirmCancel 接口保证幂等,不会重复使用或者释放资源。如果幂等控制没有做好,很有可能导致资损等严重问题

场景

网络故障、参与者宕机等都有可能造成参与者 TCC资源实际执行了二阶段防范,但是 TC 没有收到返回结果的情况,这时,TC 就会重复调用,直至调用成功,整个分布式事务结束。

为了不因为重复调用而多次占用资源,需要对服务设计时进行幂等控制

3.防悬挂

Canceltry先执行,事务管理器认为回滚成功,此时try执行数据会不一致

场景

try由于网络拥堵而超时,触发事务管理器TM回滚调用Cancel接口,而最终程序又收到了try接口,按照前面允许空回滚的逻辑的话,回滚会返回成功,但显然此时的try就不应该执行了,

否则数据就会产生不一致的情况,所以我们在处理空回滚成功之前,需要记录该全局事务的XID或者业务主键,标识这一条记录已经回滚过,try接口执行前需要进行判断,在考虑是否执行

优点
  • 跨服务、跨库,采用灵活的三方法机制通过编程模式来灵活处理事务提交、回滚
  • 异步高性能,解决了跨服务操作的原子性问题,例如:组合支付、订单减库存等场景较为实用
  • 数据库一致性完全由开发者控制,灵活度非常高
缺点
  • 代码入侵性高,每个全局事务都必须实现tryconfirmcancel接口,开发、维护成本都高
  • 需要通过编程来解决因为服务的网络、重发、宕机问题带来的空回滚幂等校验空悬挂问题,有一定技巧性【seata方面还未完全解决,需要人为考虑进去】

6.SAGA模式

使用前提
  • 适合长事务场景,每个参与者都提交本地事务,但是每个业务流程都需要依靠手动处理事务回滚
  • 依赖于事务状态机引擎保证事务一致性,需要对业务补偿流程json文件来自定义配置补偿点

Saga模式是SEATA提供的长事务解决方案,在Saga模式中,业务流程中每个参与者都提交本地事务,当出现某一个参与者失败则补偿前面已经成功的参与者,一阶段正向服务和二阶段补偿服务都由业务开发实现。

Saga模式示意图

适用场景

业务流程长、业务流程多、偏系统/体系化的全局事务

参与者包含其他公司或遗留系统服务,无法提供TCC模式要求的三个接口

优势
  • 一阶段提交本地事务,无锁,高性能
  • 事件驱动架构,参与者可异步执行,本地事务都是并行发生的,不会发生阻塞,高吞吐
  • 补偿服务易于实现
缺点
  • 难以调试,尤其是涉及到多个微服私时
  • 没有读隔离:客户正在创建一个订单,但是在下一秒,订单因为补偿交易可能会被删除
  • 面世时间较短,可参考资料较少,如果对模式的工作原理不熟悉,可能存在技术的灰色地带

7.Seata第三方基础环境

1.Nacos注册中心搭建

此处使用的注册中心为nacos,如果是其他的注册中心,同样支持,对于注册中心的搭建,交给运维即可

  • 根据naocs官网的教程走即可
2.Seata-ServerTC】搭建

seata的协调者角色,和微服务中的TMRM对应,必须使用同一个注册中心,否则无法对微服务的事务进行资源调配和管理,需要注意

3.注册中心和seata-server客户端配置

registry.confseata-serverTC】的配置,必须要保持一致!!!【可以拷贝一份到本地的resource】目录下,启动时会自动去扫描seata-server的配置,并且进行连接和注册seataTC/RM】客户端

registry {
  # file 、nacos 、eureka、redis、zk、consul、etcd3、sofa
  # 注册中心多选一,type的值来匹配下面的注册中心配置,只会有一个生效
  type = "nacos"

  nacos {
    application = "seata-server"
    serverAddr = "192.168.12.2:8848"
    group = "SEATA_GROUP"
    namespace = "36bb8351-3a3d-4b51-aefc-ef0d2ba73565"
    cluster = "default"
    username = ""
    password = ""
  }
  eureka {
    serviceUrl = "http://localhost:8761/eureka"
    application = "default"
    weight = "1"
  }
  redis {
    serverAddr = "localhost:6379"
    db = 0
    password = ""
    cluster = "default"
    timeout = 0
  }
  zk {
    cluster = "default"
    serverAddr = "127.0.0.1:2181"
    sessionTimeout = 6000
    connectTimeout = 2000
    username = ""
    password = ""
  }
  consul {
    cluster = "default"
    serverAddr = "127.0.0.1:8500"
  }
  etcd3 {
    cluster = "default"
    serverAddr = "http://localhost:2379"
  }
  sofa {
    serverAddr = "127.0.0.1:9603"
    application = "default"
    region = "DEFAULT_ZONE"
    datacenter = "DefaultDataCenter"
    cluster = "default"
    group = "SEATA_GROUP"
    addressWaitTime = "3000"
  }
  file {
    name = "file.conf"
  }
}

config {
  # file、nacos 、apollo、zk、consul、etcd3
  # 配置方式多选一,此处我们使用的是nacos
  type = "nacos"

  nacos {
    serverAddr = "192.168.12.2:8848"
    namespace = "36bb8351-3a3d-4b51-aefc-ef0d2ba73565"
    group = "SEATA_GROUP"
    username = ""
    password = ""
  }
  consul {
    serverAddr = "127.0.0.1:8500"
  }
  apollo {
    appId = "seata-server"
    apolloMeta = "http://192.168.1.204:8801"
    namespace = "application"
  }
  zk {
    serverAddr = "127.0.0.1:2181"
    sessionTimeout = 6000
    connectTimeout = 2000
    username = ""
    password = ""
  }
  etcd3 {
    serverAddr = "http://localhost:2379"
  }
  file {
    name = "file.conf"
  }
}

8. SpringCloud-AT演示

1.基本功能

business】用户购买商品 -> 【order】 创建订单记录 ->【storage】 扣减库存

business服务中,提供购买商品接口,直接远程调用orderstorage服务,做相关业务数据操作

2.项目信息
结构

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ryiqEjIc-1596016393707)(C:\Users\dell\AppData\Roaming\Typora\typora-user-images\1595473252479.png)]`

技术栈

基础架构:spring cloud Greenwich.SR2版本

注册中心:nacos 1.3.0版本

数据持久层:mybatis-plus 3.3.1版本

远程调用: feignribbon => 基于spring-boot 2.1.5.Release版本

分布式事务框架:seata 1.3.0

3.pom文件配置
1. 父pom文件
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.blue.seata</groupId>
    <artifactId>spring_cloud_seata</artifactId>
    <packaging>pom</packaging>
    <version>1.0-SNAPSHOT</version>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.1.5.RELEASE</version>
        <relativePath/>
    </parent>

    <modules>
        <module>order</module>
        <module>storage</module>
        <module>business</module>
        <module>common</module>
    </modules>

    <properties>
        <java.version>1.8</java.version>
        <spring-cloud.version>Greenwich.SR2</spring-cloud.version>
        <spring.boot.version>2.1.5.RELEASE</spring.boot.version>
        <mybatis.plus.verson>3.3.1</mybatis.plus.verson>
        <seata.version>1.3.0</seata.version>
        <alibaba-cloud.version>0.9.0.RELEASE</alibaba-cloud.version>
        <alibaba.seata.version>2.1.0.RELEASE</alibaba.seata.version>
    </properties>

    <dependencies>
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>fastjson</artifactId>
            <version>1.2.58</version>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
        </dependency>
    </dependencies>

    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-logging</artifactId>
                <version>${spring.boot.version}</version>
                <scope>compile</scope>
            </dependency>
            <dependency>
                <groupId>com.baomidou</groupId>
                <artifactId>mybatis-plus-boot-starter</artifactId>
                <version>${mybatis.plus.verson}</version>
                <optional>true</optional>
            </dependency>
            <!--定义cloud版本信息-->
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-dependencies</artifactId>
                <version>${spring-cloud.version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
            <!--阿里巴巴Nacos配置-->
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-alibaba-dependencies</artifactId>
                <version>${alibaba-cloud.version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
            <!--seata-->
            <dependency>
                <groupId>com.alibaba.cloud</groupId>
                <artifactId>spring-cloud-alibaba-seata</artifactId>
                <version>${alibaba.seata.version}</version>
                <exclusions>
                    <exclusion>
                        <artifactId>seata-all</artifactId>
                        <groupId>io.seata</groupId>
                    </exclusion>
                </exclusions>
            </dependency>
            <dependency>
                <groupId>io.seata</groupId>
                <artifactId>seata-all</artifactId>
                <version>${seata.version}</version>
            </dependency>
        </dependencies>
    </dependencyManagement>

    <!--阿里云主仓库,代理了maven central和jcenter仓库-->
    <repositories>
        <repository>
            <id>aliyun</id>
            <name>aliyun</name>
            <url>https://maven.aliyun.com/repository/public</url>
            <releases>
                <enabled>true</enabled>
            </releases>
            <snapshots>
                <enabled>false</enabled>
            </snapshots>
        </repository>
    </repositories>

    <!--profiles配置-->
    <profiles>
        <profile>
            <id>dev</id>
            <properties>
                <package.environment>dev</package.environment>
            </properties>
            <!-- 是否默认 true表示默认-->
            <activation>
                <activeByDefault>true</activeByDefault>
            </activation>
        </profile>
        <profile>
            <id>qa</id>
            <properties>
                <package.environment>qa</package.environment>
            </properties>
        </profile>
        <profile>
            <id>prod</id>
            <properties>
                <package.environment>prod</package.environment>
            </properties>
        </profile>
    </profiles>
</project>
2.子pom文件
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <parent>
        <artifactId>spring_cloud_seata</artifactId>
        <groupId>com.blue.seata</groupId>
        <version>1.0-SNAPSHOT</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>

    <artifactId>storage</artifactId>

    <dependencies>
        <!--Spring Cloud客户端全套配置-->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-nacos-config-server</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-ribbon</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-actuator</artifactId>
        </dependency>
        <!--open feign配置-->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-openfeign</artifactId>
        </dependency>
        <dependency>
            <groupId>io.github.openfeign</groupId>
            <artifactId>feign-okhttp</artifactId>
            <version>10.2.0</version>
        </dependency>
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <scope>runtime</scope>
        </dependency>
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-boot-starter</artifactId>
        </dependency>
        <dependency>
            <groupId>com.blue.seata</groupId>
            <artifactId>common</artifactId>
            <version>1.0-SNAPSHOT</version>
            <scope>compile</scope>
        </dependency>
        <!--阿里巴巴Seata分布式框架-->
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-alibaba-seata</artifactId>
            <exclusions>
                <exclusion>
                    <artifactId>seata-all</artifactId>
                    <groupId>io.seata</groupId>
                </exclusion>
            </exclusions>
        </dependency>
        <dependency>
            <groupId>io.seata</groupId>
            <artifactId>seata-all</artifactId>
        </dependency>
    </dependencies>
</project>
3.yml文件配置
1. bootstrap.yml
spring:
  application:
    name: order
  autoconfigure:
    exclude: org.springframework.cloud.netflix.eureka.EurekaClientAutoConfiguration
  cloud:
    nacos:
      discovery:
        register-enabled: true
        weight: 1
        namespace: 36bb8351-3a3d-4b51-aefc-ef0d2ba73565
        server-addr: 192.168.12.2:8848
      config:
        file-extension: yaml
        server-addr: 192.168.12.2:8848
        namespace: 36bb8351-3a3d-4b51-aefc-ef0d2ba73565
        group: SEATA_GROUP
    # seata事务组配置,需要和seata-server端的配置文件相对应
    alibaba:
      seata:
        tx-service-group: my_test_tx_group
2.application.yml
#==========================Server Config=================================
server:
  port: 7000
  servlet:
    context-path: /order
#==========================Spring Config=================================
spring:
  cloud:
    alibaba:
      seata:
        tx-service-group: my_test_tx_group
  profiles:
    active: @package.environment@  #mvn -U clean install  -Dmaven.test.skip=true -P dev
  datasource:
    url: jdbc:mysql://47.115.158.78:3306/seata_order?useUnicode=true&characterEncoding=utf8&autoReconnect=true&useSSL=false&serverTimezone=Asia/Shanghai
    username: root
    password: root
    driver-class-name: com.mysql.cj.jdbc.Driver
  swagger:
    enable: true
    security:
      filter-plugin: true
      validator-plugin: true
      username: admin
      password: admin
#========================MybatisPlus Config===============================
mybatis-plus:
  mapper-locations: classpath*:mapper/*/*Mapper.xml
  # 实体扫描,多个package用逗号或者分号分隔
  typeAliasesPackage: com.blue.seata.order.entity
  # 对应的枚举需要使用@EnumValue注解
  typeEnumsPackage: com.blue.seata.model.enums
  configuration:
    map-underscore-to-camel-case: true
    default-statement-timeout: 30000
    # 是否将sql打印到控制面板(该配置会将sql语句和查询的结果都打印到控制台)
    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
  global-config:
    # 是否自动刷新 Mapper 对应的 XML 文件
    # 不要在生产环境打开
    refresh: true
    #逻辑删除配置、表前缀设置
    db-config:
      logic-delete-value: 1
      logic-not-delete-value: 0
      table-prefix: "sys_"
      id-type: auto
    banner: false
#===================TimeOut Config=========================
hystrix:
  command:
    default:
      execution:
        isolation:
          thread:
            timeoutInMilliseconds: 100000
        timeout:
          enabled: true
ribbon:
  ConnectTimeout: 50000
  ReadTimeout: 50000

feign:
  client:
    config:
      order:
        loggerLevel: full
      storage:
        loggerLevel: full
4.数据库初始化【通用】

order.sql

DROP TABLE IF EXISTS `test_order`;
CREATE TABLE `test_order`  (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `user_id` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
  `code` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
  `count` int(11) NULL DEFAULT 0,
  `money` int(11) NULL DEFAULT 0,
  PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 27 CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;

-- ----------------------------
-- Records of test_order
-- ----------------------------
INSERT INTO `test_order` VALUES (19, 'user_admin', '001', 2, 0);

-- ----------------------------
-- Table structure for undo_log
-- ----------------------------
DROP TABLE IF EXISTS `undo_log`;
CREATE TABLE `undo_log`  (
  `id` bigint(20) NOT NULL AUTO_INCREMENT,
  `branch_id` bigint(20) NOT NULL,
  `xid` varchar(100) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL,
  `context` varchar(128) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL,
  `rollback_info` longblob NOT NULL,
  `log_status` int(11) NOT NULL,
  `log_created` datetime(0) NOT NULL,
  `log_modified` datetime(0) NOT NULL,
  `ext` varchar(100) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
  PRIMARY KEY (`id`) USING BTREE,
  UNIQUE INDEX `ux_undo_log`(`xid`, `branch_id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 14 CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;

-- ----------------------------
-- Records of undo_log
-- ----------------------------
INSERT INTO `undo_log` VALUES (12, 28419382023950336, '172.18.220.97:8091:28419359437623296', 'serializer=jackson', 0x7B7D, 1, '2020-07-20 10:08:47', '2020-07-20 10:08:47', NULL);

SET FOREIGN_KEY_CHECKS = 1;

storage.sql

-- ----------------------------
-- Table structure for test_storage
-- ----------------------------
DROP TABLE IF EXISTS `test_storage`;
CREATE TABLE `test_storage`  (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `code` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
  `count` int(11) NULL DEFAULT 0,
  PRIMARY KEY (`id`) USING BTREE,
  UNIQUE INDEX `commodity_code`(`code`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 2 CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;

-- ----------------------------
-- Records of test_storage
-- ----------------------------
INSERT INTO `test_storage` VALUES (1, '001', 98);

-- ----------------------------
-- Table structure for undo_log
-- ----------------------------
DROP TABLE IF EXISTS `undo_log`;
CREATE TABLE `undo_log`  (
  `id` bigint(20) NOT NULL AUTO_INCREMENT,
  `branch_id` bigint(20) NOT NULL,
  `xid` varchar(100) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL,
  `context` varchar(128) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL,
  `rollback_info` longblob NOT NULL,
  `log_status` int(11) NOT NULL,
  `log_created` datetime(0) NOT NULL,
  `log_modified` datetime(0) NOT NULL,
  `ext` varchar(100) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
  PRIMARY KEY (`id`) USING BTREE,
  UNIQUE INDEX `ux_undo_log`(`xid`, `branch_id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 4 CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;
  1. 全局事务配置

    1.启动类加上数据源代理注解@EnableAutoDataSourceProxy

    2.事务的起始位置加上@GlobalTransactional(name = "createOrder", timeoutMills = 60000, rollbackFor = Exception.class)

5.核心代码
1. order服务
1.1 feign接口
@FeignClient(name = "order", path = "order", fallbackFactory = RemoteOrderServiceFallbackFactory.class)
public interface RemoteOrderService {

    /**
     * 创建订单信息
     *
     * @param userId 账户
     * @param code   商品code
     * @param count  个数
     * @return json
     */
    @RequestMapping(value = "/test/create", method = RequestMethod.GET)
    JsonResult create(@RequestParam("userId") String userId, @RequestParam("code") String code, @RequestParam("count") Integer count);
}
1.2 service
@Service
public class OrderService {

    private static final Logger logger = LoggerFactory.getLogger(OrderService.class);

    @Autowired
    private OrderMapper mapper;

    @Transactional(rollbackFor = Exception.class, propagation = Propagation.REQUIRED)
    public void createOrder(String userId, String code, Integer count) {
        String xid = RootContext.getXID();
        Order order = new Order();
        order.setUserId(userId).setCode(code).setCount(count);
        this.mapper.insert(order);
        // 构建人为异常
        if (code.equals("002")) {
            throw new RuntimeException("订单中心人为异常..");
        }
        logger.info("订单中心创建订单[{}],[{}],[{}],[{}]成功", userId, code, count, xid);
    }
}
2. storage服务
2.1 feign接口
@FeignClient(name = "storage", path = "storage", fallbackFactory = RemoteStorageServiceFallbackFactory.class)
public interface RemoteStorageService {

    /**
     * 创建库存信息
     *
     * @param code  商品code
     * @param count 个数
     * @return json
     */
    @RequestMapping(value = "/test/minus", method = RequestMethod.GET)
    JsonResult minus(@RequestParam String code, @RequestParam Integer count);
}
2.2 service
@Service
public class StorageService {

    private static final Logger logger = LoggerFactory.getLogger(StorageService.class);

    @Autowired
    private StorageMapper mapper;

    @Transactional(rollbackFor = Exception.class, propagation = Propagation.REQUIRED)
    public void minus(String code, int count) {
        String xid = RootContext.getXID();
        Storage storage = this.mapper.selectOne(new LambdaQueryWrapper<Storage>().eq(Storage::getCode, code));
        storage.setCount(storage.getCount() - count);
        this.mapper.updateById(storage);
        // 构建人为异常2
        if (count < 0 || storage.getCount() < 0) {
            throw new RuntimeException("库存中心人为异常..");
        }
        logger.info("扣减库存[{}],[{}],[{}]成功", code, count, xid);
    }
}
3. business服务
1. Rest测试接口
@RestController
public class BusinessController {

    @Autowired
    private BusinessService service;

    @GetMapping("/buy")
    public boolean buy(@RequestParam String code, @RequestParam Integer count) throws InterruptedException {
        return service.buy("user_admin", code, count);
    }
}
2. service
@Service
public class BusinessService {

    private static final Logger logger = LoggerFactory.getLogger(BusinessService.class);

    @Autowired
    RemoteOrderService orderService;
    @Autowired
    RemoteStorageService storageService;

    @GlobalTransactional(name = "createOrder", timeoutMills = 60000, rollbackFor = Exception.class)
    @Transactional(rollbackFor = Exception.class, propagation = Propagation.REQUIRED)
    public boolean buy(String userId, String code, Integer count) throws InterruptedException {
        String xid = RootContext.getXID();
        logger.info("用户购买商品[{}],[{}],[{}]", code, count, xid);
        orderService.create(userId, code, count);
        logger.info("调用订单中心服务成功");
        storageService.minus(code, count);
        logger.info("调用库存中心服务成功");
//        Thread.sleep(120000);
        // 构建人为异常
        if (code.equals("001") && count == 1) {
            throw new RuntimeException("业务中心人为异常..");
        }
        return true;
    }
}
6.演示效果
  1. 数据库服务business出现异常,全局事务回滚
  2. 订单服务order出现异常,全局事务回滚
  3. 库存服务storage出现异常,全局事务回滚
  4. 接口调用超时/服务宕机,全局事务回滚

9.SpringCloud-TCC演示

【说明】

配置上基本和SpringCloud-AT模式演示一样,只是【RM】写法有所不同

1.order服务
1.1 抽离service接口

service做成一个接口和接口实现

@LocalTCC
public interface OrderService {

    /**
     * 创建订单
     *
     * @param userId 用户ID
     * @param code   商品code
     * @param count  数量
     */
    @TwoPhaseBusinessAction(name = "Tcc-Order", commitMethod = "commit", rollbackMethod = "rollback")
    void createOrder(@BusinessActionContextParameter(paramName = "userId") String userId,
                     @BusinessActionContextParameter(paramName = "code") String code,
                     @BusinessActionContextParameter(paramName = "count") Integer count);

    /**
     * Commit boolean.
     *
     * @param actionContext the action context
     * @return the boolean
     */
    boolean commit(BusinessActionContext actionContext);

    /**
     * Rollback boolean.
     *
     * @param actionContext the action context
     * @return the boolean
     */
    boolean rollback(BusinessActionContext actionContext);
}
1.2 service接口实现
@Service
public class OrderServiceImpl implements OrderService {

    private static final Logger logger = LoggerFactory.getLogger(com.blue.seata.order.service.OrderService.class);

    @Autowired
    private OrderMapper mapper;

    @Override
    @Transactional(rollbackFor = Exception.class, propagation = Propagation.REQUIRED)
    public void createOrder(String userId, String code, Integer count) {
        String xid = RootContext.getXID();
        Order order = new Order();
        order.setUserId(userId).setCode(code).setCount(count);
        this.mapper.insert(order);
        // 构建人为异常
        if (code.equals("002")) {
            throw new RuntimeException("订单中心人为异常..");
        }
        TccResultHolder.setOrderTccMap(xid, order);
        logger.info("订单中心创建订单[{}],[{}],[{}],[{}]成功", userId, code, count, xid);
    }

    @Override
    public boolean commit(BusinessActionContext actionContext) {
        String xid = actionContext.getXid();
        Object orderTccMap = TccResultHolder.getOrderTccMap(xid);
        System.out.println("TCC提交:事务上下文数据=" + orderTccMap);
        System.out.println("TCC提交, xid:" + xid + ", code:" + actionContext.getActionContext("code") + ", count:" + actionContext.getActionContext("code"));
        return true;
    }

    @Override
    public boolean rollback(BusinessActionContext actionContext) {
        String xid = actionContext.getXid();
        Order order = (Order) TccResultHolder.getOrderTccMap(xid);
        System.out.println("TCC回滚:事务上下文数据=" + order);
        System.out.println("TCC回滚业务数据,删除订单, xid:" + xid + ", code:" + actionContext.getActionContext("code") + ", count:" + actionContext.getActionContext("code"));
        if (Objects.nonNull(order)) {
            mapper.deleteById(order.getId());
        }
        return true;
    }
}
2.storage服务
1.抽离service接口
@LocalTCC
public interface StorageService {

    /**
     * Prepare boolean.
     *
     * @param code  商品code
     * @param count 数量
     * @return the boolean
     */
    @TwoPhaseBusinessAction(name = "TCC-Storage", commitMethod = "commit", rollbackMethod = "rollback")
    void minus(@BusinessActionContextParameter(paramName = "code") String code,
               @BusinessActionContextParameter(paramName = "count") int count);

    /**
     * Commit boolean.
     *
     * @param actionContext the action context
     * @return the boolean
     */
    boolean commit(BusinessActionContext actionContext);

    /**
     * Rollback boolean.
     *
     * @param actionContext the action context
     * @return the boolean
     */
    boolean rollback(BusinessActionContext actionContext);
}
2.service接口实现
@Service
public class StorageServiceImpl implements StorageService {

    private static final Logger logger = LoggerFactory.getLogger(StorageServiceImpl.class);

    @Autowired
    private StorageMapper mapper;

    @Override
    @Transactional(rollbackFor = Exception.class, propagation = Propagation.REQUIRED)
    public void minus(String code, int count) {
        String xid = RootContext.getXID();
        Storage storage = this.mapper.selectOne(new LambdaQueryWrapper<Storage>().eq(Storage::getCode, code));
        storage.setCount(storage.getCount() - count);
        this.mapper.updateById(storage);
        // 构建人为异常2
        if (count < 0 || storage.getCount() < 0) {
            throw new RuntimeException("库存中心人为异常..");
        }
        TccResultHolder.setStorageTccMap(xid, storage);
        logger.info("扣减库存[{}],[{}],[{}]成功", code, count, xid);
    }

    @Override
    public boolean commit(BusinessActionContext actionContext) {
        String xid = actionContext.getXid();
        Object storageTccMap = TccResultHolder.getStorageTccMap(xid);
        System.out.println("TCC提交:事务上下文数据=" + storageTccMap);
        System.out.println("TCC提交, xid:" + xid + ", code:" + actionContext.getActionContext("code") + ", count:" + actionContext.getActionContext("code"));
        return true;
    }

    @Override
    public boolean rollback(BusinessActionContext actionContext) {
        String xid = actionContext.getXid();
        Storage storage = (Storage) TccResultHolder.getStorageTccMap(xid);
        System.out.println("TCC回滚:事务上下文数据=" + storage);
        System.out.println("TCC回滚业务数据,回库库存数据, xid:" + xid + ", code:" + actionContext.getActionContext("code") + ", count:" + actionContext.getActionContext("code"));
        if (Objects.nonNull(storage)) {
            String count = String.valueOf(actionContext.getActionContext().get("count"));
            storage.setCount(storage.getCount() + Integer.parseInt(count));
            mapper.updateById(storage);
        }
        return true;
    }
}
3.business服务

保持不动,和章节8的配置一样即可

10.Dubbo-AT演示

【说明】

基本上和cloud版本差别不大,差的是服务暴露、接口调用、pom配置方面的区别

1.基本功能【不变】
2.基本功能
结构【不变】
技术栈

基础架构:spring-boot 2.1.5.Release版本

注册中心:nacos 1.3.0版本

数据持久层:mybatis-plus 3.3.1版本

远程调用: Dubbo 2.7.3 RPC

分布式事务框架:seata 1.3.0

3.pom文件配置
1.父pom文件
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.blue.seata</groupId>
    <artifactId>spring_cloud_seata</artifactId>
    <packaging>pom</packaging>
    <version>1.0-SNAPSHOT</version>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.1.5.RELEASE</version>
        <relativePath/>
    </parent>

    <modules>
        <module>order</module>
        <module>storage</module>
        <module>business</module>
        <module>common</module>
    </modules>

    <properties>
        <java.version>1.8</java.version>
        <dubbo.version>2.7.3</dubbo.version>
        <nacos.client.version>1.1.4</nacos.client.version>
        <seata.version>1.3.0</seata.version>
        <mybtis.plus.version>3.3.1</mybtis.plus.version>
        <spring.boot.version>2.1.5.RELEASE</spring.boot.version>
        <seata.config.version>2.1.0.Release</seata.config.version>
    </properties>

    <dependencies>
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>fastjson</artifactId>
            <version>1.2.58</version>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
        </dependency>
    </dependencies>

    <dependencyManagement>
        <dependencies>
            <!-- Spring Boot -->
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-dependencies</artifactId>
                <version>${spring.boot.version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
            <dependency>
                <groupId>com.baomidou</groupId>
                <artifactId>mybatis-plus-boot-starter</artifactId>
                <version>${mybtis.plus.version}</version>
                <optional>true</optional>
            </dependency>
            <!-- Dubbo Spring Boot Starter -->
            <dependency>
                <groupId>org.apache.dubbo</groupId>
                <artifactId>dubbo-spring-boot-starter</artifactId>
                <version>${dubbo.version}</version>
            </dependency>
            <dependency>
                <groupId>org.apache.dubbo</groupId>
                <artifactId>dubbo</artifactId>
                <version>${dubbo.version}</version>
                <exclusions>
                    <exclusion>
                        <groupId>org.springframework</groupId>
                        <artifactId>spring</artifactId>
                    </exclusion>
                    <exclusion>
                        <groupId>javax.servlet</groupId>
                        <artifactId>servlet-api</artifactId>
                    </exclusion>
                    <exclusion>
                        <groupId>log4j</groupId>
                        <artifactId>log4j</artifactId>
                    </exclusion>
                </exclusions>
            </dependency>
            <!--dubbo-nacos配置-->
            <dependency>
                <groupId>org.apache.dubbo</groupId>
                <artifactId>dubbo-registry-nacos</artifactId>
                <version>${dubbo.version}</version>
            </dependency>
            <dependency>
                <groupId>com.alibaba.nacos</groupId>
                <artifactId>nacos-client</artifactId>
                <version>${nacos.client.version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
            <!--Seata分布式事务-->
            <dependency>
                <groupId>com.alibaba.cloud</groupId>
                <artifactId>spring-cloud-alibaba-seata</artifactId>
                <version>${seata.config.version}</version>
                <exclusions>
                    <exclusion>
                        <artifactId>seata-all</artifactId>
                        <groupId>io.seata</groupId>
                    </exclusion>
                </exclusions>
            </dependency>
            <dependency>
                <groupId>io.seata</groupId>
                <artifactId>seata-all</artifactId>
                <version>${seata.version}</version>
            </dependency>
            <!--AspectJ切面-->
            <dependency>
                <groupId>org.aspectj</groupId>
                <artifactId>aspectjweaver</artifactId>
                <version>${aspectj.version}</version>
            </dependency>
        </dependencies>
    </dependencyManagement>

    <!--阿里云主仓库,代理了maven central和jcenter仓库-->
    <repositories>
        <repository>
            <id>aliyun</id>
            <name>aliyun</name>
            <url>https://maven.aliyun.com/repository/public</url>
            <releases>
                <enabled>true</enabled>
            </releases>
            <snapshots>
                <enabled>false</enabled>
            </snapshots>
        </repository>
    </repositories>

    <!--profiles配置-->
    <profiles>
        <profile>
            <id>dev</id>
            <properties>
                <package.environment>dev</package.environment>
            </properties>
            <!-- 是否默认 true表示默认-->
            <activation>
                <activeByDefault>true</activeByDefault>
            </activation>
        </profile>
        <profile>
            <id>qa</id>
            <properties>
                <package.environment>qa</package.environment>
            </properties>
        </profile>
        <profile>
            <id>prod</id>
            <properties>
                <package.environment>prod</package.environment>
            </properties>
        </profile>
    </profiles>
</project>
2.子pom文件
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <parent>
        <artifactId>spring_cloud_seata</artifactId>
        <groupId>com.blue.seata</groupId>
        <version>1.0-SNAPSHOT</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>

    <packaging>jar</packaging>
    <artifactId>order</artifactId>

    <dependencies>
        <!-- Dubbo -->
        <dependency>
            <groupId>org.apache.dubbo</groupId>
            <artifactId>dubbo-spring-boot-starter</artifactId>
        </dependency>
        <dependency>
            <groupId>org.apache.dubbo</groupId>
            <artifactId>dubbo</artifactId>
        </dependency>
        <!-- Dubbo Registry Nacos -->
        <dependency>
            <groupId>org.apache.dubbo</groupId>
            <artifactId>dubbo-registry-nacos</artifactId>
        </dependency>
        <dependency>
            <groupId>com.alibaba.nacos</groupId>
            <artifactId>nacos-client</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-actuator</artifactId>
        </dependency>
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <scope>runtime</scope>
        </dependency>
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-boot-starter</artifactId>
        </dependency>
        <dependency>
            <groupId>com.blue.seata</groupId>
            <artifactId>common</artifactId>
            <version>1.0-SNAPSHOT</version>
            <scope>compile</scope>
        </dependency>
        <!--阿里巴巴Seata分布式框架-->
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-alibaba-seata</artifactId>
            <exclusions>
                <exclusion>
                    <artifactId>seata-all</artifactId>
                    <groupId>io.seata</groupId>
                </exclusion>
            </exclusions>
        </dependency>
        <dependency>
            <groupId>io.seata</groupId>
            <artifactId>seata-all</artifactId>
            <version>1.3.0</version>
        </dependency>
    </dependencies>
</project>
4.yml文件配置

没有bootstrap.yml文件,只有application.yml

#==============================Server Config=========================================
server:
  port: 7000
  servlet:
    context-path: /order
#==============================Spring Config=========================================
spring:
  cloud:
    alibaba:
      seata:
        tx-service-group: my_test_tx_group
  application:
    name: order
  profiles:
    active: @package.environment@  #mvn -U clean install  -Dmaven.test.skip=true -P dev
  datasource:
    url: jdbc:mysql://47.115.158.78:3306/seata_order?useUnicode=true&characterEncoding=utf8&autoReconnect=true&useSSL=false&serverTimezone=Asia/Shanghai
    username: root
    password: root
    driver-class-name: com.mysql.cj.jdbc.Driver
mybatis-plus:
  mapper-locations: classpath*:mapper/*/*Mapper.xml
  # 实体扫描,多个package用逗号或者分号分隔
  typeAliasesPackage: com.blue.seata.order.entity
  # 对应的枚举需要使用@EnumValue注解
  typeEnumsPackage: com.blue.seata.model.enums
  configuration:
    map-underscore-to-camel-case: true
    default-statement-timeout: 30000
    # 是否将sql打印到控制面板(该配置会将sql语句和查询的结果都打印到控制台)
    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
  global-config:
    # 是否自动刷新 Mapper 对应的 XML 文件
    # 不要在生产环境打开
    refresh: true
    #逻辑删除配置、表前缀设置
    db-config:
      logic-delete-value: 1
      logic-not-delete-value: 0
      table-prefix: "sys_"
      id-type: auto
    banner: false
5.Dubbo接口配置xml
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:dubbo="http://dubbo.apache.org/schema/dubbo"
       xsi:schemaLocation="http://www.springframework.org/schema/beans        http://www.springframework.org/schema/beans/spring-beans-4.3.xsd        http://dubbo.apache.org/schema/dubbo        http://dubbo.apache.org/schema/dubbo/dubbo.xsd">

    <!-- 使用Nacos的注册中心 -->
    <dubbo:registry address="nacos://192.168.12.2:8848">
        <dubbo:parameter key="namespace" value="36bb8351-3a3d-4b51-aefc-ef0d2ba73565"/>
    </dubbo:registry>
    <!--Dubbo协议配置,注意服务之间的端口冲突-->
    <dubbo:protocol name="dubbo" port="20882"  threadpool="fixed"  threads="100"/>
    <!-- 提供方应用信息,用于计算依赖关系 -->
    <dubbo:application name="dubbo-provider-order"/>
    <!-- 声明需要暴露的服务接口 -->
    <dubbo:service interface="com.blue.seata.order.service.OrderService" ref="orderService"/>
</beans>
6.核心代码

10.Dubbo-TCC调用

【说明】

Dubbo-AT模式基本上一致,只是在service接口的开发有所区别,另外,参与分布式事务的接口,不必和

SpringCloud-TCC演示一样,需要使用@LocalTCC注解,此处和平常的接口dubbo接口开发无异

核心代码
1. order服务
1.service接口
public interface OrderService {

    /**
     * 创建订单接口
     *
     * @param actionContext 事务上下文【可选参数】
     * @param userId        用户ID
     * @param code          商品code码
     * @param count         数量
     */
    @TwoPhaseBusinessAction(name = "TCC-Order", commitMethod = "commit", rollbackMethod = "rollback")
    void createOrder(BusinessActionContext actionContext, @BusinessActionContextParameter(paramName = "userId") String userId,
                     @BusinessActionContextParameter(paramName = "code") String code,
                     @BusinessActionContextParameter(paramName = "count") Integer count);

    /**
     * Commit boolean.
     *
     * @param actionContext the action context
     * @return the boolean
     */
    boolean commit(BusinessActionContext actionContext);

    /**
     * Rollback boolean.
     *
     * @param actionContext the action context
     * @return the boolean
     */
    boolean rollback(BusinessActionContext actionContext);
}
2.service实现
@Service("orderService")
public class OrderServiceImpl implements OrderService {

    private static final Logger logger = LoggerFactory.getLogger(OrderServiceImpl.class);

    @Autowired
    private OrderMapper mapper;

    @Override
    @Transactional(rollbackFor = Exception.class, propagation = Propagation.REQUIRED)
    public void createOrder(BusinessActionContext actionContext, String userId, String code, Integer count) {
        String xid = RootContext.getXID();
        Order order = new Order();
        order.setUserId(userId).setCode(code).setCount(count);
        this.mapper.insert(order);
        // 构建人为异常
        if (code.equals("002")) {
            throw new RuntimeException("订单中心人为异常..");
        }
        TccResultHolder.setOrderTccMap(xid, order);
        logger.info("订单中心创建订单[{}],[{}],[{}],[{}]成功", userId, code, count, xid);
    }

    @Override
    public boolean commit(BusinessActionContext actionContext) {
        String xid = actionContext.getXid();
        Object orderTccMap = TccResultHolder.getOrderTccMap(xid);
        System.out.println("TCC提交:事务上下文数据=" + orderTccMap);
        System.out.println("TCC提交, xid:" + xid + ", code:" + actionContext.getActionContext("code") + ", count:" + actionContext.getActionContext("code"));
        return true;
    }

    @Override
    public boolean rollback(BusinessActionContext actionContext) {
        String xid = actionContext.getXid();
        Order order = (Order) TccResultHolder.getOrderTccMap(xid);
        System.out.println("TCC回滚:事务上下文数据=" + order);
        System.out.println("TCC回滚业务数据,删除订单, xid:" + xid + ", code:" + actionContext.getActionContext("code") + ", count:" + actionContext.getActionContext("code"));
        if (Objects.nonNull(order)) {
            mapper.deleteById(order.getId());
        }
        return true;
    }
}
2.storage服务
1.service接口
public interface StorageService {
    /**
     * Prepare boolean.
     *
     * @param actionContext 事务上下文【可选参数】
     * @param code          商品code
     * @param count         数量
     * @return the boolean
     */
    @TwoPhaseBusinessAction(name = "TCC-Storage", commitMethod = "commit", rollbackMethod = "rollback")
    void minus(BusinessActionContext actionContext,
               @BusinessActionContextParameter(paramName = "code") String code,
               @BusinessActionContextParameter(paramName = "count") int count);

    /**
     * Commit boolean.
     *
     * @param actionContext the action context
     * @return the boolean
     */
    boolean commit(BusinessActionContext actionContext);

    /**
     * Rollback boolean.
     *
     * @param actionContext the action context
     * @return the boolean
     */
    boolean rollback(BusinessActionContext actionContext);
}
2.service实现
@Service("storageService")
public class StorageServiceImpl implements StorageService {

    private static final Logger logger = LoggerFactory.getLogger(StorageServiceImpl.class);

    @Autowired
    private StorageMapper mapper;

    @Override
    @Transactional(rollbackFor = Exception.class, propagation = Propagation.REQUIRED)
    public void minus(BusinessActionContext actionContext, String code, int count) {
        String xid = RootContext.getXID();
        Storage storage = this.mapper.selectOne(new LambdaQueryWrapper<Storage>().eq(Storage::getCode, code));
        storage.setCount(storage.getCount() - count);
        this.mapper.updateById(storage);
        // 构建人为异常2
        if (count == 100) {
            throw new RuntimeException("库存中心人为异常..");
        }
        TccResultHolder.setStorageTccMap(xid, storage);
        logger.info("扣减库存[{}],[{}],[{}]成功", code, count, xid);
    }

    @Override
    public boolean commit(BusinessActionContext actionContext) {
        String xid = actionContext.getXid();
        Object storageTccMap = TccResultHolder.getStorageTccMap(xid);
        System.out.println("TCC提交:事务上下文数据=" + storageTccMap);
        System.out.println("TCC提交, xid:" + xid + ", code:" + actionContext.getActionContext("code") + ", count:" + actionContext.getActionContext("code"));
        return true;
    }

    @Override
    public boolean rollback(BusinessActionContext actionContext) {
        String xid = actionContext.getXid();
        Storage storage = (Storage) TccResultHolder.getStorageTccMap(xid);
        System.out.println("TCC回滚:事务上下文数据=" + storage);
        System.out.println("TCC回滚业务数据,回库库存数据, xid:" + xid + ", code:" + actionContext.getActionContext("code") + ", count:" + actionContext.getActionContext("code"));
        if (Objects.nonNull(storage)) {
            String count = String.valueOf(actionContext.getActionContext().get("count"));
            storage.setCount(storage.getCount() + Integer.parseInt(count));
            mapper.updateById(storage);
        }
        return true;
    }
}
3.business服务

和之前的版本无差异

11.Eureka-AT版本

【说明】

SpringCloud-AT基本没有变化,除了注册中心是eureka,其他唯一的变化就是在seata的配置文件registry.conf上而已,此外,我使用的是seata的配置方式为file模式,因此多了一个配置文件file.conf,这个根据自己需要配置即可,代码上无任何区别,只是多了一个file.conf文件,不管seata的配置、注册怎么变,我们只需要和seata-serverTC】保持一致即可,务必保证两端的配置一下,这样才可以使seata插件生效!

registry.conf
registry {
  # file 、nacos 、eureka、redis、zk、consul、etcd3、sofa
  type = "eureka"

  nacos {
    serverAddr = "localhost"
    namespace = ""
    cluster = "default"
  }
  eureka {
    serviceUrl = "http://eureka.springcloud.cn/eureka/"
    application = "SEATA"
    weight = "1"
  }
  redis {
    serverAddr = "localhost:6379"
    db = "0"
    password = ""
    cluster = "default"
    timeout = "0"
  }
  zk {
    cluster = "default"
    serverAddr = "127.0.0.1:2181"
    session.timeout = 6000
    connect.timeout = 2000
    username = ""
    password = ""
  }
  consul {
    cluster = "default"
    serverAddr = "127.0.0.1:8500"
  }
  etcd3 {
    cluster = "default"
    serverAddr = "http://localhost:2379"
  }
  sofa {
    serverAddr = "127.0.0.1:9603"
    application = "default"
    region = "DEFAULT_ZONE"
    datacenter = "DefaultDataCenter"
    cluster = "default"
    group = "SEATA_GROUP"
    addressWaitTime = "3000"
  }
  file {
    name = "file.conf"
  }
}

config {
  # file、nacos 、apollo、zk、consul、etcd3、springCloudConfig
  type = "file"

  nacos {
    serverAddr = "localhost"
    namespace = ""
    group = "SEATA_GROUP"
  }
  consul {
    serverAddr = "127.0.0.1:8500"
  }
  apollo {
    app.id = "seata-server"
    apollo.meta = "http://192.168.1.204:8801"
    namespace = "application"
  }
  zk {
    serverAddr = "127.0.0.1:2181"
    session.timeout = 6000
    connect.timeout = 2000
    username = ""
    password = ""
  }
  etcd3 {
    serverAddr = "http://localhost:2379"
  }
  file {
    name = "file.conf"
  }
}
file.conf
transport {
  # tcp udt unix-domain-socket
  type = "TCP"
  #NIO NATIVE
  server = "NIO"
  #enable heartbeat
  heartbeat = true
  # the client batch send request enable
  enableClientBatchSendRequest = true
  #thread factory for netty
  threadFactory {
    bossThreadPrefix = "NettyBoss"
    workerThreadPrefix = "NettyServerNIOWorker"
    serverExecutorThread-prefix = "NettyServerBizHandler"
    shareBossWorker = false
    clientSelectorThreadPrefix = "NettyClientSelector"
    clientSelectorThreadSize = 1
    clientWorkerThreadPrefix = "NettyClientWorkerThread"
    # netty boss thread size,will not be used for UDT
    bossThreadSize = 1
    #auto default pin or 8
    workerThreadSize = "default"
  }
  shutdown {
    # when destroy server, wait seconds
    wait = 3
  }
  serialization = "seata"
  compressor = "none"
}
service {
  #transaction service group mapping
  vgroupMapping.test_seata_group = "SEATA"
  #only support when registry.type=file, please don't set multiple addresses
  SEATA.grouplist = "127.0.0.1:8091"
  #degrade, current not support
  enableDegrade = false
  #disable seata
  disableGlobalTransaction = false
}

client {
  rm {
    asyncCommitBufferLimit = 10000
    lock {
      retryInterval = 10
      retryTimes = 30
      retryPolicyBranchRollbackOnConflict = true
    }
    reportRetryCount = 5
    tableMetaCheckEnable = false
    reportSuccessEnable = false
  }
  tm {
    commitRetryCount = 5
    rollbackRetryCount = 5
  }
  undo {
    dataValidation = true
    logSerialization = "jackson"
    logTable = "undo_log"
  }
  log {
    exceptionRate = 100
  }
}

12.开发要点

  1. seata框架有 3 种形式可以代理数据源:

    1. 依赖 seata-spring-boot-starter 时,自动代理数据源,无需额外处理
    2. 依赖seata-all 时,使用 @EnableAutoDataSourceProxy (since 1.1.0) 注解,注解参数可选择 jdk代理或者 cglib 代理
    3. 依赖 seata-all 时,也可以手动使用 DatasourceProxy来包装 DataSource

    尝试过将其存放到yml文件,但是目前官网demo中,还是没有推荐此用法,后续可能会扩展。

  2. 配置 GlobalTransactionScanner,使用 seata-all时需要手动配置,使用seata-spring-boot-starter 时无需额外处理

  3. 参与全局事务的业务表中必须包含单列主键,暂不支持复合主键,建议先建一个自增id主键 。

  4. 每个业务库中必须包含 undo_log 表,若与分库分表组件联用,分库不分表。

  5. 跨微服务链路的事务需要对相应 RPC 框架支持,目前 seata-all 中已经支持:Apache DubboAlibaba Dubbosofa-RPCMotangRpchttpClient,对于 Spring Cloud的支持,请大家引用 spring-cloud-alibaba-seata。其他自研框架、异步模型、消息消费事务模型请结合 API自行支持。

  6. 目前AT模式支持的数据库有:MySQLOraclePostgreSQLTiDB

  7. 使用注解开启分布式事务时,若默认服务 provider 端加入 consumer 端的事务,provider可不标注注解。但是,provider 同样需要相应的依赖和配置,仅可省略注解。

  8. 使用注解开启分布式事务时,若要求事务回滚,必须将异常抛出到事务的发起方,被事务发起方的 @GlobalTransactional 注解感知到。provide 直接抛出异常 或 定义错误码由 consumer 判断再抛出异常。

  9. 是否可以不使用conf类型配置文件,直接将配置写入配置文件?

    目前seata-all包需要使用conf类型的配置文件,后续升级才有可能支持配置文件的写法。

    当前项目可以通过依赖seata-spring-boot-starter,然后将配置项写入到配置文件,这样可以不使用conf类型的文件

  10. TCC模式下dubbospring cloud下的区别?

    SpringCloud服务接口设计到分布式事务时,需要加上@LocalTCC注解,其他编程方式无区别

  11. 日志出现no available service 'null' found, please make sure registry config correct

    说明seata配置没有和seata-server对应上,需要检查配置,尤其是事务组的配置
    另外一个原因就是你的模块没有引入spring-cloud-alibaba-seata依赖引起的

  12. seata配置成功的标识?

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-g2jJcu6N-1596016393711)(C:\Users\dell\AppData\Roaming\Typora\typora-user-images\1596014052839.png)]

  13. 关于TCCconfirm接口的开发,如何与try接口区分开?

    建议在confirm处做打印和记录即可【放空处理】,无需再里面写太多的数据、业务层面操作,将所有的操作堆放到try即可,这样只需要去关注trycancel的业务处理结果以及异常。

评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值