Seata 是什么?
Seata 是一款开源的分布式事务解决方案,致力于提供高性能和简单易用的分布式事务服务。Seata 将为用户提供了 AT、TCC、SAGA 和 XA 事务模式,为用户打造一站式的分布式解决方案。
关于Seata的介绍和事务的详细流转细节参考 Seata官网
本文采用docker部署seata服务
1、运行镜像
docker run --name seata-server -p 8091:8091 -d seataio/seata-server
2、复制配置文件到主机 当前目录
docker cp seata-server:/seata-server .
3、停止服务
docker stop seata-server
4、删除服务
docker rm seata-server
5、重新运行服务
# 脚本
# BEGIN ANSIBLE MANAGED BLOCK
#!/bin/bash
HOME="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
docker rm -f seata-server;
docker run --name seata-server \
--restart=always \
-v $HOME/seata-server:/seata-server \
-e SEATA_IP=192.168.8.43 \
-e SEATA_PORT=8091 \
-p 8091:8091 \
-d seataio/seata-server
# END ANSIBLE MANAGED BLOCK
6、切换到seata配置文件目录
cd /seata-server/resources
修改register.conf
- 如果是配置中心是file的话,会使用file.conf里面的配置,如果是其他,使用配置中心的配置
- 直连 eureka/consul/apollo/etcd/zookeeper/sofa/redis/file
本文采用nacos配置
- 修改 register.type 和 config.type 为 nacos
然后修改nacos的相关配置
# 注册中心配置
registry {
# file 、nacos 、eureka、redis、zk、consul、etcd3、sofa
type = "nacos" # 注册类型
loadBalance = "RandomLoadBalance"
loadBalanceVirtualNodes = 10
nacos {
application = "seata-server-lss"
serverAddr = "123.57.26.81:8848"
group = "SEATA_GROUP"
namespace = "lss_test"
cluster = "default"
username = "nacos"
password = "nacos"
}
}
# 配置中心配置
config {
# file、nacos 、apollo、zk、consul、etcd3
type = "nacos"
nacos {
serverAddr = "123.57.26.81:8848"
namespace = "lss_test"
group = "SEATA_GROUP"
username = "nacos"
password = "nacos"
dataId = "seataServer.properties"
}
}
7、重启seata-server
docker restart seata-server
查看nacos,发现seata 服务端已正常启动
接下来配置项目
参考seata官网的账户、订单项目
1、新建项目,如下图
2、pom.xml
4.0.0
org.springframework.boot
spring-boot-starter-parent
2.3.2.RELEASE
com.lss
seata_xa
1.0.0
pom
1.4.0
2.2.3.RELEASE
business_xa
order_xa
account_xa
org.springframework.boot
spring-boot-starter-web
org.projectlombok
lombok
1.18.8
com.alibaba.cloud
spring-cloud-starter-alibaba-nacos-config
com.alibaba.cloud
spring-cloud-starter-alibaba-nacos-discovery
com.alibaba.cloud
spring-cloud-starter-alibaba-seata
io.seata
seata-spring-boot-starter
io.seata
seata-spring-boot-starter
${seata.version}
com.alibaba
druid-spring-boot-starter
1.1.10
org.springframework.cloud
spring-cloud-starter-openfeign
2.1.0.RELEASE
mysql
mysql-connector-java
5.1.48
org.springframework.boot
spring-boot-starter-jdbc
2.1.0.RELEASE
org.apache.maven.plugins
maven-compiler-plugin
1.8
org.springframework.cloud
spring-cloud-dependencies
Hoxton.SR8
pom
import
com.alibaba.cloud
spring-cloud-alibaba-dependencies
${alibaba.cloud.version}
pom
import
3、config.txt 和 nacos-config.sh是从 官网 拷出来的配置,需要上传到nacos,具体也可参考/seata-server/resources/README-zh.md 里面的描述
- nacos-init.sh 是运行 nacos-config.sh 的脚本
#-h: host, the default value is localhost.
#
#-p: port, the default value is 8848.
#
#-g: Configure grouping, the default value is 'SEATA_GROUP'.
#
#-t: Tenant information, corresponding to the namespace ID field of Nacos, the default value is ''.
#
#-u: username, nacos 1.2.0+ on permission control, the default value is ''.
#
#-w: password, nacos 1.2.0+ on permission control, the default value is ''.
bash nacos-config.sh -h 123.57.26.81 -p 8848 -g SEATA_GROUP -t lss_test -u nacos -w nacos
执行后 bash nacos-init.sh
后,在nacos上可查看到相关配置
项目整体结构图
4、business_xa项目
- bootstrap.yml
server:
port: 17000
nacos:
namespace: lss_test
address: 123.57.26.81:8848
spring:
datasource:
url: jdbc:mysql://localhost:3306/xa_order?useSSL=false&serverTimezone=UTC
username: root
password: 12345678
driver-class-name: com.mysql.jdbc.Driver
application:
name: business_xa
cloud:
nacos:
namespace: ${nacos.namespace}
server-addr: ${nacos.address}
config:
enabled: true
namespace: ${spring.cloud.nacos.namespace}
# file-extension: yml
# shared-configs:
# - data-id: common.yml
# refresh: true
discovery:
enabled: true
namespace: ${spring.cloud.nacos.namespace}
register-enabled: true
# seata 配置
seata:
# 注册信息
registry:
type: nacos
nacos:
application: seata-server-lss # 这个是 seata 服务端的应用名称
server-addr: ${nacos.address} # nacos 服务地址
group : "SEATA_GROUP" # nacos 分组
namespace: ${nacos.namespace} # nacos 命名空间
username: "nacos"
password: "nacos"
# 配置信息
config:
type: nacos
nacos:
server-addr: 123.57.26.81:8848
group: "SEATA_GROUP"
namespace: ${nacos.namespace}
username: "nacos"
password: "nacos"
application-id: ${spring.application.name}
tx-service-group: seata-server_xa_test # 自定义事物组 tc
- BusinessController.java
package com.lss.sample.operator;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
@RequestMapping("api")
@RestController
@Slf4j
public class BusinessController {
@Autowired
BusinessService businessService;
@GetMapping("purchase")
public String purchase(@RequestParam(value = "type", defaultValue = "1") Integer type ) {
try {
businessService.purchase(type);
} catch (Exception exx) {
log.info("异常:{}", exx);
return "Purchase Failed:" + exx.getMessage();
}
return "SUCCESS";
}
}
- BusinessService.java
package com.lss.sample.operator;
import com.lss.sample.feign.AccountFeignClient;
import com.lss.sample.feign.OrderFeignClient;
import io.seata.core.context.RootContext;
import io.seata.spring.annotation.GlobalTransactional;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
@Service
@Slf4j
public class BusinessService {
final String SUCCESS = "SUCCESS";
@Autowired
private OrderFeignClient orderFeignClient;
@Autowired
AccountFeignClient accountFeignClient;
@GlobalTransactional(rollbackFor = Exception.class)
public void purchase(Integer type) {
String xid = RootContext.getXID();
log.info("business-xid:{}", xid);
String accountResult = accountFeignClient.add();
throw new RuntimeException("账户服务调用失败,事务回滚!");
if (!SUCCESS.equals(accountResult)) {
throw new RuntimeException("账户服务调用失败,事务回滚!");
}else {
log.info("账户服务调用成功...");
}
//
String orderResult = orderFeignClient.create(type);
if (!SUCCESS.equals(orderResult)) {
log.info("订单服务调用失败...");
throw new RuntimeException("订单服务调用失败,事务回滚!");
}else {
log.info("订单服务调用成功...");
}
}
}
- AccountFeignClient.java
package com.lss.sample.feign;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
@FeignClient(name = "AccountFeignClient", url = "127.0.0.1:17002")
public interface AccountFeignClient {
@GetMapping("api/add")
String add();
}
- OrderFeignClient.java
package com.lss.sample.feign;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
@FeignClient(name = "OrderFeignClient", url = "127.0.0.1:17001")
public interface OrderFeignClient {
@GetMapping("api/create")
String create(@RequestParam(value = "type") Integer type);
}
- 启动类 BusinessXAApplication.java
package com.lss.sample;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.openfeign.EnableFeignClients;
@SpringBootApplication
@EnableFeignClients
public class BusinessXAApplication {
public static void main(String[] args) {
SpringApplication.run(BusinessXAApplication.class, args);
}
}
5、account_xa项目
- account_xa服务的bootstrap.yml
server:
port: 17002
nacos:
namespace: lss_test
address: 123.57.26.81:8848
spring:
datasource:
url: jdbc:mysql://localhost:3306/xa_account?useSSL=false&serverTimezone=UTC
username: root
password: 12345678
# type: com.alibaba.druid.pool.DruidDataSource
driver-class-name: com.mysql.jdbc.Driver
application:
name: account_xa
cloud:
nacos:
namespace: ${nacos.namespace}
server-addr: ${nacos.address}
config:
enabled: true
namespace: ${spring.cloud.nacos.namespace}
# file-extension: yml
# shared-configs:
# - data-id: common.yml
# refresh: true
discovery:
enabled: true
namespace: ${spring.cloud.nacos.namespace}
register-enabled: true
# seata 配置
seata:
# 注册信息
registry:
type: nacos
nacos:
application: seata-server-lss # 这个是 seata 服务端的应用名称
server-addr: ${nacos.address} # nacos 服务地址
group : "SEATA_GROUP" # nacos 分组
namespace: ${nacos.namespace} # nacos 命名空间
username: "nacos"
password: "nacos"
# 配置信息
config:
type: nacos
nacos:
server-addr: 123.57.26.81:8848
group: "SEATA_GROUP"
namespace: ${nacos.namespace}
username: "nacos"
password: "nacos"
application-id: ${spring.application.name}
tx-service-group: seata-server_xa_test # 自定义事物组 tc
- AccountController.java
package com.lss.sample.operator;
import io.seata.core.context.RootContext;
import io.seata.spring.annotation.GlobalTransactional;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("api")
@Slf4j
public class AccountController {
@Autowired
private JdbcTemplate jdbcTemplate;
@GetMapping("add")
public String add() {
String xid = RootContext.getXID();
log.info("account-xid:{}", xid);
jdbcTemplate.update("update account set account = 10000 where id = 4 ");
return "SUCCESS";
}
}
- DataSourceConfig.java
package com.lss.sample.config;
import com.alibaba.druid.pool.DruidDataSource;
import io.seata.rm.datasource.DataSourceProxy;
import io.seata.rm.datasource.xa.DataSourceProxyXA;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.jdbc.core.JdbcTemplate;
import javax.sql.DataSource;
@Configuration
public class DataSourceConfig {
@Bean
@ConfigurationProperties(prefix = "spring.datasource")
public DruidDataSource druidDataSource() {
return new DruidDataSource();
}
@Bean("dataSourceProxy")
public DataSource dataSource(DruidDataSource druidDataSource) {
// DataSourceProxy for AT mode
// return new DataSourceProxy(druidDataSource);
// DataSourceProxyXA for XA mode
return new DataSourceProxyXA(druidDataSource);
}
@Bean("jdbcTemplate")
public JdbcTemplate jdbcTemplate(DataSource dataSourceProxy) {
return new JdbcTemplate(dataSourceProxy);
}
}
- 启动类 AccountXAApplication
package com.lss.sample;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.openfeign.EnableFeignClients;
import org.springframework.transaction.annotation.EnableTransactionManagement;
@SpringBootApplication
@EnableFeignClients
@EnableTransactionManagement
public class AccountXAApplication {
public static void main(String[] args) {
SpringApplication.run(AccountXAApplication.class, args);
}
}
6、order_xa项目
- bootstrap.yml
server:
port: 17001
nacos:
namespace: lss_test
address: 123.57.26.81:8848
spring:
datasource:
url: jdbc:mysql://localhost:3306/xa_order?useSSL=false&serverTimezone=UTC
username: root
password: 12345678
driver-class-name: com.mysql.jdbc.Driver
application:
name: order_xa
cloud:
nacos:
namespace: ${nacos.namespace}
server-addr: ${nacos.address}
config:
enabled: true
namespace: ${spring.cloud.nacos.namespace}
# file-extension: yml
# shared-configs:
# - data-id: common.yml
# refresh: true
discovery:
enabled: true
namespace: ${spring.cloud.nacos.namespace}
register-enabled: true
# seata 配置
seata:
# 注册信息
registry:
type: nacos
nacos:
application: seata-server-lss # 这个是 seata 服务端的应用名称
server-addr: ${nacos.address} # nacos 服务地址
group : "SEATA_GROUP" # nacos 分组
namespace: ${nacos.namespace} # nacos 命名空间
username: "nacos"
password: "nacos"
# 配置信息
config:
type: nacos
nacos:
server-addr: 123.57.26.81:8848
group: "SEATA_GROUP"
namespace: ${nacos.namespace}
username: "nacos"
password: "nacos"
application-id: ${spring.application.name}
tx-service-group: seata-server_xa_test # 自定义事物组 tc
- OrderController.java
package com.lss.sample.operator;
import io.seata.core.context.RootContext;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
@RequestMapping("api")
@RestController
@Slf4j
public class OrderController {
@Autowired
JdbcTemplate jdbcTemplate;
@GetMapping("create")
public String create(@RequestParam(value = "type") Integer type) {
String xid = RootContext.getXID();
log.info("account-xid:{}", xid);
try {
deal(type);
} catch (Exception e) {
return "FAIL";
}
return "SUCCESS";
}
@Transactional
public void deal(Integer type) {
jdbcTemplate.update("update `order` set num = 100 where id = 1 ");
if (type.equals(2)) {
log.info("order 调用异常...");
throw new RuntimeException("order 调用异常...");
}
}
}
- DataSourceConfig
package com.lss.sample.config;
import com.alibaba.druid.pool.DruidDataSource;
import io.seata.rm.datasource.xa.DataSourceProxyXA;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.jdbc.core.JdbcTemplate;
import javax.sql.DataSource;
@Configuration
public class DataSourceConfig {
@Bean
@ConfigurationProperties(prefix = "spring.datasource")
public DruidDataSource druidDataSource() {
return new DruidDataSource();
}
@Bean("dataSourceProxy")
public DataSource dataSource(DruidDataSource druidDataSource) {
// DataSourceProxy for AT mode
// return new DataSourceProxy(druidDataSource);
// DataSourceProxyXA for XA mode
return new DataSourceProxyXA(druidDataSource);
}
@Bean("jdbcTemplate")
public JdbcTemplate jdbcTemplate(DataSource dataSourceProxy) {
return new JdbcTemplate(dataSourceProxy);
}
}
- 启动类OrderXAApplication.java
package com.lss.sample;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.openfeign.EnableFeignClients;
@SpringBootApplication
@EnableFeignClients
public class OrderXAApplication {
public static void main(String[] args) {
SpringApplication.run(OrderXAApplication.class, args);
}
}
运行项目
1、business_xa、account_xa、order_xa依次运行后,在nacos上发现项目都已经注册上去了,但是项目会报一个错误
no available service 'null' found, please make sure registry config correct
报错原因在这个类里 io.seata.core.rpc.netty.NettyClientChannelManager
根据seata导入客户端相关配置后
添加一个配置
- service.vgroupMapping.(自定义的事务组名称 tc)=default 就可以了
本文的配置为:service.vgroupMapping.seata-server_xa_test = default
nacos上如下图
2、在数据库中添加sql脚本
- xa_account 库
DROP TABLE IF EXISTS `account`;
CREATE TABLE `account` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`account` varchar(11) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=5 DEFAULT CHARSET=utf8mb4;
DROP TABLE IF EXISTS `undo_log`;
CREATE TABLE `undo_log` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`branch_id` bigint(20) NOT NULL,
`xid` varchar(100) NOT NULL,
`context` varchar(128) NOT NULL,
`rollback_info` longblob NOT NULL,
`log_status` int(11) NOT NULL,
`log_created` datetime NOT NULL,
`log_modified` datetime NOT NULL,
`ext` varchar(100) DEFAULT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `ux_undo_log` (`xid`,`branch_id`)
) ENGINE=InnoDB AUTO_INCREMENT=12 DEFAULT CHARSET=utf8;
- xa_order 库
DROP TABLE IF EXISTS `order`;
CREATE TABLE `order` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`num` int(11) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8mb4;
DROP TABLE IF EXISTS `undo_log`;
CREATE TABLE `undo_log` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`branch_id` bigint(20) NOT NULL,
`xid` varchar(100) NOT NULL,
`context` varchar(128) NOT NULL,
`rollback_info` longblob NOT NULL,
`log_status` int(11) NOT NULL,
`log_created` datetime NOT NULL,
`log_modified` datetime NOT NULL,
`ext` varchar(100) DEFAULT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `ux_undo_log` (`xid`,`branch_id`)
) ENGINE=InnoDB AUTO_INCREMENT=12 DEFAULT CHARSET=utf8;
3、至此,seata集成已经完成了,启动项目后,访问business_xa服务
curl http://localhost:17000/api/purchase?type=1
发现数据库数据被正常更新
重新手动更新数据后,再次访问
curl http://localhost:17000/api/purchase?type=2
发现数据库数据回滚