一、先启动注册中心
这里使用注册中心eureka
https://blog.csdn.net/jy02268879/article/details/100176361
二、seata-server服务端启动及其配置
下载seata-server服务端代码:https://github.com/seata/seata/releases
要跑在Linux上就下载seata-server-1.4.1.tar.gz
我这里是跑在windows上做个测试,所以下的seata-server-1.4.1.zip
下载后可见目录
修改服务端配置:
conf/registry.conf
registry {
# file 、nacos 、eureka、redis、zk、consul、etcd3、sofa
# 我这里用的注册中心是eureka
type = "eureka"
loadBalance = "RandomLoadBalance"
loadBalanceVirtualNodes = 10
nacos {
application = "seata-server"
serverAddr = "127.0.0.1:8848"
group = "SEATA_GROUP"
namespace = ""
cluster = "default"
username = ""
password = ""
}
# 我这里用的注册中心是eureka
# 配置eureka注册中心的地址
# 配置本服务在注册中心中叫什么名字
eureka {
serviceUrl = "http://localhost:8100/eureka"
application = "seata_tc_server"
weight = "1"
}
redis {
serverAddr = "localhost:6379"
db = 0
password = ""
cluster = "default"
timeout = 0
}
zk {
cluster = "default"
serverAddr = "127.0.0.1:2181"
sessionTimeout = 6000
connectTimeout = 2000
username = ""
password = ""
}
consul {
cluster = "default"
serverAddr = "127.0.0.1:8500"
}
etcd3 {
cluster = "default"
serverAddr = "http://localhost:2379"
}
sofa {
serverAddr = "127.0.0.1:9603"
application = "default"
region = "DEFAULT_ZONE"
datacenter = "DefaultDataCenter"
cluster = "default"
group = "SEATA_GROUP"
addressWaitTime = "3000"
}
file {
name = "file.conf"
}
}
# 我这里配置中心就是用的file ,配置文件,就是安装包中的file.conf文件
config {
# file、nacos 、apollo、zk、consul、etcd3
type = "file"
nacos {
serverAddr = "127.0.0.1:8848"
namespace = ""
group = "SEATA_GROUP"
username = ""
password = ""
}
consul {
serverAddr = "127.0.0.1:8500"
}
apollo {
appId = "seata-server"
apolloMeta = "http://192.168.1.204:8801"
namespace = "application"
apolloAccesskeySecret = ""
}
zk {
serverAddr = "127.0.0.1:2181"
sessionTimeout = 6000
connectTimeout = 2000
username = ""
password = ""
}
etcd3 {
serverAddr = "http://localhost:2379"
}
file {
name = "file.conf"
}
}
conf/file.conf
## transaction log store, only used in seata-server
store {
# 这里表示seata-server服务端用什么数据库,这里用的mysql
## store mode: file、db、redis
mode = "db"
## file store property
file {
## store location dir
dir = "sessionStore"
# branch session size , if exceeded first try compress lockkey, still exceeded throws exceptions
maxBranchSessionSize = 16384
# globe session size , if exceeded throws exceptions
maxGlobalSessionSize = 512
# file buffer size , if exceeded allocate new buffer
fileWriteBufferCacheSize = 16384
# when recover batch read size
sessionReloadReadSize = 100
# async, sync
flushDiskMode = async
}
## database store property
# 用的数据库的地址账号密码驱动之类的
db {
## the implement of javax.sql.DataSource, such as DruidDataSource(druid)/BasicDataSource(dbcp)/HikariDataSource(hikari) etc.
datasource = "dbcp"
## mysql/oracle/postgresql/h2/oceanbase etc.
dbType = "mysql"
driverClassName = "com.mysql.jdbc.Driver"
url = "jdbc:mysql://127.0.0.1:3306/seata"
user = "root"
password = "root"
minConn = 1
maxConn = 10
globalTable = "global_table"
branchTable = "branch_table"
lockTable = "lock_table"
queryLimit = 100
maxWait = 5000
}
## redis store property
redis {
host = "127.0.0.1"
port = "6379"
password = ""
database = "0"
minConn = 1
maxConn = 10
maxTotal = 100
queryLimit = 100
}
}
这里就需要去file.conf的配置文件里面配的mysql中去创建三张表
CREATE TABLE `branch_table` (
`branch_id` bigint(20) NOT NULL,
`xid` varchar(128) NOT NULL,
`transaction_id` bigint(20) DEFAULT NULL,
`resource_group_id` varchar(32) DEFAULT NULL,
`resource_id` varchar(256) DEFAULT NULL,
`branch_type` varchar(8) DEFAULT NULL,
`status` tinyint(4) DEFAULT NULL,
`client_id` varchar(64) DEFAULT NULL,
`application_data` varchar(2000) DEFAULT NULL,
`gmt_create` datetime DEFAULT NULL,
`gmt_modified` datetime DEFAULT NULL,
PRIMARY KEY (`branch_id`),
KEY `idx_xid` (`xid`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
CREATE TABLE `global_table` (
`xid` varchar(128) NOT NULL,
`transaction_id` bigint(20) DEFAULT NULL,
`status` tinyint(4) NOT NULL,
`application_id` varchar(32) DEFAULT NULL,
`transaction_service_group` varchar(32) DEFAULT NULL,
`transaction_name` varchar(128) DEFAULT NULL,
`timeout` int(11) DEFAULT NULL,
`begin_time` bigint(20) DEFAULT NULL,
`application_data` varchar(2000) DEFAULT NULL,
`gmt_create` datetime DEFAULT NULL,
`gmt_modified` datetime DEFAULT NULL,
PRIMARY KEY (`xid`),
KEY `idx_gmt_modified_status` (`gmt_modified`,`status`),
KEY `idx_transaction_id` (`transaction_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
CREATE TABLE `lock_table` (
`row_key` varchar(128) NOT NULL,
`xid` varchar(96) DEFAULT NULL,
`transaction_id` bigint(20) DEFAULT NULL,
`branch_id` bigint(20) NOT NULL,
`resource_id` varchar(256) DEFAULT NULL,
`table_name` varchar(32) DEFAULT NULL,
`pk` varchar(36) DEFAULT NULL,
`gmt_create` datetime DEFAULT NULL,
`gmt_modified` datetime DEFAULT NULL,
PRIMARY KEY (`row_key`),
KEY `idx_branch_id` (`branch_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
现在可以启动seata-server客户端了
bin/seata-server.bat
三、客户端代码
代码例子
用户购买商品的业务逻辑。整个业务逻辑由3个微服务提供支持:
- 仓储服务:对给定的商品扣除仓储数量。
- 订单服务:根据采购需求创建订单。
- 帐户服务:从用户帐户中扣除余额。
注意这里,每个服务是单独的数据库
注意,要用seata就需要在每个服务的数据库中都创建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=21 DEFAULT CHARSET=utf8;
CREATE TABLE `account` (
`user_id` varchar(50) NOT NULL,
`money` double DEFAULT NULL COMMENT '余额',
PRIMARY KEY (`user_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
CREATE TABLE `business_table` (
`business_id` bigint(20) NOT NULL AUTO_INCREMENT,
`user_id` varchar(50) DEFAULT NULL,
`commodity_code` varchar(50) DEFAULT NULL,
`order_count` int(11) DEFAULT NULL,
PRIMARY KEY (`business_id`)
) ENGINE=InnoDB AUTO_INCREMENT=64 DEFAULT CHARSET=utf8;
CREATE TABLE `order_table` (
`order_id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '订单号',
`user_id` varchar(50) DEFAULT NULL,
`commodity_code` varchar(50) DEFAULT NULL COMMENT '商品代码',
`order_count` int(11) DEFAULT NULL,
`order_money` double DEFAULT NULL,
PRIMARY KEY (`order_id`)
) ENGINE=InnoDB AUTO_INCREMENT=56 DEFAULT CHARSET=utf8;
CREATE TABLE `storage` (
`commodity_code` varchar(50) NOT NULL COMMENT '商品代码',
`storage_count` int(11) DEFAULT NULL COMMENT '库存数量',
PRIMARY KEY (`commodity_code`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
每一个需要用到seata的服务需要的修改
目录
pom.xml
<!--seata todo 如果用2.2.0release 启动后一直报错:string不能转换为boolean service.disableGlobalTransaction-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-alibaba-seata</artifactId>
<version>2.1.0.RELEASE</version>
<exclusions>
<exclusion>
<artifactId>seata-all</artifactId>
<groupId>io.seata</groupId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<artifactId>seata-all</artifactId>
<groupId>io.seata</groupId>
<version>1.4.1</version>
</dependency>
在resource目录下有两个配置文件file.conf registry.conf
只要需要用到seata的服务,都需要在在即的resource目录下建立者两个文件
registry.conf
registry {
# file 、nacos 、eureka、redis、zk、consul、etcd3、sofa
type = "eureka"
loadBalance = "RandomLoadBalance"
loadBalanceVirtualNodes = 10
nacos {
application = "seata-server"
serverAddr = "127.0.0.1:8848"
group = "SEATA_GROUP"
namespace = ""
cluster = "default"
username = ""
password = ""
}
eureka {
serviceUrl = "http://localhost:8100/eureka"
application = "seata_tc_server"
weight = "1"
}
redis {
serverAddr = "localhost:6379"
db = 0
password = ""
cluster = "default"
timeout = 0
}
zk {
cluster = "default"
serverAddr = "127.0.0.1:2181"
sessionTimeout = 6000
connectTimeout = 2000
username = ""
password = ""
}
consul {
cluster = "default"
serverAddr = "127.0.0.1:8500"
}
etcd3 {
cluster = "default"
serverAddr = "http://localhost:2379"
}
sofa {
serverAddr = "127.0.0.1:9603"
application = "default"
region = "DEFAULT_ZONE"
datacenter = "DefaultDataCenter"
cluster = "default"
group = "SEATA_GROUP"
addressWaitTime = "3000"
}
file {
name = "file.conf"
}
}
config {
# file、nacos 、apollo、zk、consul、etcd3
type = "file"
nacos {
serverAddr = "127.0.0.1:8848"
namespace = ""
group = "SEATA_GROUP"
username = ""
password = ""
}
consul {
serverAddr = "127.0.0.1:8500"
}
apollo {
appId = "seata-server"
apolloMeta = "http://192.168.1.204:8801"
namespace = "application"
apolloAccesskeySecret = ""
}
zk {
serverAddr = "127.0.0.1:2181"
sessionTimeout = 6000
connectTimeout = 2000
username = ""
password = ""
}
etcd3 {
serverAddr = "http://localhost:2379"
}
file {
name = "file.conf"
}
}
file.conf
transport {
# tcp udt unix-domain-socket
type = "TCP"
#NIO NATIVE
server = "NIO"
#enable heartbeat
heartbeat = true
# the client batch send request enable
enableClientBatchSendRequest = true
#thread factory for netty
threadFactory {
bossThreadPrefix = "NettyBoss"
workerThreadPrefix = "NettyServerNIOWorker"
serverExecutorThread-prefix = "NettyServerBizHandler"
shareBossWorker = false
clientSelectorThreadPrefix = "NettyClientSelector"
clientSelectorThreadSize = 1
clientWorkerThreadPrefix = "NettyClientWorkerThread"
# netty boss thread size,will not be used for UDT
bossThreadSize = 1
#auto default pin or 8
workerThreadSize = "default"
}
shutdown {
# when destroy server, wait seconds
wait = 3
}
serialization = "seata"
compressor = "none"
}
service {
#这里注意,等号前后都是配置,前面是yml里配置的事务组,后面是register.conf里定义的seata-server
vgroupMapping.test_tx_group = "seata_tc_server"
#only support when registry.type=file, please don't set multiple addresses
seata_tc_server.grouplist = "127.0.0.1:8091"
#degrade, current not support
enableDegrade = false
#disable seata
disableGlobalTransaction = false
}
client {
rm {
asyncCommitBufferLimit = 10000
lock {
retryInterval = 10
retryTimes = 30
retryPolicyBranchRollbackOnConflict = true
}
reportRetryCount = 5
tableMetaCheckEnable = false
reportSuccessEnable = false
}
tm {
commitRetryCount = 5
rollbackRetryCount = 5
}
undo {
dataValidation = true
logSerialization = "jackson"
logTable = "undo_log"
}
log {
exceptionRate = 100
}
}
application.yml 只要需要用到seata的服务,都需要在配置这个
###起个名字作为服务名称(该服务注册到eureka注册中心的名称,比如订单服务)
spring:
cloud:
alibaba:
seata:
tx-service-group: test_tx_group # 定义事务组的名称
启动类里面加上 只要需要用到seata的服务,都需要在配置这个
@SpringBootApplication(exclude = DataSourceAutoConfiguration.class)
因为这里是用的mybaits所以需要再加个配置类
package com.sid.config;
import com.alibaba.druid.pool.DruidDataSource;
import io.seata.rm.datasource.DataSourceProxy;
import org.apache.ibatis.session.SqlSessionFactory;
import org.mybatis.spring.SqlSessionFactoryBean;
import org.mybatis.spring.transaction.SpringManagedTransactionFactory;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
import javax.sql.DataSource;
@Configuration
public class DataSourceProxyConfig {
@Bean(destroyMethod = "close", initMethod = "init")
@ConfigurationProperties(prefix = "spring.datasource")
public DruidDataSource druidDataSource() {
return new DruidDataSource();
}
@Bean
@Primary
public DataSourceProxy dataSourceProxy(DruidDataSource druidDataSource) {
return new DataSourceProxy(druidDataSource);
}
@Bean
public SqlSessionFactory sqlSessionFactory(DataSourceProxy dataSourceProxy) throws Exception {
SqlSessionFactoryBean factoryBean = new SqlSessionFactoryBean();
factoryBean.setDataSource(dataSourceProxy);
/**
* 如果不自己把mapping的路径设置进去就一直报错
* ibatis.binding.BindingException: Invalid bound statement (not found)
* */
factoryBean.setMapperLocations(new PathMatchingResourcePatternResolver()
.getResources("classpath*:/mapping/*.xml"));
factoryBean.setTransactionFactory(new SpringManagedTransactionFactory());
return factoryBean.getObject();
}
}
使用
在business的service代码中
在该方法外面加入@GlobalTransactional表示开启全局事务
/**
* 采购
*/
@GlobalTransactional
@Transactional
@Override
public void purchase(String userId, String commodityCode, int orderCount) {
Business b = new Business();
b.setUserId(userId);
b.setCommodityCode(commodityCode);
b.setOrderCount(orderCount);
int i = businessMapper.insertSelective(b);
//远程调用
String storageDeductResult = storageService.deduct(commodityCode, orderCount);
//远程调用
Order order = orderService.create(userId, commodityCode, orderCount);
String orderCreateResult = order.getMsg();
if(!storageDeductResult.equals("success") || !orderCreateResult.equals("success")){
System.out.println("resutl storage service :"+storageDeductResult);
System.out.println("resutl order service :"+orderCreateResult);
throw new RuntimeException("BusinessServiceImpl purchase fail");
}
}
在storage的service中
/**
* 商品扣库存
* */
@Override
@Transactional
public String deduct(String commodityCode, int count) {
Storage storage = storageMapper.selectByPrimaryKey(commodityCode);
Integer storageCount = storage.getStorageCount();
storage.setStorageCount(storageCount - count);
int i = storageMapper.updateByPrimaryKey(storage);
//int ii = 1/0;
if(i == 1){
return "success";
}
return "fail";
}
在order的service中
/**
* 创建订单
*/
@Transactional
public Order create(String userId, String commodityCode, Integer orderCount) {
Double orderMoney = Double.valueOf(orderCount);
Order order = new Order();
order.setUserId(userId);
order.setCommodityCode(commodityCode);
order.setOrderCount(orderCount);
order.setOrderMoney(orderMoney);
orderMapper.insertSelective(order);
/**
* 这里的RPC调用是同步的
* todo 需要写个异步的例子 这个可能需要用dubbo来写哦 feign都是同步个嘛
* */
String accountDebitResult = accountService.debit(userId, orderMoney);
if (!accountDebitResult.equals("success")) {
System.out.println("result account service :"+accountDebitResult);
throw new RuntimeException("OrderService create method rpc request accountService debit fail. accountDebitResult:"+accountDebitResult);
}
order.setMsg("success");
return order;
}
在account的service中
/**
* 扣钱
* */
@Override
@Transactional
public String debit(String userId, Double money) {
Account account = accountMapper.selectByPrimaryKey(userId);
Double money1 = account.getMoney();
account.setMoney(money1-money);
int i = accountMapper.updateByPrimaryKey(account);
if(i == 1){
return "success";
}
return "fail";
}