这里写目录标题
一、简介
背景
分布式事务是跨系统、跨机器之间的事务,由于其不满足单机的ACID
特性,所以较普通事务来说复杂了很多
而对微服务而言,其实就是微服务接口调用不同的微服务时,涉及到跨库的事务数据一致性的问题,尤其是在服务
调用过程中,针对异常,对数据一致性的要求尤为苛刻
二、理论依据
CAP原则
CAP
原则又称CAP
定理,指的是在一个分布式系统中, Consistency
(一致性)、 Availability
(可用性)、Partition tolerance
(分区容错性),三者不可得兼。
分布式系统的CAP
理论:理论首先把分布式系统中的三个特性进行了如下归纳:
- 一致性(C):在分布式系统中的所有数据备份,在同一时刻是否同样的值。(等同于所有节点访问同一份最新的数据副本)
- 可用性(A):在集群中一部分节点故障后,集群整体是否还能响应客户端的读写请求。(对数据更新具备高可用性)
- 分区容忍性(P):以实际效果而言,分区相当于对通信的时限要求。系统如果不能在时限内达成数据一致性,就意味着发生了分区的情况,必须就当前操作在C和A之间做出选择。
按照CAP
理论,我们选择分布式事务的解决方案时,必然要在CP
和AP
之间做选择,剩下的一个我们则尽量去保证,这就需要根据不同业务场景来选择。
BASE
理论
BASE
是Basically Available
(基本可用)、Soft state
(软状态)和Eventually consistent
(最终一致性)三个短语的简写,BASE是对CAP中一致性和可用性权衡的结果,其来源于对大规模互联网系统分布式实践的结论,是基于CAP
定理逐步演化而来的,其核心思想是即使无法做到强一致性(Strong consistency
),但每个应用都可以根据自身的业务特点,采用适当的方式来使系统达到最终一致性(Eventual consistency
)。接下来我们着重对BASE
中的三要素进行详细讲解。
基本可用
基本可用是指分布式系统在出现不可预知故障的时候,允许损失部分可用性——但请注意,这绝不等价于系统不可用,以下两个就是“基本可用”的典型例子。
- 响应时间上的损失:正常情况下,一个在线搜索引擎需要0.5秒内返回给用户相应的查询结果,但由于出现异常(比如系统部分机房发生断电或断网故障),查询结果的响应时间增加到了1~2秒。
- 功能上的损失:正常情况下,在一个电子商务网站上进行购物,消费者几乎能够顺利地完成每一笔订单,但是在一些节日大促购物高峰的时候,由于消费者的购物行为激增,为了保护购物系统的稳定性,部分消费者可能会被引导到一个降级页面。
弱状态也称为软状态,和硬状态相对,是指允许系统中的数据存在中间状态,并认为该中间状态的存在不会影响系统的整体可用性,即允许系统在不同节点的数据副本之间进行数据听不的过程存在延时。
最终一致性【有延迟】
最终一致性强调的是系统中所有的数据副本,在经过一段时间的同步后,最终能够达到数据一致的状态。
因此,最终一致性的本质是需要系统保证最终数据能够达到一致,而不需要实时保证系统数据的强一致性。
三、柔性事务解决方案
柔性事务满足BASE
理论(基本可用,最终一致),刚性事务满足ACID
理论
一、两阶段型
1.1 XA两段式【基础】
阶段一:提交事务请求
1、事务询问。协调者向所有参与者发送事务内容,询问是否可以进行事务提交操作,然后就开始等待参与者的响应。
2、执行事务。各参与者节点执行事务操作,并将Undo
和Redo
信息记入事务日志中。
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
请求,执行事务操作后,将Undo
和Redo
信息记录到事务日志中。
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
支持事务消息,也需要手动回滚、补偿事务,对于rabbitmq
和kafka
都不支持事务消息,付费的RocketMq
变种Ons
比免费更强大。
五、主流分布式事务中间件
分布式事务中间件其本身并不创建事务,而是基于对本地事务的协调从而达到事务一致性的效果
5.1 SEATA
【推荐】
简介
源自阿里云的全局事务服务【GTS
】的框架,高性能且易于使用,旨在实现简单并快速的事务提交、回滚。
已为用户提供了AT
、TCC
、SAGA
三种事务模式,为用户打造一站式的分布式解决方案.2014年面世到现在
已经更新了6年时间。
源码: https://github.com/seata/seata
文档: http://seata.io/zh-cn/
特点:最终一致性,可能存在脏读
事务组成
TC-事务协调者
维护全局和分支事务的状态,驱动全局事务提交或回滚。
TM-事务管理器
定义全局事务的范围:开始全局事务、提交或回滚全局事务。
RM-资源管理器
管理分支事务处理的资源,与TC
交谈以注册分支事务和报告分支事务的状态,并驱动分支事务提交或回滚。
5.2 TX-LCN
四、方案比较
核心 | RocketMq | TX-LCN | SEATA |
---|---|---|---|
兼容性 | 不存在兼容性问题 | SpringCloud ,Dubbo | SpringCloud ,Dubbo |
高可用 | 支持集群 | 组件支持集群 | 支持集群化 |
事务机制 | ack 、事务消息机制 | TXC -代理转本地事务处理 | AT -根据undo_log 逆sql |
模式 | MQ 需要支持事务消息 | 支持TCC 、TXC | 支持AT 、TCC 、SAGA 、XA |
扩展性 | 接入RocketMq 即可 | 强 | 强 |
CAP 理论 | 最终一致性 | 【CP 】强一致性,可能死锁 | 【AP 】最终一致性,可能脏读 |
界面管理 | 有 | 有 | 无 |
性能 | 高 | 一般 | 高 |
开发难度 | 复杂,更关注MQ 的特性 | 一般 | 一般 |
文献资料 | 多 | 多 | 多 |
开源情况 | 部分开源,收费版ONS 更强大 | 已停止更新 | 全面开源 |
整合难度 | 一般 | TCC 模式复杂,其他易 | TCC 模式复杂,其他易 |
五、SEATA
依赖的基础环境
- 微服务【
TM/RM
】必须和seata-server
【TC
】同属一个注册中心,不能拆分 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
事务的关系型数据库,利用各服务的本地事务来保证事务数据一致性
事务的管理流程
优点
- 代码侵入性最低,简单的加注解方式即可实现分布式事务
- 无阻塞,数据源代理的方式通过本地事务做提交和日志逆读写来实现回滚,效率高
- 开发方便,学习成本较低,只需要缕清楚谁是
TM
的角色再加上注解即可
缺点
- 依赖于数据库的底层事务支持
- 对复杂业务的回滚,仅仅支持到数据库级别,非数据库级别的回滚【
redis
、mq
】操作无法回滚【和传统的本地事务一样,这部分操作不可逆】
5.TCC
模式
使用前提
- 基于手动编码的方式保证事务的一致性,适合复杂业务场景
- 对事务的一致性要求全靠人为把控,
cancel
阶段的异常需要额外处理做数据兜底 - 参与全局事务的
RM
需要针对每个方法写单独的confirm
和cancel
接口
行为阶段
- 一阶段 prepare 行为:调用 自定义 的 prepare 逻辑。
- 二阶段 commit 行为:调用 自定义 的 commit 逻辑。
- 二阶段 rollback 行为:调用 自定义 的 rollback 逻辑。
支持把自定义的分支事务纳入到全局事务管理中,自己设计、编排业务代码的补偿机制
注意事项
TCC
模式下注意允许空回滚
、幂等校验
、悬挂处理
1.允许空回滚
try
未执行,Cancel
执行了,导致根本就没有相应的业务数据进行回滚,出现此情况,要允许空回滚
场景
-
try
超时(丢包) -
分布式事务回滚触发
cancel
-
未收到
try
而收到Cancel
2.幂等校验
对于同一个分布式事务的同一个分支事务,重复去调用该分支事务的第二阶段接口,因此,要求 TCC
的二阶段 Confirm
和 Cancel
接口保证幂等,不会重复使用或者释放资源。如果幂等控制没有做好,很有可能导致资损等严重问题
场景
网络故障、参与者宕机等都有可能造成参与者 TCC
资源实际执行了二阶段防范,但是 TC 没有收到返回结果的情况,这时,TC
就会重复调用,直至调用成功,整个分布式事务结束。
为了不因为重复调用而多次占用资源,需要对服务设计时进行幂等控制
3.防悬挂
Cancel
比try
先执行,事务管理器认为回滚成功,此时try
执行数据会不一致
场景
try
由于网络拥堵而超时,触发事务管理器TM
回滚调用Cancel
接口,而最终程序又收到了try
接口,按照前面允许空回滚的逻辑的话,回滚会返回成功,但显然此时的try就不应该执行了,
否则数据就会产生不一致的情况,所以我们在处理空回滚成功之前,需要记录该全局事务的XID
或者业务主键,标识这一条记录已经回滚过,try
接口执行前需要进行判断,在考虑是否执行
优点
- 跨服务、跨库,采用灵活的三方法机制通过编程模式来灵活处理事务提交、回滚
- 异步高性能,解决了跨服务操作的原子性问题,例如:组合支付、订单减库存等场景较为实用
- 数据库一致性完全由开发者控制,灵活度非常高
缺点
- 代码入侵性高,每个全局事务都必须实现
try
、confirm
、cancel
接口,开发、维护成本都高 - 需要通过编程来解决因为服务的网络、重发、宕机问题带来的
空回滚
、幂等校验
、空悬挂
问题,有一定技巧性【seata
方面还未完全解决,需要人为考虑进去】
6.SAGA
模式
使用前提
- 适合长事务场景,每个参与者都提交本地事务,但是每个业务流程都需要依靠手动处理事务回滚
- 依赖于事务状态机引擎保证事务一致性,需要对业务补偿流程
json
文件来自定义配置补偿点
Saga
模式是SEATA
提供的长事务解决方案,在Saga
模式中,业务流程中每个参与者都提交本地事务,当出现某一个参与者失败则补偿前面已经成功的参与者,一阶段正向服务和二阶段补偿服务都由业务开发实现。
适用场景
业务流程长、业务流程多、偏系统/体系化的全局事务
参与者包含其他公司或遗留系统服务,无法提供TCC
模式要求的三个接口
优势
- 一阶段提交本地事务,无锁,高性能
- 事件驱动架构,参与者可异步执行,本地事务都是并行发生的,不会发生阻塞,高吞吐
- 补偿服务易于实现
缺点
- 难以调试,尤其是涉及到多个微服私时
- 没有读隔离:客户正在创建一个订单,但是在下一秒,订单因为补偿交易可能会被删除
- 面世时间较短,可参考资料较少,如果对模式的工作原理不熟悉,可能存在技术的灰色地带
7.Seata
第三方基础环境
1.Nacos
注册中心搭建
此处使用的注册中心为nacos
,如果是其他的注册中心,同样支持,对于注册中心的搭建,交给运维即可
- 根据
naocs
官网的教程走即可
2.Seata-Server
【TC
】搭建
seata
的协调者角色,和微服务中的TM
和RM
对应,必须使用同一个注册中心,否则无法对微服务的事务进行资源调配和管理,需要注意
3.注册中心和seata-server
客户端配置
registry.conf
是seata-server
【TC
】的配置,必须要保持一致!!!【可以拷贝一份到本地的resource
】目录下,启动时会自动去扫描seata-server
的配置,并且进行连接和注册seata
【TC/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
服务中,提供购买商品接口,直接远程调用order
、storage
服务,做相关业务数据操作
2.项目信息
结构
技术栈
基础架构:spring cloud Greenwich.SR2版本
注册中心:nacos 1.3.0版本
数据持久层:mybatis-plus 3.3.1版本
远程调用: feign
、ribbon
=> 基于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.启动类加上数据源代理注解
@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.演示效果
- 数据库服务
business
出现异常,全局事务回滚 - 订单服务
order
出现异常,全局事务回滚 - 库存服务
storage
出现异常,全局事务回滚 - 接口调用超时/服务宕机,全局事务回滚
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-server
【TC
】保持一致即可,务必保证两端的配置一下,这样才可以使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.开发要点
-
seata
框架有 3 种形式可以代理数据源:- 依赖
seata-spring-boot-starter
时,自动代理数据源,无需额外处理 - 依赖
seata-all
时,使用@EnableAutoDataSourceProxy
(since 1.1.0) 注解,注解参数可选择jdk
代理或者cglib
代理 - 依赖
seata-all
时,也可以手动使用DatasourceProxy
来包装DataSource
尝试过将其存放到
yml
文件,但是目前官网demo
中,还是没有推荐此用法,后续可能会扩展。 - 依赖
-
配置
GlobalTransactionScanner
,使用seata-all
时需要手动配置,使用seata-spring-boot-starter
时无需额外处理 -
参与全局事务的业务表中必须包含单列主键,暂不支持复合主键,建议先建一个自增
id
主键 。 -
每个业务库中必须包含
undo_log
表,若与分库分表组件联用,分库不分表。 -
跨微服务链路的事务需要对相应 RPC 框架支持,目前 seata-all 中已经支持:
Apache Dubbo
、Alibaba Dubbo
、sofa-RPC
、Motan
、gRpc
、httpClient
,对于Spring Cloud
的支持,请大家引用spring-cloud-alibaba-seata
。其他自研框架、异步模型、消息消费事务模型请结合API
自行支持。 -
目前AT模式支持的数据库有:
MySQL
、Oracle
、PostgreSQL
和TiDB
。 -
使用注解开启分布式事务时,若默认服务 provider 端加入 consumer 端的事务,
provider
可不标注注解。但是,provider
同样需要相应的依赖和配置,仅可省略注解。 -
使用注解开启分布式事务时,若要求事务回滚,必须将异常抛出到事务的发起方,被事务发起方的
@GlobalTransactional
注解感知到。provide
直接抛出异常 或 定义错误码由consumer
判断再抛出异常。 -
是否可以不使用
conf
类型配置文件,直接将配置写入配置文件?目前
seata-all
包需要使用conf
类型的配置文件,后续升级才有可能支持配置文件的写法。当前项目可以通过依赖
seata-spring-boot-starter
,然后将配置项写入到配置文件,这样可以不使用conf
类型的文件 -
TCC
模式下dubbo
和spring cloud
下的区别?SpringCloud
服务接口设计到分布式事务时,需要加上@LocalTCC
注解,其他编程方式无区别 -
日志出现
no available service 'null' found, please make sure registry config correct
说明
seata
配置没有和seata-server
对应上,需要检查配置,尤其是事务组的配置
另外一个原因就是你的模块没有引入spring-cloud-alibaba-seata依赖引起的 -
seata
配置成功的标识? -
关于
TCC
下confirm
接口的开发,如何与try
接口区分开?建议在
confirm
处做打印和记录即可【放空处理】,无需再里面写太多的数据、业务层面操作,将所有的操作堆放到try
即可,这样只需要去关注try
和cancel
的业务处理结果以及异常。