Seata 是一款开源的分布式事务解决方案,致力于提供高性能和简单易用的分布式事务服务。Seata 将为用户提供了 AT、TCC、SAGA 和 XA 事务模式,为用户打造一站式的分布式解决方案。
TC (Transaction Coordinator) - 事务协调者
维护全局和分支事务的状态,驱动全局事务提交或回滚。
TM (Transaction Manager) - 事务管理器
定义全局事务的范围:开始全局事务、提交或回滚全局事务。
RM (Resource Manager) - 资源管理器
管理分支事务处理的资源,与TC交谈以注册分支事务和报告分支事务的状态,并驱动分支事务提交或回滚。
因为Seatas是Spring Cloud Alibaba下面的一款开源分布式事务解决框架,所以按照Spring Cloud Alibaba提供的毕业版本对应关系,减少不必要的问题。
spring-cloud-alibaba版本依赖关系
Spring Cloud Alibaba | 2.2.7.RELEASE |
---|---|
Spring Cloud | Hoxton.SR12 |
Spring Boot | 2.3.12.RELEASE |
Seata | 1.3.0 |
官网下载地址 | github下载地址
这里选择seata 1.3.0版本的zip进行下载
所有配置文件都在conf
目录下
mode
方式为db
file.conf配置seata-server数据存储方式,mode
字段指定模式,有file、db、redis等。默认是file,这边选择db数据库模式。
## transaction log store, only used in seata-server
store {
## store mode: file、db、redis
mode = "db"
## database store property
db {
## the implement of javax.sql.DataSource, such as DruidDataSource(druid)/BasicDataSource(dbcp)/HikariDataSource(hikari) etc.
datasource = "druid"
## mysql/oracle/postgresql/h2/oceanbase etc.
dbType = "mysql"
## mysql 8 要改驱动com.mysql.jdbc.Driver为com.mysql.cj.jdbc.Driver
driverClassName = "com.mysql.cj.jdbc.Driver"
## 下面数据库配置要改成自己的
url = "jdbc:mysql://127.0.0.1:3306/seata?serverTimezone=UTC&useUnicode=true&characterEncoding=utf-8"
user = "root"
password = "123456"
minConn = 5
maxConn = 30
globalTable = "global_table"
branchTable = "branch_table"
lockTable = "lock_table"
queryLimit = 100
maxWait = 5000
}
}
sql脚本地址:https://github.com/seata/seata/tree/1.3.0/script/server/db,这里选择mysql.sql
-- -------------------------------- The script used when storeMode is 'db' --------------------------------
-- the table to store GlobalSession data
CREATE TABLE IF NOT EXISTS `global_table`
(
`xid` VARCHAR(128) NOT NULL,
`transaction_id` BIGINT,
`status` TINYINT 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` DATETIME,
`gmt_modified` DATETIME,
PRIMARY KEY (`xid`),
KEY `idx_gmt_modified_status` (`gmt_modified`, `status`),
KEY `idx_transaction_id` (`transaction_id`)
) ENGINE = InnoDB
DEFAULT CHARSET = utf8;
-- the table to store BranchSession data
CREATE TABLE IF NOT EXISTS `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` TINYINT,
`client_id` VARCHAR(64),
`application_data` VARCHAR(2000),
`gmt_create` DATETIME(6),
`gmt_modified` DATETIME(6),
PRIMARY KEY (`branch_id`),
KEY `idx_xid` (`xid`)
) ENGINE = InnoDB
DEFAULT CHARSET = utf8;
-- the table to store lock data
CREATE TABLE IF NOT EXISTS `lock_table`
(
`row_key` VARCHAR(128) NOT NULL,
`xid` VARCHAR(96),
`transaction_id` BIGINT,
`branch_id` BIGINT NOT NULL,
`resource_id` VARCHAR(256),
`table_name` VARCHAR(32),
`pk` VARCHAR(36),
`gmt_create` DATETIME,
`gmt_modified` DATETIME,
PRIMARY KEY (`row_key`),
KEY `idx_branch_id` (`branch_id`)
) ENGINE = InnoDB
DEFAULT CHARSET = utf8;
registry.conf配置seata-server的注册中心和配置中心地址。默认type是file,这里选择nacos作为注册和配置中心
registry {
# file 、nacos 、eureka、redis、zk、consul、etcd3、sofa
type = "nacos"
nacos {
# 注册到nacos的服务名
application = "seata-server"
# nacos地址
serverAddr = "127.0.0.1:8848"
# 服务分组名称
group = "SEATA_GROUP"
# 服务所在的nacos命名空间
namespace = ""
cluster = "default"
username = "nacos"
password = "nacos"
}
}
config {
# file、nacos 、apollo、zk、consul、etcd3
type = "nacos"
nacos {
serverAddr = "127.0.0.1:8848"
namespace = ""
group = "SEATA_GROUP"
username = "nacos"
password = "nacos"
}
}
初始化配置中心,把https://github.com/seata/seata/tree/1.3.0/ 下的整个文件夹script文件夹拷贝到seata的目录下。
编辑script/config-center目录下的config.txt文件
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
# 注意这里是事务分组,默认分组名称是my_test_tx_group,
# 这里的default对应的是刚刚配置的nacos注册中心cluster = "default" 集群名称
service.vgroupMapping.my_test_tx_group=default
service.default.grouplist=127.0.0.1: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.sqlParserType=druid
client.rm.reportSuccessEnable=false
client.rm.sagaBranchRegisterEnable=false
client.tm.commitRetryCount=5
client.tm.rollbackRetryCount=5
client.tm.degradeCheck=false
client.tm.degradeCheckAllowTimes=10
client.tm.degradeCheckPeriod=2000
# 这里mode指定为db, 默认是file
store.mode=db
store.db.datasource=druid
store.db.dbType=mysql
# 数据库连接配置这里也改成自己的
store.db.driverClassName=com.mysql.cj.jdbc.Driver
store.db.url=jdbc:mysql://127.0.0.1:3306/seata?serverTimezone=UTC&useUnicode=true&characterEncoding=utf-8
store.db.user=root
store.db.password=123456
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.host=127.0.0.1
store.redis.port=6379
store.redis.maxConn=10
store.redis.minConn=1
store.redis.database=0
store.redis.password=null
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.log.exceptionRate=100
transport.serialization=seata
transport.compressor=none
metrics.enabled=false
metrics.registryType=compact
metrics.exporterList=prometheus
metrics.exporterPrometheusPort=9898
将config.txt的配置推送到nacos的配置中心,在script/config-center/nacos目录下有两个脚本文件,nacos-config.py和nacos-config.sh,这边选择sh文件执行,因为有安装git有git bash可以执行sh文件,命令如下:
sh nacos-config.sh -h 127.0.0.1 -p 8848 -g SEATA_GROUP -t 30fe3db1-8205-4ee8-86be-be79aa67a1b2 -u nacos -w nacos
bin目录下的seata-server.bat
查看nacos服务
-- 新建order数据库 添加order_tbl表
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;
-- 新建account数据库 添加account_tbl表
DROP TABLE IF EXISTS `account_tbl`;
CREATE TABLE `account_tbl` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`user_id` varchar(255) DEFAULT NULL,
`money` int(11) DEFAULT 0,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
两个库要添加undo_log
表,用于记录数据在本地事务前后的状态,seata默认使用AT模式,所以使用https://github.com/seata/seata/tree/1.3.0/script/client/at/db 下的建表语句mysql.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 `undo_log`
(
`branch_id` BIGINT(20) NOT NULL COMMENT 'branch transaction id',
`xid` VARCHAR(100) NOT NULL COMMENT 'global transaction id',
`context` VARCHAR(128) NOT NULL COMMENT 'undo_log context,such as serialization',
`rollback_info` LONGBLOB NOT NULL COMMENT 'rollback info',
`log_status` INT(11) NOT NULL COMMENT '0:normal status,1:defense status',
`log_created` DATETIME(6) NOT NULL COMMENT 'create datetime',
`log_modified` DATETIME(6) NOT NULL COMMENT 'modify datetime',
UNIQUE KEY `ux_undo_log` (`xid`, `branch_id`)
) ENGINE = InnoDB
AUTO_INCREMENT = 1
DEFAULT CHARSET = utf8 COMMENT ='AT transaction mode undo table';
<properties>
<java.version>1.8java.version>
<maven.compiler.source>8maven.compiler.source>
<maven.compiler.target>8maven.compiler.target>
<spring.boot.version>2.3.12.RELEASEspring.boot.version>
<spring.cloud.alibaba>2.2.7.RELEASEspring.cloud.alibaba>
<spring.cloud.version>Hoxton.SR12spring.cloud.version>
properties>
<dependencies>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-webartifactId>
dependency>
<dependency>
<groupId>com.alibaba.cloudgroupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discoveryartifactId>
dependency>
<dependency>
<groupId>com.alibaba.cloudgroupId>
<artifactId>spring-cloud-starter-alibaba-nacos-configartifactId>
dependency>
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-openfeignartifactId>
dependency>
<dependency>
<groupId>com.alibabagroupId>
<artifactId>druid-spring-boot-starterartifactId>
dependency>
<dependency>
<groupId>mysqlgroupId>
<artifactId>mysql-connector-javaartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-data-jpaartifactId>
dependency>
<dependency>
<groupId>org.projectlombokgroupId>
<artifactId>lombokartifactId>
dependency>
<dependency>
<groupId>com.alibaba.cloudgroupId>
<artifactId>spring-cloud-starter-alibaba-seataartifactId>
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>org.springframework.cloudgroupId>
<artifactId>spring-cloud-dependenciesartifactId>
<version>${spring.cloud.version}version>
<type>pomtype>
<scope>importscope>
dependency>
<dependency>
<groupId>com.alibaba.cloudgroupId>
<artifactId>spring-cloud-alibaba-dependenciesartifactId>
<version>${spring.cloud.alibaba}version>
<type>pomtype>
<scope>importscope>
dependency>
dependencies>
dependencyManagement>
这里引入的是spring-cloud-starter-alibaba-seata
,里面帮我们解决了feign调用XID参数传递的问题,不然子事务获取不到全局事务的XID。
server:
port: 8001
# 数据源配置
spring:
application:
name: seata-account
datasource:
url: jdbc:mysql://localhost:3306/seata-account?serverTimezone=UTC&useUnicode=true&characterEncoding=utf-8
driver-class-name: com.mysql.cj.jdbc.Driver
type: com.alibaba.druid.pool.DruidDataSource # 自定义数据源
username: root
password: 123456
# 服务注册中心
cloud:
nacos:
server-addr: localhost:8848
# seata配置,与seata-server的registry.conf一致
seata:
enabled: true
tx-service-group: my_test_tx_group
registry:
type: nacos
nacos:
application: seata-server
server-addr: 127.0.0.1:8848
group: SEATA_GROUP
namespace:
username: nacos
password: nacos
config:
type: nacos
nacos:
server-addr: 127.0.0.1:8848
group: SEATA_GROUP
namespace:
username: nacos
password: nacos
@EnableFeignClients
@EnableDiscoveryClient
@SpringBootApplication
public class SeataAccountApp {
public static void main(String[] args) {
SpringApplication.run(SeataAccountApp.class, args);
}
}
// entity
@Data
@Accessors(chain = true)
@Entity
@Table(name="account_tbl")
public class Account implements Serializable {
@Id
private int id;
private String userId;
private int money;
}
// DAO层
public interface AccountDao extends JpaRepository<Account, Integer> {
}
// Service接口
public interface AccountService {
/**
* 账户扣款
*/
void debit(String userId, int money);
}
// AccountService实现
@Service
public class AccountServiceImpl implements AccountService {
@Autowired
private AccountDao accountDao;
@Transactional(rollbackFor = Exception.class)
@Override
public void debit(String userId, int money) {
System.out.println("SEATA全局事务XID=================>" + RootContext.getXID());
Account account = accountDao.getOne(Integer.valueOf(userId));
account.setMoney(account.getMoney() - money);
accountDao.save(account);
}
}
// controller层
@RestController
public class AccountController {
@Autowired
private AccountService accountService;
@GetMapping("debit")
public String debit(@RequestParam("userId") String userId, @RequestParam("money") int money) {
accountService.debit(userId, money);
return "ok";
}
}
server:
port: 8002
# 数据源配置
spring:
application:
name: seata-order
datasource:
url: jdbc:mysql://localhost:3306/seata-order?serverTimezone=UTC&useUnicode=true&characterEncoding=utf-8
driver-class-name: com.mysql.cj.jdbc.Driver
type: com.alibaba.druid.pool.DruidDataSource # 自定义数据源
username: root
password: 123456
# 服务注册中心
cloud:
nacos:
server-addr: localhost:8848
# seata配置,与seata-server的registry.conf一致
seata:
enabled: true
tx-service-group: my_test_tx_group
registry:
type: nacos
nacos:
application: seata-server
server-addr: 127.0.0.1:8848
group: SEATA_GROUP
namespace:
username: nacos
password: nacos
config:
type: nacos
nacos:
server-addr: 127.0.0.1:8848
group: SEATA_GROUP
namespace:
username: nacos
password: nacos
编写AccountFeign.java
@FeignClient(name = "seata-account")
public interface AccountFeign {
@GetMapping("debit")
String debit(@RequestParam("userId") String userId, @RequestParam("money") int money);
}
@Entity
@Data
@Accessors(chain = true)
@Table(name="order_tbl")
public class Order implements Serializable {
@Id
private int id;
private String userId;
private String commodityCode;
private int count;
private int money;
}
public interface OrderDao extends JpaRepository<Order, Integer> {
}
public interface OrderService {
void createOrder(String userId, String commodityCode, int orderCount);
}
@Service
public class OrderServiceImpl implements OrderService {
@Autowired
private OrderDao orderDao;
@Autowired
private AccountFeign accountFeign;
@Override
@Transactional(rollbackFor = Exception.class)
public void createOrder(String userId, String commodityCode, int orderCount) {
System.out.println("SEATA全局事务id=================>" + RootContext.getXID());
// 调用account服务
accountFeign.debit(userId, 10);
Order order = new Order().setUserId(userId).setCommodityCode(commodityCode)
.setCount(orderCount).setMoney(10);
// 保存订单
orderDao.save(order);
}
}
@RestController
public class OrderController {
@Autowired
private OrderService orderService;
@GetMapping("createOrder")
public String createOrder(@RequestParam("userId") String userId, @RequestParam("commodityCode") String commodityCode, @RequestParam("orderCount") int orderCount) {
orderService.createOrder(userId, commodityCode, orderCount);
return "创建订单完成!";
}
}
正常流程是账户扣款成功 -> 创建订单成功
制造一个异常
@Service
public class OrderServiceImpl implements OrderService {
@Autowired
private OrderDao orderDao;
@Autowired
private AccountFeign accountFeign;
@Override
@Transactional(rollbackFor = Exception.class)
public void createOrder(String userId, String commodityCode, int orderCount) {
System.out.println("SEATA全局事务id=================>" + RootContext.getXID());
// 调用account服务
accountFeign.debit(userId, 10);
Order order = new Order().setUserId(userId).setCommodityCode(commodityCode)
.setCount(orderCount).setMoney(10);
// 保存订单
orderDao.save(order);
// ArithmeticException 异常
System.out.println(100/0);
}
}
此时,账户扣款成功 -> 订单创建失败,两个服务的事务没有一起回滚。
创建全局事务
@Service
public class OrderServiceImpl implements OrderService {
@Autowired
private OrderDao orderDao;
@Autowired
private AccountFeign accountFeign;
@Override
@GlobalTransactional
public void createOrder(String userId, String commodityCode, int orderCount) {
System.out.println("SEATA全局事务id=================>" + RootContext.getXID());
// 调用account服务
accountFeign.debit(userId, 10);
Order order = new Order().setUserId(userId).setCommodityCode(commodityCode)
.setCount(orderCount).setMoney(10);
// 保存订单
orderDao.save(order);
// ArithmeticException 异常
System.out.println(100/0);
}
}
把@Transactional换成@GlobalTransactional
表示开启全局事务,一旦有子事务失败,则全局事务回发起回滚,回滚所有子事务。