微服务实战(五) Seata 分布式事务
官方文档:https://seata.io/zh-cn/
概述
Seata 是一款开源的分布式事务解决方案,致力于提供高性能和简单易用的分布式事务服务。Seata 将为用户提供了 AT、TCC、SAGA 和 XA 事务模式,为用户打造一站式的分布式解决方案。
Seata 是阿里开源的分布式事务框架,属于二阶段提交模式。
Seata术语
-
TC (Transaction Coordinator) - 事务协调者
维护全局和分支事务的状态,驱动全局事务提交或回滚。
-
TM (Transaction Manager) - 事务管理器
定义全局事务的范围:开始全局事务、提交或回滚全局事务。
-
RM (Resource Manager) - 资源管理器
管理分支事务处理的资源,与TC交谈以注册分支事务和报告分支事务的状态,并驱动分支事务提交或回滚。
AT工作模式
图解
单体应用架构
微服务应用架构
Seate处理事务流程
工作机制
两阶段提交协议的演变:
- 一阶段:业务数据和回滚日志记录在同一个本地事务中提交,释放本地锁和连接资源。
- 二阶段:
- 提交异步化,非常快速地完成。
- 回滚通过一阶段的回滚日志进行反向补偿。
一阶段(1~4)
二阶段-回滚(5~7)
二阶段-提交(8)
- Business 是业务入口,在程序中会通过注解(
@GlobalTransactional
)来说明他是一个全局事务,这时他的角色为 TM(事务管理者) - Business 会请求 TC(事务协调器,一个独立运行的服务),说明自己要开启一个全局事务,TC 会生成一个全局事务ID(XID),并返回给 Business
- Business 得到 XID 后,开始调用微服务,例如调用 Storage。
- Storage 会收到 XID,知道自己的事务属于这个全局事务。Storage 执行自己的业务逻辑,执行业务 SQL,并把前后镜像数据以及业务 SQL 相关的信息组成一条回滚日志记录,插入到
UNDO_LOG
表中。向 TC 注册分支, 申请全局锁,拿到锁后提交本地事务,将本地事务提交的结果上报给 TC。 Storage 的角色是 RM(资源管理者),管理分支事务处理的资源。 - 如果所有分支都没有报错,TM向TC发起全局事务提交请求,TC再向RM发送提交请求。如果存在分支报错,TM向TC发起全局事务回滚请求,TC在向个RM发送分支事务回滚请求,
- RM收到TC 的分支回滚请求,开启一个本地事务,通过 XID 和 Branch ID 查找到相应的 UNDO LOG 记录。如果回滚日志存在:将后置镜像与当前数据对比,如果数据一致表示可以回滚(没有发生脏写),通过回滚日志的前置镜像生成回滚SQL, 执行数据回滚。如果回滚日志不存在:插入一条状态为全局事物已完成(数据库的值是: 1 )的回滚日志, 避免另一个线程提交成功。提交本地事务,并把本地事务的执行结果(即分支事务回滚的结果)上报给 TC。
- 如果存在分支回滚失败,TC会重试发起分支回滚请求。当分支都回滚成功,TC向分支发送提交请求。
- RM收到 TC 的分支提交请求,把请求放入一个异步任务的队列中,马上返回提交成功的结果给 TC。 异步任务阶段的分支提交请求将异步和批量地删除相应 UNDO LOG 记录。
启动服务端
- 这里服务端存储模式采用的为db模式,服务注册和配置中心为Nacos
下载安装包
下载地址:https://github.com/seata/seata/releases
#解压到指定文件夹
tar -zxvf ./seata-server-1.3.0.tar.gz -C /sunny/software/
下载资源
下载地址:https://github.com/seata/seata/tree/1.3.0/script
目录结构
#存放client端sql脚本 (包含 undo_log表) ,参数配置
client
#各个配置中心参数导入脚本,config.txt(包含server和client,原名nacos-config.txt)为通用参数文件
config-center
#server端数据库脚本 (包含 lock_table、branch_table 与 global_table) 及各个容器配置
server
client
需要客户端 添加undo_log表
config-center
将
config-center
目录下的config.txt
放到seata安装目录下,nacos-config.sh
放到seata bin目录下server
Mysql创建数据库seata,执行server目录下的sql脚本,创建表lock_table、branch_table 与 global_table。
修改配置
cd /sunny/software/seata/conf/
#这里采用的Nacos的服务注册与用户中心 所以不需要修改file.conf 只需要修改registry.conf 填写Nacos相关配置
vim ./registry.conf
#修改注册配置
registry{
# file 、nacos 、eureka、redis、zk、consul、etcd3、sofa
type = "nacos"
nacos {
application = "seata-server"
serverAddr = "127.0.0.1:8848"
group = "SEATA_GROUP"
namespace = "e90d261b-9c05-4bcb-b99f-b419d952737a"
cluster = "default"
username = "nacos"
password = "nacos"
}
}
config {
# file、nacos 、apollo、zk、consul、etcd3
type = "nacos"
nacos {
serverAddr = "127.0.0.1:8848"
namespace = "e90d261b-9c05-4bcb-b99f-b419d952737a"
group = "SEATA_GROUP"
username = "nacos"
password = "nacos"
}
}
推送配置到Nacos
- 推送配置到Nacos,需要前面下载资源中config-center的
config.txt
和nacos-config.sh
修改config.txt
- service.vgroupMapping.my_test_tx_group:my_test_tx_group为seata客户端事务组名称,需要与客户端配置相同,多个则配置多行
- service.default.grouplist :seata服务端地址
- store.mode=file:默认file ,需要修改为db
- store.db.url=…:数据库地址
- store.db.user=username:数据库用户名
- store.db.password=password:数据库密码
- store.db.driverClassName:mysql8.0 需要改为com.mysql.cj.jdbc.Driver
transport.type=TCP
transport.server=NIO
transport.heartbeat=true
transport.enableClientBatchSendRequest=false
transport.threadFactory.bossThreadPrefix=NettyBoss
transport.threadFactory.workerThreadPrefix=NettyServerNIOWorker
transport.threadFactory.serverExecutorThreadPrefix=NettyServerBizHandler
transport.threadFactory.shareBossWorker=false
transport.threadFactory.clientSelectorThreadPrefix=NettyClientSelector
transport.threadFactory.clientSelectorThreadSize=1
transport.threadFactory.clientWorkerThreadPrefix=NettyClientWorkerThread
transport.threadFactory.bossThreadSize=1
transport.threadFactory.workerThreadSize=default
transport.shutdown.wait=3
service.vgroupMapping.sunnyws-seata-order-group=default
service.vgroupMapping.sunnyws-seata-storage-group=default
service.default.grouplist=172.16.220.50:8091
service.enableDegrade=false
service.disableGlobalTransaction=false
client.rm.asyncCommitBufferLimit=10000
client.rm.lock.retryInterval=10
client.rm.lock.retryTimes=30
client.rm.lock.retryPolicyBranchRollbackOnConflict=true
client.rm.reportRetryCount=5
client.rm.tableMetaCheckEnable=false
client.rm.tableMetaCheckerInterval=60000
client.rm.sqlParserType=druid
client.rm.reportSuccessEnable=false
client.rm.sagaBranchRegisterEnable=false
client.tm.commitRetryCount=5
client.tm.rollbackRetryCount=5
client.tm.defaultGlobalTransactionTimeout=60000
client.tm.degradeCheck=false
client.tm.degradeCheckAllowTimes=10
client.tm.degradeCheckPeriod=2000
store.mode=file
store.publicKey=
store.file.dir=file_store/data
store.file.maxBranchSessionSize=16384
store.file.maxGlobalSessionSize=512
store.file.fileWriteBufferCacheSize=16384
store.file.flushDiskMode=async
store.file.sessionReloadReadSize=100
store.db.datasource=druid
store.db.dbType=mysql
store.db.driverClassName=com.mysql.cj.jdbc.Driver
store.db.url=jdbc:mysql://172.16.220.50:3306/seata?useUnicode=true&rewriteBatchedStatements=true
store.db.user=root
store.db.password=root
store.db.minConn=5
store.db.maxConn=30
store.db.globalTable=global_table
store.db.branchTable=branch_table
store.db.queryLimit=100
store.db.lockTable=lock_table
store.db.maxWait=5000
store.redis.mode=single
store.redis.single.host=127.0.0.1
store.redis.single.port=6379
store.redis.maxConn=10
store.redis.minConn=1
store.redis.maxTotal=100
store.redis.database=0
store.redis.password=
store.redis.queryLimit=100
server.recovery.committingRetryPeriod=1000
server.recovery.asynCommittingRetryPeriod=1000
server.recovery.rollbackingRetryPeriod=1000
server.recovery.timeoutRetryPeriod=1000
server.maxCommitRetryTimeout=-1
server.maxRollbackRetryTimeout=-1
server.rollbackRetryTimeoutUnlockEnable=false
client.undo.dataValidation=true
client.undo.logSerialization=jackson
client.undo.onlyCareUpdateColumns=true
server.undo.logSaveDays=7
server.undo.logDeletePeriod=86400000
client.undo.logTable=undo_log
client.undo.compress.enable=true
client.undo.compress.type=zip
client.undo.compress.threshold=64k
log.exceptionRate=100
transport.serialization=seata
transport.compressor=none
metrics.enabled=false
metrics.registryType=compact
metrics.exporterList=prometheus
metrics.exporterPrometheusPort=9898
推送配置
#-h -p 指定nacos的端口地址;
#-g 指定配置的分组,注意,是配置的分组;
#-t 指定命名空间id;
#-u -w指定nacos的用户名和密码
#需要已经启动Nacos
sh nacos-config.sh -h 127.0.0.1 -p 8848 -g SEATA_GROUP -t e90d261b-9c05-4bcb-b99f-b419d952737a -u nacos -w nacos
#示例
[root@localhost bin]# sh nacos-config.sh -h 127.0.0.1 -p 8848 -g SEATA_GROUP -t e90d261b-9c05-4bcb-b99f-b419d952737a -u nacos -w nacos
set nacosAddr=127.0.0.1:8848
set group=SEATA_GROUP
Set transport.type=TCP successfully
Set transport.server=NIO successfully
Set transport.heartbeat=true successfully
Set transport.enableClientBatchSendRequest=false successfully
Set transport.threadFactory.bossThreadPrefix=NettyBoss successfully
Set transport.threadFactory.workerThreadPrefix=NettyServerNIOWorker successfully
Set transport.threadFactory.serverExecutorThreadPrefix=NettyServerBizHandler successfully
Set transport.threadFactory.shareBossWorker=false successfully
Set transport.threadFactory.clientSelectorThreadPrefix=NettyClientSelector successfully
Set transport.threadFactory.clientSelectorThreadSize=1 successfully
Set transport.threadFactory.clientWorkerThreadPrefix=NettyClientWorkerThread successfully
Set transport.threadFactory.bossThreadSize=1 successfully
Set transport.threadFactory.workerThreadSize=default successfully
Set transport.shutdown.wait=3 successfully
Set service.vgroupMapping.my_test_tx_group=default successfully
Set service.default.grouplist=127.0.0.1:8091 successfully
Set service.enableDegrade=false successfully
启动服务端
#-h: 注册到注册中心的ip
#-p: Server rpc 监听端口
#-m: 全局事务会话信息存储模式,file、db、redis,优先读取启动参数 (Seata-Server 1.3及以上版本支持redis)
#-n: Server node,多个Server时,需区分各自节点,用于生成不同区间的transactionId,以免冲突
#-e: 多环境配置参考 http://seata.io/en-us/docs/ops/multi-configuration-isolation.html
#前台启动
seata-server.sh -h 172.16.220.50 -p 8091 -m db -n 1
#后台启动
nohup ./seata-server.sh -h 172.16.220.50 -p 8091 -m db -n 1 > ./nohuo.out 2>&1 &
业务系统集成Client
- Nacos+seata
引入依赖
<!-- SpringCloud Ailibaba Nacos -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<!-- SpringCloud Ailibaba seata -->
<dependency>
<groupId>io.seata</groupId>
<artifactId>seata-spring-boot-starter</artifactId>
<version>1.4.1</version>
</dependency>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-seata</artifactId>
<exclusions>
<exclusion>
<groupId>io.seata</groupId>
<artifactId>seata-spring-boot-starter</artifactId>
</exclusion>
</exclusions>
</dependency>
<!-- feign -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
引入数据库进行测试
<!-- mybatis-plus -->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>${mybatis-plus.version}</version>
</dependency>
<!-- druid -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>${druid.version}</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>${mysql.version}</version>
<scope>runtime</scope>
</dependency>
添加配置
- 服务端service.vgroupMapping.XXX配置一般与application.name相同,也与客户端tx-service-group配置相同
server:
port: 8002
spring:
application:
name: sunnyws-seata-order
profiles:
active: dev
cloud:
nacos:
discovery:
server-addr: 172.16.220.50:8848
namespace: e90d261b-9c05-4bcb-b99f-b419d952737a
datasource:
type: com.alibaba.druid.pool.DruidDataSource
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://172.16.220.50:3306/seata_order?useUnicode=true&characterEncoding=utf-8&useSSL=false&serverTimezone = GMT&allowPublicKeyRetrieval=true
username: root
password: root
seata:
enabled: true
application-id: ${spring.application.name} # seata 应用编号,默认为 ${spring.application.name}
tx-service-group: sunnyws-seata-order-group # seata 事务组编号,用于 TC 集群名
# seata 服务配置项,对应 ServiceProperties 类
service:
grouplist:
default: 172.16.220.50:8091
# 虚拟组和分组的映射, key一定要与 my_test_tx_group一致
vgroupMapping:
sunnyws-seata-order-group: default
#不禁用全局事务
disable-global-transaction: false
添加注解
- 业务上添加
@GlobalTransactional
注解
测试
- sunnyws-seata-order 下订单
- sunnyws-seata-storage 减库存
- 客户端配置都同上面相同
- 当通过feign调用storage 减库存后,抛出异常,查看是否回滚。
将本地@Transactional
去除后,可以在异常抛出前看到订单和库存都已更新到数据库,并且客户端数据库undo_log
表和seata服务端数据global_table
表都生成了记录,xid也都相同。
当放开断点,抛出异常后,订单和库存也都回到之前的数据,undo_log和服务端的事务记录也被删除掉了。
作者: SunnyWs
链接: https://sunnyws.com/posts/7a1077b7/
来源: SunnyWs’Blog