分布式事务是跨系统、跨机器之间的事务,由于其不满足单机的ACID
特性,所以较普通事务来说复杂了很多
而对微服务而言,其实就是微服务接口调用不同的微服务时,涉及到跨库的事务数据一致性的问题,尤其是在服务
调用过程中,针对异常,对数据一致性的要求尤为苛刻
CAP
原则又称CAP
定理,指的是在一个分布式系统中, Consistency
(一致性)、 Availability
(可用性)、Partition tolerance
(分区容错性),三者不可得兼。
分布式系统的CAP
理论:理论首先把分布式系统中的三个特性进行了如下归纳:
按照CAP
理论,我们选择分布式事务的解决方案时,必然要在CP
和AP
之间做选择,剩下的一个我们则尽量去保证,这就需要根据不同业务场景来选择。
BASE
理论BASE
是Basically Available
(基本可用)、Soft state
(软状态)和Eventually consistent
(最终一致性)三个短语的简写,BASE是对CAP中一致性和可用性权衡的结果,其来源于对大规模互联网系统分布式实践的结论,是基于CAP
定理逐步演化而来的,其核心思想是即使无法做到强一致性(Strong consistency
),但每个应用都可以根据自身的业务特点,采用适当的方式来使系统达到最终一致性(Eventual consistency
)。接下来我们着重对BASE
中的三要素进行详细讲解。
基本可用是指分布式系统在出现不可预知故障的时候,允许损失部分可用性——但请注意,这绝不等价于系统不可用,以下两个就是“基本可用”的典型例子。
弱状态也称为软状态,和硬状态相对,是指允许系统中的数据存在中间状态,并认为该中间状态的存在不会影响系统的整体可用性,即允许系统在不同节点的数据副本之间进行数据听不的过程存在延时。
最终一致性强调的是系统中所有的数据副本,在经过一段时间的同步后,最终能够达到数据一致的状态。
因此,最终一致性的本质是需要系统保证最终数据能够达到一致,而不需要实时保证系统数据的强一致性。
柔性事务满足BASE
理论(基本可用,最终一致),刚性事务满足ACID
理论
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)]
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
的参与者数据不一致。
全称: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
比免费更强大。
分布式事务中间件其本身并不创建事务,而是基于对本地事务的协调从而达到事务一致性的效果
SEATA
【推荐】源自阿里云的全局事务服务【GTS
】的框架,高性能且易于使用,旨在实现简单并快速的事务提交、回滚。
已为用户提供了AT
、TCC
、SAGA
三种事务模式,为用户打造一站式的分布式解决方案.2014年面世到现在
已经更新了6年时间。
源码: https://github.com/seata/seata
文档: http://seata.io/zh-cn/
特点:最终一致性,可能存在脏读
维护全局和分支事务的状态,驱动全局事务提交或回滚。
定义全局事务的范围:开始全局事务、提交或回滚全局事务。
管理分支事务处理的资源,与TC
交谈以注册分支事务和报告分支事务的状态,并驱动分支事务提交或回滚。
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
是分布式事务的协调者,不涉及全局事务的控制业务数据和回滚日志记录在同一个本地事务中提交,释放本地锁和连接资源。
2.1 提交异步化,非常快速地完成。
2.2 回滚通过一阶段的回滚日志进行反向补偿。
在回滚、提交本地事务之前,各个分布式事务都必须拿到全局锁才行,超时则直接回归本地事务,
由于在任何情况下,全局锁都只被一个服务拿到锁,所以不会出现脏写的问题
在数据库本地事务隔离级别 读已提交(Read Committed) 或以上的基础上,Seata
(AT 模式)的默认全局隔离级别是 读未提交(Read Uncommitted)
AT
模式ACID
事务的关系型数据库Java
应用,通过 JDBC
访问数据库利用undo_log
的逆读写能力回滚事务
数据库日志文件说明
- undo log
记录更新前数据,用于保证事务原子性
- redo log
记录更新后数据,用于保证事务的持久性
基于支持本地ACID
事务的关系型数据库,利用各服务的本地事务来保证事务数据一致性
事务的管理流程
TM
的角色再加上注解即可redis
、mq
】操作无法回滚【和传统的本地事务一样,这部分操作不可逆】TCC
模式cancel
阶段的异常需要额外处理做数据兜底RM
需要针对每个方法写单独的confirm
和cancel
接口支持把自定义的分支事务纳入到全局事务管理中,自己设计、编排业务代码的补偿机制
TCC
模式下注意允许空回滚
、幂等校验
、悬挂处理
try
未执行,Cancel
执行了,导致根本就没有相应的业务数据进行回滚,出现此情况,要允许空回滚
场景
try
超时(丢包)
分布式事务回滚触发cancel
未收到try
而收到Cancel
对于同一个分布式事务的同一个分支事务,重复去调用该分支事务的第二阶段接口,因此,要求 TCC
的二阶段 Confirm
和 Cancel
接口保证幂等,不会重复使用或者释放资源。如果幂等控制没有做好,很有可能导致资损等严重问题
场景
网络故障、参与者宕机等都有可能造成参与者 TCC
资源实际执行了二阶段防范,但是 TC 没有收到返回结果的情况,这时,TC
就会重复调用,直至调用成功,整个分布式事务结束。
为了不因为重复调用而多次占用资源,需要对服务设计时进行幂等控制
Cancel
比try
先执行,事务管理器认为回滚成功,此时try
执行数据会不一致
场景
try
由于网络拥堵而超时,触发事务管理器TM
回滚调用Cancel
接口,而最终程序又收到了try
接口,按照前面允许空回滚的逻辑的话,回滚会返回成功,但显然此时的try就不应该执行了,
否则数据就会产生不一致的情况,所以我们在处理空回滚成功之前,需要记录该全局事务的XID
或者业务主键,标识这一条记录已经回滚过,try
接口执行前需要进行判断,在考虑是否执行
try
、confirm
、cancel
接口,开发、维护成本都高空回滚
、幂等校验
、空悬挂
问题,有一定技巧性【seata
方面还未完全解决,需要人为考虑进去】SAGA
模式json
文件来自定义配置补偿点Saga
模式是SEATA
提供的长事务解决方案,在Saga
模式中,业务流程中每个参与者都提交本地事务,当出现某一个参与者失败则补偿前面已经成功的参与者,一阶段正向服务和二阶段补偿服务都由业务开发实现。
业务流程长、业务流程多、偏系统/体系化的全局事务
参与者包含其他公司或遗留系统服务,无法提供TCC
模式要求的三个接口
Seata
第三方基础环境Nacos
注册中心搭建此处使用的注册中心为nacos
,如果是其他的注册中心,同样支持,对于注册中心的搭建,交给运维即可
naocs
官网的教程走即可Seata-Server
【TC
】搭建seata
的协调者角色,和微服务中的TM
和RM
对应,必须使用同一个注册中心,否则无法对微服务的事务进行资源调配和管理,需要注意
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"
}
}
SpringCloud-AT
演示【business
】用户购买商品 -> 【order
】 创建订单记录 ->【storage
】 扣减库存
business
服务中,提供购买商品接口,直接远程调用order
、storage
服务,做相关业务数据操作
基础架构: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
pom
文件配置pom
文件
<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.0modelVersion>
<groupId>com.blue.seatagroupId>
<artifactId>spring_cloud_seataartifactId>
<packaging>pompackaging>
<version>1.0-SNAPSHOTversion>
<parent>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-parentartifactId>
<version>2.1.5.RELEASEversion>
<relativePath/>
parent>
<modules>
<module>ordermodule>
<module>storagemodule>
<module>businessmodule>
<module>commonmodule>
modules>
<properties>
<java.version>1.8java.version>
<spring-cloud.version>Greenwich.SR2spring-cloud.version>
<spring.boot.version>2.1.5.RELEASEspring.boot.version>
<mybatis.plus.verson>3.3.1mybatis.plus.verson>
<seata.version>1.3.0seata.version>
<alibaba-cloud.version>0.9.0.RELEASEalibaba-cloud.version>
<alibaba.seata.version>2.1.0.RELEASEalibaba.seata.version>
properties>
<dependencies>
<dependency>
<groupId>com.alibabagroupId>
<artifactId>fastjsonartifactId>
<version>1.2.58version>
dependency>
<dependency>
<groupId>org.projectlombokgroupId>
<artifactId>lombokartifactId>
dependency>
dependencies>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-loggingartifactId>
<version>${spring.boot.version}version>
<scope>compilescope>
dependency>
<dependency>
<groupId>com.baomidougroupId>
<artifactId>mybatis-plus-boot-starterartifactId>
<version>${mybatis.plus.verson}version>
<optional>trueoptional>
dependency>
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-dependenciesartifactId>
<version>${spring-cloud.version}version>
<type>pomtype>
<scope>importscope>
dependency>
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-alibaba-dependenciesartifactId>
<version>${alibaba-cloud.version}version>
<type>pomtype>
<scope>importscope>
dependency>
<dependency>
<groupId>com.alibaba.cloudgroupId>
<artifactId>spring-cloud-alibaba-seataartifactId>
<version>${alibaba.seata.version}version>
<exclusions>
<exclusion>
<artifactId>seata-allartifactId>
<groupId>io.seatagroupId>
exclusion>
exclusions>
dependency>
<dependency>
<groupId>io.seatagroupId>
<artifactId>seata-allartifactId>
<version>${seata.version}version>
dependency>
dependencies>
dependencyManagement>
<repositories>
<repository>
<id>aliyunid>
<name>aliyunname>
<url>https://maven.aliyun.com/repository/publicurl>
<releases>
<enabled>trueenabled>
releases>
<snapshots>
<enabled>falseenabled>
snapshots>
repository>
repositories>
<profiles>
<profile>
<id>devid>
<properties>
<package.environment>devpackage.environment>
properties>
<activation>
<activeByDefault>trueactiveByDefault>
activation>
profile>
<profile>
<id>qaid>
<properties>
<package.environment>qapackage.environment>
properties>
profile>
<profile>
<id>prodid>
<properties>
<package.environment>prodpackage.environment>
properties>
profile>
profiles>
project>
pom
文件
<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_seataartifactId>
<groupId>com.blue.seatagroupId>
<version>1.0-SNAPSHOTversion>
parent>
<modelVersion>4.0.0modelVersion>
<artifactId>storageartifactId>
<dependencies>
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discoveryartifactId>
dependency>
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-alibaba-nacos-config-serverartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-webartifactId>
dependency>
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-netflix-ribbonartifactId>
dependency>
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-netflix-hystrixartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-actuatorartifactId>
dependency>
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-openfeignartifactId>
dependency>
<dependency>
<groupId>io.github.openfeigngroupId>
<artifactId>feign-okhttpartifactId>
<version>10.2.0version>
dependency>
<dependency>
<groupId>mysqlgroupId>
<artifactId>mysql-connector-javaartifactId>
<scope>runtimescope>
dependency>
<dependency>
<groupId>com.baomidougroupId>
<artifactId>mybatis-plus-boot-starterartifactId>
dependency>
<dependency>
<groupId>com.blue.seatagroupId>
<artifactId>commonartifactId>
<version>1.0-SNAPSHOTversion>
<scope>compilescope>
dependency>
<dependency>
<groupId>com.alibaba.cloudgroupId>
<artifactId>spring-cloud-alibaba-seataartifactId>
<exclusions>
<exclusion>
<artifactId>seata-allartifactId>
<groupId>io.seatagroupId>
exclusion>
exclusions>
dependency>
<dependency>
<groupId>io.seatagroupId>
<artifactId>seata-allartifactId>
dependency>
dependencies>
project>
yml
文件配置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
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
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)
order
服务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);
}
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);
}
}
storage
服务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);
}
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);
}
}
business
服务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);
}
}
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;
}
}
business
出现异常,全局事务回滚order
出现异常,全局事务回滚storage
出现异常,全局事务回滚SpringCloud-TCC
演示配置上基本和SpringCloud-AT
模式演示一样,只是【RM
】写法有所不同
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);
}
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;
}
}
storage
服务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);
}
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;
}
}
business
服务保持不动,和章节8的配置一样即可
Dubbo-AT
演示基本上和cloud
版本差别不大,差的是服务暴露、接口调用、pom
配置方面的区别
基础架构:spring-boot 2.1.5.Release
版本
注册中心:nacos 1.3.0版本
数据持久层:mybatis-plus 3.3.1版本
远程调用: Dubbo 2.7.3 RPC
分布式事务框架:seata 1.3.0
pom
文件配置pom
文件
<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.0modelVersion>
<groupId>com.blue.seatagroupId>
<artifactId>spring_cloud_seataartifactId>
<packaging>pompackaging>
<version>1.0-SNAPSHOTversion>
<parent>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-parentartifactId>
<version>2.1.5.RELEASEversion>
<relativePath/>
parent>
<modules>
<module>ordermodule>
<module>storagemodule>
<module>businessmodule>
<module>commonmodule>
modules>
<properties>
<java.version>1.8java.version>
<dubbo.version>2.7.3dubbo.version>
<nacos.client.version>1.1.4nacos.client.version>
<seata.version>1.3.0seata.version>
<mybtis.plus.version>3.3.1mybtis.plus.version>
<spring.boot.version>2.1.5.RELEASEspring.boot.version>
<seata.config.version>2.1.0.Releaseseata.config.version>
properties>
<dependencies>
<dependency>
<groupId>com.alibabagroupId>
<artifactId>fastjsonartifactId>
<version>1.2.58version>
dependency>
<dependency>
<groupId>org.projectlombokgroupId>
<artifactId>lombokartifactId>
dependency>
dependencies>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-dependenciesartifactId>
<version>${spring.boot.version}version>
<type>pomtype>
<scope>importscope>
dependency>
<dependency>
<groupId>com.baomidougroupId>
<artifactId>mybatis-plus-boot-starterartifactId>
<version>${mybtis.plus.version}version>
<optional>trueoptional>
dependency>
<dependency>
<groupId>org.apache.dubbogroupId>
<artifactId>dubbo-spring-boot-starterartifactId>
<version>${dubbo.version}version>
dependency>
<dependency>
<groupId>org.apache.dubbogroupId>
<artifactId>dubboartifactId>
<version>${dubbo.version}version>
<exclusions>
<exclusion>
<groupId>org.springframeworkgroupId>
<artifactId>springartifactId>
exclusion>
<exclusion>
<groupId>javax.servletgroupId>
<artifactId>servlet-apiartifactId>
exclusion>
<exclusion>
<groupId>log4jgroupId>
<artifactId>log4jartifactId>
exclusion>
exclusions>
dependency>
<dependency>
<groupId>org.apache.dubbogroupId>
<artifactId>dubbo-registry-nacosartifactId>
<version>${dubbo.version}version>
dependency>
<dependency>
<groupId>com.alibaba.nacosgroupId>
<artifactId>nacos-clientartifactId>
<version>${nacos.client.version}version>
<type>pomtype>
<scope>importscope>
dependency>
<dependency>
<groupId>com.alibaba.cloudgroupId>
<artifactId>spring-cloud-alibaba-seataartifactId>
<version>${seata.config.version}version>
<exclusions>
<exclusion>
<artifactId>seata-allartifactId>
<groupId>io.seatagroupId>
exclusion>
exclusions>
dependency>
<dependency>
<groupId>io.seatagroupId>
<artifactId>seata-allartifactId>
<version>${seata.version}version>
dependency>
<dependency>
<groupId>org.aspectjgroupId>
<artifactId>aspectjweaverartifactId>
<version>${aspectj.version}version>
dependency>
dependencies>
dependencyManagement>
<repositories>
<repository>
<id>aliyunid>
<name>aliyunname>
<url>https://maven.aliyun.com/repository/publicurl>
<releases>
<enabled>trueenabled>
releases>
<snapshots>
<enabled>falseenabled>
snapshots>
repository>
repositories>
<profiles>
<profile>
<id>devid>
<properties>
<package.environment>devpackage.environment>
properties>
<activation>
<activeByDefault>trueactiveByDefault>
activation>
profile>
<profile>
<id>qaid>
<properties>
<package.environment>qapackage.environment>
properties>
profile>
<profile>
<id>prodid>
<properties>
<package.environment>prodpackage.environment>
properties>
profile>
profiles>
project>
pom
文件
<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_seataartifactId>
<groupId>com.blue.seatagroupId>
<version>1.0-SNAPSHOTversion>
parent>
<modelVersion>4.0.0modelVersion>
<packaging>jarpackaging>
<artifactId>orderartifactId>
<dependencies>
<dependency>
<groupId>org.apache.dubbogroupId>
<artifactId>dubbo-spring-boot-starterartifactId>
dependency>
<dependency>
<groupId>org.apache.dubbogroupId>
<artifactId>dubboartifactId>
dependency>
<dependency>
<groupId>org.apache.dubbogroupId>
<artifactId>dubbo-registry-nacosartifactId>
dependency>
<dependency>
<groupId>com.alibaba.nacosgroupId>
<artifactId>nacos-clientartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-webartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-actuatorartifactId>
dependency>
<dependency>
<groupId>mysqlgroupId>
<artifactId>mysql-connector-javaartifactId>
<scope>runtimescope>
dependency>
<dependency>
<groupId>com.baomidougroupId>
<artifactId>mybatis-plus-boot-starterartifactId>
dependency>
<dependency>
<groupId>com.blue.seatagroupId>
<artifactId>commonartifactId>
<version>1.0-SNAPSHOTversion>
<scope>compilescope>
dependency>
<dependency>
<groupId>com.alibaba.cloudgroupId>
<artifactId>spring-cloud-alibaba-seataartifactId>
<exclusions>
<exclusion>
<artifactId>seata-allartifactId>
<groupId>io.seatagroupId>
exclusion>
exclusions>
dependency>
<dependency>
<groupId>io.seatagroupId>
<artifactId>seata-allartifactId>
<version>1.3.0version>
dependency>
dependencies>
project>
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
Dubbo
接口配置xml
<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">
<dubbo:registry address="nacos://192.168.12.2:8848">
<dubbo:parameter key="namespace" value="36bb8351-3a3d-4b51-aefc-ef0d2ba73565"/>
dubbo:registry>
<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>
Dubbo-TCC
调用和Dubbo-AT
模式基本上一致,只是在service
接口的开发有所区别,另外,参与分布式事务的接口,不必和
SpringCloud-TCC演示
一样,需要使用@LocalTCC
注解,此处和平常的接口dubbo
接口开发无异
order
服务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);
}
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;
}
}
storage
服务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);
}
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;
}
}
business
服务和之前的版本无差异
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
}
}
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-al
包需要使用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
对应上,需要检查配置,尤其是事务组的配置
seata
配置成功的标识?
关于TCC
下confirm
接口的开发,如何与try
接口区分开?
建议在confirm
处做打印和记录即可【放空处理】,无需再里面写太多的数据、业务层面操作,将所有的操作堆放到try
即可,这样只需要去关注try
和cancel
的业务处理结果以及异常。