Seata 分布式事务
单体应用被拆分成微服务应用,原来的多个模块被拆分成多个独立的应用,分别使用多个独立的数据源,业务操作需要调用多个服务来完成。此时每个服务内部的数据一致性由本地事务来保证,但是全局的数据一致性问题没有办法保证。
简介
Seata是一款开源的分布式事务解决方案,旨在解决分布式事务场景下的数据一致性问题。Seata提供了一系列的技术手段,如TC(事务协调者)、TM(事务管理器)、RM(资源管理器)等,来确保分布式事务的正确性和一致性。
Seata的工作原理是通过TC来协调各个RM,实现全局事务的管理。当一个分布式事务开始时,TM会向TC注册一个全局事务,然后TC会向各个RM注册分支事务。在分支事务执行完成后,RM会将事务执行结果通知TC,TC会根据所有分支事务的执行结果来决定是否提交或回滚全局事务。
Seata支持多种分布式事务模式,如AT模式、TCC模式、Saga模式等。同时,Seata还提供了一些高级功能,如分布式锁、分布式ID、分布式事务日志等,以满足不同场景下的需求。
TC(Transaction Coordinator,事务协调者)
是全局事务的协调者,负责协调各个RM的工作。当一个分布式事务开始时,TM会向TC注册一个全局事务,然后TC会向各个RM注册分支事务。在分支事务执行完成后,RM会将事务执行结果通知TC,TC会根据所有分支事务的执行结果来决定是否提交或回滚全局事务。TM(Transaction Manager,事务管理器)
负责全局事务的注册和提交/回滚。当一个分布式事务开始时,TM会向TC注册一个全局事务,然后根据事务的执行结果来决定是否提交或回滚全局事务。RM(Resource Manager,资源管理器)
负责本地事务的执行和管理。当TC向RM注册分支事务后,RM会执行分支事务并将执行结果报告给TC,然后根据TC的指示来提交或回滚本地事务。
Seata的处理流程
- TM向TC注册全局事务,全局事务创建成功并生成一个全局唯一的XID。
- XID在微服务调用链路的上下文中传播。
- RM向TC注册分支事务,将其纳入XID对应全局事务的管辖。
- TM向TC发起针对XID的全局提交或回滚决议。
- TC调度XID下管辖的全部分支事务完成提交或者回滚请求。
安装Seata
安装之前
Seata分TC、TM和RM三个角色,TC(Server端)为单独服务端部署,TM和RM(Client端)由业务系统集成。本文使用docker安装。
安装之前按照官网的说法Seata Server 1.5.0版本开始,配置文件改为application.yml,所以在使用自定义配置的时候,需要先把原生配置拷贝出来。
为了获取seata server 1.5.0的配置文件,我们需要先启动一个seata server 1.5.0的服务,然后再从启动的容器实例中把默认的配置文件复制出来,再进行修改。
先启动一个容器
version: "3.8" services: seata-server: image: seataio/seata-server:${latest-release-version} container_name: seata-server ports: - "7091:7091" - "8091:8091"
从容器中将配置文件拷贝到一个临时目录
docker cp seata-server:/seata-server/resources /tmp
后续使用拷贝出来的
application.yml
配置文件即可。
修改配置文件
这里使用Nacos为注册中心以及配置中心。修改seata.config
以及seata.registry
。
server:
port: 7091
spring:
application:
name: seata-server
logging:
config: classpath:logback-spring.xml
file:
path: ${user.home}/logs/seata
extend:
logstash-appender:
destination: 127.0.0.1:4560
kafka-appender:
bootstrap-servers: 127.0.0.1:9092
topic: logback_to_logstash
console:
user:
username: seata
password: seata
seata:
config:
# support: nacos, consul, apollo, zk, etcd3
type: nacos
nacos:
server-addr: nacos-server:8848
namespace: seata-server
group: SEATA_GROUP
usernam: nacos
password: nacos
data-id: seataServer.properties
registry:
# support: nacos, eureka, redis, zk, consul, etcd3, sofa
type: nacos
nacos:
application: seata-server
server-addr: nacos-server:8848
group: SEATA_GROUP
namespace: seata-server
# tc集群名称
cluster: default
username: nacos
password: nacos
# store:
# # support: file 、 db 、 redis
# mode: file
# server:
# service-port: 8091 #If not configured, the default is '${server.port} + 1000'
security:
secretKey: SeataSecretKey0c382ef121d778043159209298fd40bf3850a017
tokenValidityInMilliseconds: 1800000
ignore:
urls: /,/**/*.css,/**/*.js,/**/*.html,/**/*.map,/**/*.svg,/**/*.png,/**/*.ico,/console-fe/public/**,/api/v1/auth/login
在nacos配置中心上创建配置文件
此处dataId为seataServer.properties
修改nacos上的配置
这里我使用的是mysql8,官方提供了建表脚本文件。
store.mode=db
#-----db-----
store.db.datasource=druid
store.db.dbType=mysql
# 需要根据mysql的版本调整driverClassName
# mysql8及以上版本对应的driver:com.mysql.cj.jdbc.Driver
# mysql8以下版本的driver:com.mysql.jdbc.Driver
store.db.driverClassName=com.mysql.cj.jdbc.Driver
store.db.url=jdbc:mysql://mysql:3306/seata-server?useUnicode=true&characterEncoding=utf8&connectTimeout=1000&socketTimeout=3000&autoReconnect=true&useSSL=false
store.db.user= 用户名
store.db.password=密码
# 数据库初始连接数
store.db.minConn=1
# 数据库最大连接数
store.db.maxConn=20
# 获取连接时最大等待时间 默认5000,单位毫秒
store.db.maxWait=5000
# 全局事务表名 默认global_table
store.db.globalTable=global_table
# 分支事务表名 默认branch_table
store.db.branchTable=branch_table
# 全局锁表名 默认lock_table
store.db.lockTable=lock_table
# 查询全局事务一次的最大条数 默认100
store.db.queryLimit=100
# undo保留天数 默认7天,log_status=1(附录3)和未正常清理的undo
server.undo.logSaveDays=7
# undo清理线程间隔时间 默认86400000,单位毫秒
server.undo.logDeletePeriod=86400000
# 二阶段提交重试超时时长 单位ms,s,m,h,d,对应毫秒,秒,分,小时,天,默认毫秒。默认值-1表示无限重试
# 公式: timeout>=now-globalTransactionBeginTime,true表示超时则不再重试
# 注: 达到超时时间后将不会做任何重试,有数据不一致风险,除非业务自行可校准数据,否者慎用
server.maxCommitRetryTimeout=-1
# 二阶段回滚重试超时时长
server.maxRollbackRetryTimeout=-1
# 二阶段提交未完成状态全局事务重试提交线程间隔时间 默认1000,单位毫秒
server.recovery.committingRetryPeriod=1000
# 二阶段异步提交状态重试提交线程间隔时间 默认1000,单位毫秒
server.recovery.asynCommittingRetryPeriod=1000
# 二阶段回滚状态重试回滚线程间隔时间 默认1000,单位毫秒
server.recovery.rollbackingRetryPeriod=1000
# 超时状态检测重试线程间隔时间 默认1000,单位毫秒,检测出超时将全局事务置入回滚会话管理器
server.recovery.timeoutRetryPeriod=1000
docker-compose.yml文件
version: "3.8"
services:
seata1:
image: seataio/seata-server:latest
container_name: seata1
ports:
- "7091:7091"
- "8091:8091"
environment:
- STORE_MODE=db
# 以SEATA_IP作为host注册seata server,可选, 指定seata-server启动的IP, 该IP用于向注册中心注册时使用, 如eureka等
- SEATA_IP=${seata_ip}
# 可选, 指定seata-server启动的端口, 默认为 8091
- SEATA_PORT=8091
volumes:
- "/usr/share/zoneinfo/Asia/Shanghai:/etc/localtime" #设置系统时区
- "/usr/share/zoneinfo/Asia/Shanghai:/etc/timezone" #设置时区
# 假设我们通过docker cp命令把资源文件拷贝到相对路径`./seata-server/resources`中
- "./seata-server/resources:/seata-server/resources"
seata2:
image: seataio/seata-server:latest
container_name: seata2
ports:
- "7092:7091"
- "8092:8092"
environment:
- STORE_MODE=db
# 以SEATA_IP作为host注册seata server,可选, 指定seata-server启动的IP, 该IP用于向注册中心注册时使用, 如eureka等
- SEATA_IP=${seata_ip}
# 可选, 指定seata-server启动的端口, 默认为 8091
- SEATA_PORT=8092
volumes:
- "/usr/share/zoneinfo/Asia/Shanghai:/etc/localtime" #设置系统时区
- "/usr/share/zoneinfo/Asia/Shanghai:/etc/timezone" #设置时区
# 假设我们通过docker cp命令把资源文件拷贝到相对路径`./seata-server/resources`中
- "./seata-server/resources:/seata-server/resources"
踩坑:
services.seata.environment
下的SEATA_IP不填写的话,会默认将容器IP注册到Nacos,可能导致网络不通。
services.seata.environment
下SEATA_PORT,这里特别注意下,当时没有仔细看官网的环境变量描述,直接拷贝了docker-compose.yml,以为这个配置的仅仅是注册到nacos的端口,实际上也是seata服务启动的端口,官网上集群配置第二个是错的。直接启动客户端,总是会连不上第二个seata,客户端日志会打印出这个日志:
can not connect to xxx.xxx.xxx.xxx:8092 cause:can not register RM,err:register RMROLE error, errMsg:null
官网的配置:
这里官网上配置启动端口为8092
,注册到nacos的端口也是8092
,但是实际上seata在容器内启动的端口是8091
,这里导致我就是连不上第二台服务。
配置客户端
Maven依赖
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-seata</artifactId>
<version>${seata.version}</version>
</dependency>
客户端application.yml配置
seata:
enabled: true
tx-service-group: test_tx_group
service:
vgroup-mapping:
test_tx_group: default
registry:
type: nacos #注册中心
nacos:
server-addr: ${spring.cloud.nacos.discovery.server-addr}
group: SEATA_GROUP
namespace: seata-server
cluster: default
config:
type: nacos #配置中心
nacos:
server-addr: ${spring.cloud.nacos.discovery.server-addr}
namespace: seata-server
group: SEATA_GROUP
data-id: seataServer.properties
Seata原理
- TM开启分布式事务(TM向TC注册全局事务记录)。
- 按业务场景,编排数据库,服务等事务内资源(RM向TC汇报资源准备状态)。
- TM结束分布式事务,事务一阶段结束(TM通知TC提交/回滚分布式事务)。
- TC汇总事务信息,决定分布式事务是提交还是回滚。
- TC通知所有RM提交/回滚资源。
AT模式是怎么做到对业务无侵入的
事务一阶段
在第一阶段,Seata会拦截业务SQL:
- 解析SQL语义,找到业务SQL要更新的业务数据,在业务被更新之前,将其保存为“before image”。
- 执行业务SQL更新业务数据,在业务数据更新之后。
- 其保存为“after image”,最后生成行锁。
以上操作全部在一个数据库事务内完成,这样保证了一阶段操作的原子性。
事务二阶段
二阶段提交
在二阶段如果没有异常,顺利提交的话。以为在一阶段,业务SQL已经提交到了数据库,所以Seata框架只需要将一阶段保存的快照数据和行锁删除掉,完成数据清理即可。
二阶段回滚
二阶段如果是回滚的话,seata就需要回滚一阶段已经执行的业务SQL,还原业务数据。其回滚方式便是使用“before image”来还原业务数据。但在还原之前要先校验脏写,对比“数据库当前的业务数据”和after image。如果两份数据完全一致就说明没有脏写,可以还原业务数据,如果不一致就说明有脏写,出现脏写就需要转人工处理。