Appearance
Seata
Seata(Simple Extensible Autonomous Transaction Architecture,简单可扩展自治事务框架)是一款开源的分布式事务解决方案,致力于在微服务架构下提供高性能和简单易用的分布式事务服务。
如下图所示,Seata 中有三大模块,分别是 TM
、RM
和 TC
。 其中 TM
和 RM
是作为 Seata 的客户端与业务系统集成在一起,TC
作为 Seata 的服务端独立部署。
(图片来源:Seata 官网)
TC (Transaction Coordinator) - 事务协调者
维护全局和分支事务的状态,驱动全局事务提交或回滚。
TM (Transaction Manager) - 事务管理器
定义全局事务的范围:开始全局事务、提交或回滚全局事务。
RM (Resource Manager) - 资源管理器
管理分支事务处理的资源,与TC交谈以注册分支事务和报告分支事务的状态,并驱动分支事务提交或回滚。
在 Seata 中,分布式事务的执行流程:
- TM 开启分布式事务(TM 向 TC 注册全局事务记录);
- 按业务场景,编排数据库、服务等事务内资源(RM 向 TC 汇报资源准备状态 );
- TM 结束分布式事务,事务一阶段结束(TM 通知 TC 提交/回滚分布式事务);
- TC 汇总事务信息,决定分布式事务是提交还是回滚;
- TC 通知所有 RM 提交/回滚 资源,事务二阶段结束。
分布式事务 Seata 解决方案
Seata 会有 4 种分布式事务解决方案,分别是 AT 模式、TCC 模式、Saga 模式和 XA 模式。
AT 模式
AT 模式是一种无侵入的分布式事务解决方案。在 AT 模式下,用户只需关注自己的“业务 SQL”,用户的 “业务 SQL” 作为一阶段,Seata 框架会自动生成事务的二阶段提交和回滚操作。
(图片来源:Seata 官网)
AT 模式如何做到对业务的无侵入
一阶段:
在一阶段,Seata 会拦截“业务 SQL”,首先解析 SQL 语义,找到“业务 SQL”要更新的业务数据,在业务数据被更新前,将其保存成“before image”,然后执行“业务 SQL”更新业务数据,在业务数据更新之后,再将其保存成“after image”,最后生成行锁。以上操作全部在一个数据库事务内完成,这样保证了一阶段操作的原子性。
(图片来源:Seata 官网)
二阶段提交:
二阶段如果是提交的话,因为“业务 SQL”在一阶段已经提交至数据库, 所以 Seata 框架只需将一阶段保存的快照数据和行锁删掉,完成数据清理即可。
(图片来源:Seata 官网)
二阶段回滚:
二阶段如果是回滚的话,Seata 就需要回滚一阶段已经执行的“业务 SQL”,还原业务数据。回滚方式便是用“before image”还原业务数据;但在还原前要首先要校验脏写,对比“数据库当前业务数据”和 “after image”,如果两份数据完全一致就说明没有脏写,可以还原业务数据,如果不一致就说明有脏写,出现脏写就需要转人工处理。
(图片来源:Seata 官网)
AT 模式的一阶段、二阶段提交和回滚均由 Seata 框架自动生成,用户只需编写“业务 SQL”,便能轻松接入分布式事务,AT 模式是一种对业务无任何侵入的分布式事务解决方案。
Spring 集成分布式事务框架 Seata(AT 模式)
单机
Seata-Server
Server 端存储模式(store.mode
)现有 file
、db
、redis
三种(后续将引入 raft
、mongodb
)。file
模式无需改动,直接启动即可。
注:file
模式为单机模式,全局事务会话信息内存中读写并持久化本地文件 root.data,性能较高。
在 Releases 页面下载相应版本并解压。
启动 seata-server。
- 在 Linux/Mac 下 -
./bin/seata-server.sh
- 在 Windows 下 -
bin\seata-server.bat
支持的启动参数:
参数 全写 作用 备注 -h --host 指定在注册中心注册的 IP 不指定时获取当前的 IP,外部访问部署在云环境和容器中的 server 建议指定 -p --post 指定 server 启动的端口 默认为 8091
-m --storeMode 事务日志存储方式 支持 file
、db
、redis
,默认为file
。注:redis
需 seata-server 1.3 版本及以上-n --serverNode 用于指定 seata-server 节点 ID 如 1,2,3...,默认为 1 -e --seataEnv 指定 seata-server 运行环境 如 dev
、test
等,服务启动时会使用 registry-dev.conf 这样的配置如:
sh$ ./bin/seata-server.sh -p 8091 -h 127.0.0.1 -m file
- 在 Linux/Mac 下 -
Seata-Client
添加 Seata 依赖
<dependencyManagement>
中加入 Seata 依赖管理xml<dependencyManagement> <dependencies> <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-alibaba-dependencies</artifactId> <version>2021.1</version> <type>pom</type> <scope>import</scope> </dependency> </dependencies> </dependencyManagement>
<dependencies>
中加入 Seata 依赖xml<dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-seata</artifactId> </dependency>
undo_log 建表
注:每个业务库中必须包含 undo_log 表,若与分库分表组件联用,分库不分表。
PostgreSQL 数据库 undo_log 表结构
sql-- for AT mode you must to init this sql for you business database. the seata server not need it. CREATE TABLE IF NOT EXISTS public.undo_log ( id SERIAL NOT NULL, branch_id BIGINT NOT NULL, xid VARCHAR(128) NOT NULL, context VARCHAR(128) NOT NULL, rollback_info BYTEA NOT NULL, log_status INT NOT NULL, log_created TIMESTAMP(0) NOT NULL, log_modified TIMESTAMP(0) NOT NULL, CONSTRAINT pk_undo_log PRIMARY KEY (id), CONSTRAINT ux_undo_log UNIQUE (xid, branch_id) ); CREATE SEQUENCE IF NOT EXISTS undo_log_id_seq INCREMENT BY 1 MINVALUE 1 ;
项目配置
application.properties
propertiesspring.cloud.alibaba.seata.tx-service-group=my_test_tx_group seata.enabled=true seata.registry.type=file seata.config.type=file seata.service.grouplist.default=127.0.0.1:8091
数据源配置
Druid
javaimport javax.sql.DataSource; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import com.alibaba.druid.pool.DruidDataSource; @Configuration public class DataSourceConfiguration { @ConfigurationProperties(prefix = "spring.datasource") @Bean public DataSource dataSource() { DruidDataSource druidDataSource = new DruidDataSource(); return druidDataSource; } }
开启分布式事务
只需要使用一个
@GlobalTransactional
注解在业务方法上javaimport io.seata.spring.annotation.GlobalTransactional; @GlobalTransactional public void purchase(String userId, String commodityCode, int count) { storageFeignClient.deduct(commodityCode, count); orderFeignClient.create(userId, commodityCode, count); storageFeignClient.validStorageData(); }
集群
Seata 的高可用依赖于注册中心、配置中心和数据库来实现。本次部署使用 Nacos 作为注册中心和配置中心,使用 PostgreSQL 作为存储事务数据的数据库。
Nacos
在 Releases 页面下载相应版本并解压。
直接启动(
standalone
代表着单机模式运行,非集群模式)。- 在 Linux/Mac 下 -
./bin/startup.sh -m standalone
- 在 Windows 下 -
bin\startup.cmd -m standalone
- 在 Linux/Mac 下 -
启动端口默认
8848
,访问路径默认 /nacos。(图片来源:自己截得)
进入 Nacos 控制台,用户名和密码默认都是
nacos
。(图片来源:自己截得)
(图片来源:自己截得)
Seata-Server
Server 端存储模式(store.mode
)现有 file
、db
、redis
三种(后续将引入 raft
、mongodb
)。db
模式为高可用模式,全局事务会话信息通过 db
共享,相应性能差些。
创建
db
模式需要的表。全局事务会话信息由 3 块内容构成,全局事务-->分支事务-->全局锁,对应表
global_table
、branch_table
、lock_table
。sql-- -------------------------------- The script used when storeMode is 'db' -------------------------------- -- the table to store GlobalSession data CREATE TABLE IF NOT EXISTS public.global_table ( xid VARCHAR(128) NOT NULL, transaction_id BIGINT, status SMALLINT NOT NULL, application_id VARCHAR(32), transaction_service_group VARCHAR(32), transaction_name VARCHAR(128), timeout INT, begin_time BIGINT, application_data VARCHAR(2000), gmt_create TIMESTAMP(0), gmt_modified TIMESTAMP(0), CONSTRAINT pk_global_table PRIMARY KEY (xid) ); CREATE INDEX idx_gmt_modified_status ON public.global_table (gmt_modified, status); CREATE INDEX idx_transaction_id ON public.global_table (transaction_id); -- the table to store BranchSession data CREATE TABLE IF NOT EXISTS public.branch_table ( branch_id BIGINT NOT NULL, xid VARCHAR(128) NOT NULL, transaction_id BIGINT, resource_group_id VARCHAR(32), resource_id VARCHAR(256), branch_type VARCHAR(8), status SMALLINT, client_id VARCHAR(64), application_data VARCHAR(2000), gmt_create TIMESTAMP(6), gmt_modified TIMESTAMP(6), CONSTRAINT pk_branch_table PRIMARY KEY (branch_id) ); CREATE INDEX idx_xid ON public.branch_table (xid); -- the table to store lock data CREATE TABLE IF NOT EXISTS public.lock_table ( row_key VARCHAR(128) NOT NULL, xid VARCHAR(128), transaction_id BIGINT, branch_id BIGINT NOT NULL, resource_id VARCHAR(256), table_name VARCHAR(32), pk VARCHAR(36), gmt_create TIMESTAMP(0), gmt_modified TIMESTAMP(0), CONSTRAINT pk_lock_table PRIMARY KEY (row_key) ); CREATE INDEX idx_branch_id ON public.lock_table (branch_id); CREATE TABLE distributed_lock ( lock_key VARCHAR(20) NOT NULL, lock_value VARCHAR(20) NOT NULL, expire BIGINT NOT NULL, CONSTRAINT pk_distributed_lock_table PRIMARY KEY (lock_key) ); INSERT INTO distributed_lock (lock_key, lock_value, expire) VALUES ('AsyncCommitting', ' ', 0); INSERT INTO distributed_lock (lock_key, lock_value, expire) VALUES ('RetryCommitting', ' ', 0); INSERT INTO distributed_lock (lock_key, lock_value, expire) VALUES ('RetryRollbacking', ' ', 0); INSERT INTO distributed_lock (lock_key, lock_value, expire) VALUES ('TxTimeoutCheck', ' ', 0);
同时,也需要创建
undo_log
表。sql-- for AT mode you must to init this sql for you business database. the seata server not need it. CREATE TABLE IF NOT EXISTS public.undo_log ( id SERIAL NOT NULL, branch_id BIGINT NOT NULL, xid VARCHAR(128) NOT NULL, context VARCHAR(128) NOT NULL, rollback_info BYTEA NOT NULL, log_status INT NOT NULL, log_created TIMESTAMP(0) NOT NULL, log_modified TIMESTAMP(0) NOT NULL, CONSTRAINT pk_undo_log PRIMARY KEY (id), CONSTRAINT ux_undo_log UNIQUE (xid, branch_id) ); CREATE SEQUENCE IF NOT EXISTS undo_log_id_seq INCREMENT BY 1 MINVALUE 1 ;
配置导入 Nacos。
为了方便管理 Seata 配置,在 Nacos 控制台建立一个 seata 命名空间。
(图片来源:自己截得)
记下命名空间 ID。
(图片来源:自己截得)
调整 config.txt 内容中。
propertiesservice.vgroupMapping.my_test_tx_group=default store.mode=db store.db.datasource=druid store.db.dbType=postgresql store.db.driverClassName=org.postgresql.Driver store.db.url=jdbc:postgresql://localhost:5432/postgres?currentSchema=seata store.db.user=postgres store.db.password=postgres
(图片来源:自己截得)
执行 nacos-config.sh 脚本。
sh$ ./nacos-config.sh -h localhost -p 8848 -g SEATA_GROUP -t f66d1e4b-003a-4247-8612-bcb3beb37ef3
参数描述:https://github.com/seata/seata/tree/develop/script/config-center
提示
执行 nacos-config.sh 脚本时需要 config.txt 在脚本文件的父目录下。
(图片来源:自己截得)
config.txt 里的配置已同步到 Nacos 中。
(图片来源:自己截得)
在 Releases 页面下载相应版本并解压。
修改 seata-server 中 registry.conf 的注册中心配置
configregistry { type = "nacos" nacos { application = "seata-server" serverAddr = "127.0.0.1:8848" group = "SEATA_GROUP" namespace = "f66d1e4b-003a-4247-8612-bcb3beb37ef3" cluster = "default" username = "" password = "" } } config { type = "nacos" nacos { serverAddr = "127.0.0.1:8848" namespace = "f66d1e4b-003a-4247-8612-bcb3beb37ef3" group = "SEATA_GROUP" username = "" password = "" } }
启动 seata-server。
- 在 Linux/Mac 下 -
./bin/seata-server.sh
- 在 Windows 下 -
bin\seata-server.bat
支持的启动参数:
参数 全写 作用 备注 -h --host 指定在注册中心注册的 IP 不指定时获取当前的 IP,外部访问部署在云环境和容器中的 server 建议指定 -p --post 指定 server 启动的端口 默认为 8091
-m --storeMode 事务日志存储方式 支持 file
、db
、redis
,默认为file
。注:redis
需 seata-server 1.3 版本及以上-n --serverNode 用于指定 seata-server 节点 ID 如 1,2,3...,默认为 1 -e --seataEnv 指定 seata-server 运行环境 如 dev
、test
等,服务启动时会使用 registry-dev.conf 这样的配置如:
sh$ ./bin/seata-server.sh -p 8091 -h 127.0.0.1 -m db -n 1
- 在 Linux/Mac 下 -
查看是否注册到 Nacos。
(图片来源:自己截得)
启动多个 seata-server 实例。
sh$ ./bin/seata-server.sh -p 8091 -h 127.0.0.1 -m db -n 2 $ ./bin/seata-server.sh -p 8091 -h 127.0.0.1 -m db -n 3 ...
Seata-Client
添加 Seata 依赖
<dependencyManagement>
中加入 Seata 依赖管理xml<dependencyManagement> <dependencies> <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-alibaba-dependencies</artifactId> <version>2021.1</version> <type>pom</type> <scope>import</scope> </dependency> </dependencies> </dependencyManagement>
<dependencies>
中加入 Seata 依赖xml<dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-seata</artifactId> </dependency> <dependency> <groupId>com.alibaba.nacos</groupId> <artifactId>nacos-client</artifactId> </dependency>
undo_log 建表
注:每个业务库中必须包含 undo_log 表,若与分库分表组件联用,分库不分表。
PostgreSQL 数据库 undo_log 表结构
sql-- for AT mode you must to init this sql for you business database. the seata server not need it. CREATE TABLE IF NOT EXISTS public.undo_log ( id SERIAL NOT NULL, branch_id BIGINT NOT NULL, xid VARCHAR(128) NOT NULL, context VARCHAR(128) NOT NULL, rollback_info BYTEA NOT NULL, log_status INT NOT NULL, log_created TIMESTAMP(0) NOT NULL, log_modified TIMESTAMP(0) NOT NULL, CONSTRAINT pk_undo_log PRIMARY KEY (id), CONSTRAINT ux_undo_log UNIQUE (xid, branch_id) ); CREATE SEQUENCE IF NOT EXISTS undo_log_id_seq INCREMENT BY 1 MINVALUE 1 ;
项目配置
application.properties
propertiesseata.registry.type=nacos seata.registry.nacos.application=seata-server seata.registry.nacos.server-addr=127.0.0.1:8848 seata.registry.nacos.group=SEATA_GROUP seata.registry.nacos.namespace=f66d1e4b-003a-4247-8612-bcb3beb37ef3 seata.registry.nacos.username= seata.registry.nacos.password= seata.config.type=nacos seata.config.nacos.namespace=f66d1e4b-003a-4247-8612-bcb3beb37ef3 seata.config.nacos.server-addr=127.0.0.1:8848 seata.config.nacos.group=SEATA_GROUP seata.config.nacos.username= seata.config.nacos.password=
数据源配置
Druid
javaimport javax.sql.DataSource; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import com.alibaba.druid.pool.DruidDataSource; @Configuration public class DataSourceConfiguration { @ConfigurationProperties(prefix = "spring.datasource") @Bean public DataSource dataSource() { DruidDataSource druidDataSource = new DruidDataSource(); return druidDataSource; } }
开启分布式事务
只需要使用一个
@GlobalTransactional
注解在业务方法上javaimport io.seata.spring.annotation.GlobalTransactional; @GlobalTransactional public void purchase(String userId, String commodityCode, int count) { storageFeignClient.deduct(commodityCode, count); orderFeignClient.create(userId, commodityCode, count); storageFeignClient.validStorageData(); }