官网:https://seata.io/zh-cn/docs/overview/what-is-seata.html
源码:https://github.com/seata/seata
一、Seata是什么?
seata 是一款开源的分布式事务解决方案,致力于高性能和简单易用的分布式事务服务。Seatea 将为用户提供了AT、TCC、SAGA和XA事务模式,为用户打造一站式的分布式解决方案。AT模式是阿里首推的模式,阿里云有商用版本的GTS(Global Transaction Service 全局事务服务)
1. 四种分布式事务模式(了解更多):
- AT 模式是无侵入的分布式事务解决方案,适用于不希望对业务进行改造的场景,几乎0学习成本。但是要注意共享数据 会存在脏写,如果是数据只由事务调用者单独控制,那么就没有问题。
- TCC 模式是高性能分布式事务解决方案,适用于核心系统等对性能有很高要求的场景。要注意并发、幂等,要解决空回滚、防悬挂控制。
- Saga 模式是长事务解决方案,适用于业务流程长且需要保证事务最终一致性的业务系统,Saga 模式一阶段就会提交本地事务,无锁,长流程情况下可以保证性能,多用于渠道层、集成层业务系统。事务参与者可能是其它公司的服务或者是遗留系统的服务,无法进行改造和提供 TCC 要求的接口,也可以使用 Saga 模式。要注意幂等,要解决空补偿、防悬挂控制。
- XA模式是分布式强一致性的解决方案,但性能低而使用较少。
2. 事务简介
事务(Transaction)是访问并可能更型数据库中各个数据项的一个执行单元(unit)。在关系型数据库中,一个事务由一组SQL语句组成。事务应该具有4个属性:
- 原子性(atomicity):一个事务是不可再分割的整体,要么全部被执行,要么全部执行。
- 一致性(consistency):事务执行完毕时一个事务必须使数据库从一个一致性状态变换到另一个一致性状态,也就是说一个事务执行之前和执行之后都必须处于一致性状态。
- 隔离性(isolation):一个事务不受其他事务的干扰,并且多个事务彼此隔离。隔离性又分四个级别:读未提交(read uncommitted)、读已提交(read committed,解决脏读)、可重复读(repeatable read,解决虚读)、串行化(serializable,解决幻读)。
- 持久性(durability):持久性是指事务一旦提交,它对数据库的改变就应该是永久性的。接下来的其他操作或故障不应该对其有任何影响。
3. Seata架构的三大角色
3.1 在Seata架构中,有一下三个角色:
- TC (Transaction Coordinator 事务协调者):维护全局和分支事务的状态,驱动全局事务提交或回滚。
- TM (Transaction Manager 事务管理器):定义全局事务的范围,开始全局事务、提交或回滚全局事务。
- RM (Resource Manger 资源管理器):管理分支事务处理的资源,与TC交谈以注册分支事务和报告分支事务的状态,并驱动分支事务提交或回滚。
其中,TC为单独部署的Server服务端,TM 和 RM 为嵌入到应用中的 Client 客户端。
3.2 在Seatea中,一个分布式事务的生命周期如下:
(1) TM 请求 TC 开启一个全局事务,TC 会生成一个 XID 作为该全局事务的编号。XID 会在微服务的调用链路中传播,保证将多个微服务的子事务关联在一起。(全局事务信息存储在 global_table )
(2) RM 请求 TC 将本地事务注册为全局事务的分支事务,通过全局事务的 XID 进行关联。
(3) TM 请求 TC 告诉 XID 对应的全局事务是进行提交还是回滚。(事务参与者存储在branch_table)
(4) TC 驱动 RM 们将 XID 对应的自己的本地事务进行提交还是回滚。
二、Seata快速开始
1.Seata Server 环境搭建
官方文档:https://seata.io/zh-cn/docs/ops/deploy-guide-beginner.html
① Server 端存储模式(store.mode)支持三种:
- file:单机模式,全局事务会话信息内存中读写并持久化本地文件root.data,性能较高(默认)。
- db:高可用模式,全局事务会话信息通过db共享,相应性能差些 (mysql数据库需要使用5.7及以上,注意:mysql 8有坑,后面有提供解决办法)。
- redis:Seata-Server 1.3及以上版本支持,性能较高,存在事务信息丢失风险,请提前配置合适当前场景的redis持久化配置。
② 资源目录(需下载,后面有用):https://github.com/seata/seata/tree/1.4.2/script
- client:存放client端SQL脚本,参数配置
- config-center:各个配置中心参数导入脚本,config.txt(包含server和client,原名nacos-config.txt)为通用参数文件
- server:server端数据库脚本及各个容器配置
③ db存储模式+Nacos(注册&配置中心)部署
-
步骤一:下载启动包
-
步骤二:建表
对应sql文件脚本地址:https://github.com/seata/seata/tree/1.4.2/script/server/db
进成后如下所示:
global_table:全局事务信息
branch_table :事务参与者信息(分支事务)
lock_table:全局锁 -
步骤三:修改存储模式为db
启动包: seata–>conf–>file.conf,修改store.mode=“db” ;并且修改db数据库相关配置(如url、user、password之类的)
源码: 根目录–>seata-server–>resources–>file.conf,修改store.mode=“db”
-
步骤四:修改注册中心
启动包: seata–>conf–>registry.conf,修改type = “nacos”;并填写nacos相关配置
源码: 根目录–>seata-server–>resources–>registry.conf,修改type = “nacos”
-
步骤五:修改配置中心
启动包: seata–>conf–>registry.conf,修改config.type = “nacos”;并填写nacos相关配置 -
步骤六:将配置注册到nacos \script\config-center\nacos(此处需要使用到上面下载的资源)
完整配置信息文件地址:https://github.com/seata/seata/blob/1.4.2/script/config-center/config.txt
注意坑:使用mysql 8 数据库请注意: -
先检查 lib目录下面是否有mysql 8依赖(如果seata是1.4.2,mysql依赖在
lib/jdbc
下),如果没有需要添加“mysql-connector-java-8.0.19.jar” -
注意驱动类:
store.db.driverClassName = com.mysql.cj.jdbc.Driver
(不是com.mysql.jdbc.Driver) -
注意mysql url 需要带上时区信息:
store.db.url=jdbc:mysql://127.0.0.1:3306/seata?characterEncoding=utf8&useSSL=false&serverTimezone=UTC&rewriteBatchedStatements=true
执行脚本 nacos-config.sh
-h: 注册到注册中心的ip
-p: Server rpc 监听端口
-g: 配置分组,默认值为 ‘SEATA_GROUP’
-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
-t: 租户信息,对应Nacos的命名空间ID字段,默认值为空
查看数据
- 步骤七:启动项目
源码启动: 执行Server.java的main方法
命令启动: seata-server.sh -h 127.0.0.1 -p 8091 -m db -n 1 -e test
-h: 注册到注册中心的ip
-p: Server rpc 监听端口
-m: 全局事务会话信息存储模式,file、db、redis,优先读取启动参数 (Seata-Server 1.3及以上版本支持redis),mo
-n: Server node,多个Server时,需区分各自节点,用于生成不同区间的transactionId,以免冲突
-e: 多环境配置参考 http://seata.io/en-us/docs/ops/multi-configuration-isolation.html
④ seata启动错误:
错误一:数据库问题
ERROR --- [ionPool-Create-1033638837] com.alibaba.druid.pool.DruidDataSource : create connection SQLException, url: jdbc:mysql://127.0.0.1:3306/seata?useUnicode=true&rewriteBatchedStatements=true, errorCode 0, state 08001
==>
com.mysql.jdbc.exceptions.jdbc4.MySQLNonTransientConnectionException: Could not create connection to database server.
at sun.reflect.NativeConstructorAccessorImpl.newInstance0(Native Method) ~[na:1.8.0_282]
at sun.reflect.NativeConstructorAccessorImpl.newInstance(NativeConstructorAccessorImpl.java:62) ~[na:1.8.0_282]
at sun.reflect.DelegatingConstructorAccessorImpl.newInstance(DelegatingConstructorAccessorImpl.java:45) ~[na:1.8.0_282]
at java.lang.reflect.Constructor.newInstance(Constructor.java:423) ~[na:1.8.0_282]
at com.mysql.jdbc.Util.handleNewInstance(Util.java:389) ~[na:na]
at com.mysql.jdbc.Util.getInstance(Util.java:372) ~[na:na]
at com.mysql.jdbc.SQLError.createSQLException(SQLError.java:958) ~[na:na]
at com.mysql.jdbc.SQLError.createSQLException(SQLError.java:937) ~[na:na]
at com.mysql.jdbc.SQLError.createSQLException(SQLError.java:926) ~[na:na]
at com.mysql.jdbc.SQLError.createSQLException(SQLError.java:872) ~[na:na]
at com.mysql.jdbc.ConnectionImpl.connectOneTryOnly(ConnectionImpl.java:2316) ~[na:na]
at com.mysql.jdbc.ConnectionImpl.createNewIO(ConnectionImpl.java:2069) ~[na:na]
at com.mysql.jdbc.ConnectionImpl.<init>(ConnectionImpl.java:794) ~[na:na]
at com.mysql.jdbc.JDBC4Connection.<init>(JDBC4Connection.java:44) ~[na:na]
at sun.reflect.NativeConstructorAccessorImpl.newInstance0(Native Method) ~[na:1.8.0_282]
at sun.reflect.NativeConstructorAccessorImpl.newInstance(NativeConstructorAccessorImpl.java:62) ~[na:1.8.0_282]
at sun.reflect.DelegatingConstructorAccessorImpl.newInstance(DelegatingConstructorAccessorImpl.java:45) ~[na:1.8.0_282]
at java.lang.reflect.Constructor.newInstance(Constructor.java:423) ~[na:1.8.0_282]
at com.mysql.jdbc.Util.handleNewInstance(Util.java:389) ~[na:na]
at com.mysql.jdbc.ConnectionImpl.getInstance(ConnectionImpl.java:399) ~[na:na]
at com.mysql.jdbc.NonRegisteringDriver.connect(NonRegisteringDriver.java:325) ~[na:na]
at com.alibaba.druid.pool.DruidAbstractDataSource.createPhysicalConnection(DruidAbstractDataSource.java:1644) ~[druid-1.1.23.jar:1.1.23]
at com.alibaba.druid.pool.DruidAbstractDataSource.createPhysicalConnection(DruidAbstractDataSource.java:1710) ~[druid-1.1.23.jar:1.1.23]
at com.alibaba.druid.pool.DruidDataSource$CreateConnectionThread.run(DruidDataSource.java:2774) ~[druid-1.1.23.jar:1.1.23]
Caused by: java.lang.NullPointerException: null
at com.mysql.jdbc.ConnectionImpl.getServerCharset(ConnectionImpl.java:2989) ~[na:na]
at com.mysql.jdbc.MysqlIO.sendConnectionAttributes(MysqlIO.java:1873) ~[na:na]
at com.mysql.jdbc.MysqlIO.proceedHandshakeWithPluggableAuthentication(MysqlIO.java:1802) ~[na:na]
at com.mysql.jdbc.MysqlIO.doHandshake(MysqlIO.java:1206) ~[na:na]
at com.mysql.jdbc.ConnectionImpl.coreConnect(ConnectionImpl.java:2239) ~[na:na]
at com.mysql.jdbc.ConnectionImpl.connectOneTryOnly(ConnectionImpl.java:2270) ~[na:na]
... 13 common frames omitted
解决方案: 先排查mysql url等配置信息是否错误,然后查看mysql版本。本人是MySQL版本问题,用的是MySQL8.0,Seata store.db.url
配置没有加上时区信息,其次还有修改MySQL驱动类(1.4.2对应的配置是:store.db.driverClassName)
seata 其他版本我不太清楚,反正1.4.2是有提供MySQL的jar依赖,在lib/jdbc目录下
⑤启动成功:
2. Seata Client 环境搭建
添加两张业务表(一个订单表、一个库存表)
DROP TABLE IF EXISTS `storage_tbl`;
CREATE TABLE `storage_tbl` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`commodity_code` varchar(255) DEFAULT NULL,
`count` int(11) DEFAULT 0,
PRIMARY KEY (`id`),
UNIQUE KEY (`commodity_code`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
DROP TABLE IF EXISTS `order_tbl`;
CREATE TABLE `order_tbl` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`user_id` varchar(255) DEFAULT NULL,
`commodity_code` varchar(255) DEFAULT NULL,
`count` int(11) DEFAULT 0,
`money` int(11) DEFAULT 0,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
① 引入依赖
<!-- seata -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-seata</artifactId>
</dependency>
② 添加配置
# seata 配置
seata:
tx-service-group: my_test_tx_group # 对应nacos配置 service.vgroupMapping.my_test_tx_group
service:
vgroup-mapping:
my_test_tx_group: 'default' # 对应nacos配置 service.vgroupMapping.my_test_tx_group 的值 default
registry:
type: nacos
nacos: # 对应配置类:RegistryNacosProperties
server-addr: ${spring.cloud.nacos.discovery.server-addr}
application: seata-server # seata server 的服务名,默认:seata-server
username: ${spring.cloud.nacos.discovery.username}
password: ${spring.cloud.nacos.discovery.password}
group: SEATA_GROUP # seata server所在的组名,默认为:SEATA_GROUP
namespace: ${spring.cloud.nacos.discovery.namespace}
config:
type: nacos
server-addr: ${spring.cloud.nacos.discovery.server-addr}
username: ${spring.cloud.nacos.discovery.username}
password: ${spring.cloud.nacos.discovery.password}
group: SEATA_GROUP
namespace: ${spring.cloud.nacos.discovery.namespace}
注意事项:
- seata.tx-service-group 配置项一定要配置nacos配置中心service.vgroupMapping对应的my_test_tx_group。也就是说一定要保持一致。
- seata.service.vgroup-mapping.my_test_tx_group配置项一定要配nacos配置中心对应service.vgroupMapping.my_test_tx_group配置祥的值。
如果没有注意上方两点将会导致启动时报:no available service ‘default’ found, please make sure registry config correct。
③ 创建 UNDO_LOG 表
SEATA AT 模式需要 UNDO_LOG 表(使用到的数据库都需要此表)
-- 注意此处0.3.0+ 增加唯一索引 ux_undo_log
CREATE TABLE `undo_log` (
`id` bigint(20) NOT NULL AUTO_INCREMENT ,
`branch_id` bigint(20) NOT NULL COMMENT '分支事务ID',
`xid` varchar(100) NOT NULL COMMENT '全局事务ID',
`context` varchar(128) NOT NULL COMMENT '上下文',
`rollback_info` longblob NOT NULL COMMENT '回滚信息',
`log_status` int(11) NOT NULL COMMENT '状态,0正常,1全局已完成',
`log_created` datetime NOT NULL COMMENT '创建时间',
`log_modified` datetime NOT NULL COMMENT '修改时间',
`ext` varchar(100) DEFAULT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `ux_undo_log` (`xid`,`branch_id`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8 COMMENT = 'AT transaction mode undo table';
UNDO_LOG 表名是可以配置的,对应的文档地址
可在此处修改表名:
④ 具体使用
将@Transactional改为@GlobalTransactional
⑤ 项目使用全局异常拦截导致不生效问题
问题排查核心,判断AB服务的xid是否相同
使用手动回滚:GlobalTransactionContext.reload(RootContext.getXID()).rollback();
如果上述操作依然无法解决问题,可以补上“SeataHandlerInterceptor”操作: