配置
版本说明:
springCloud Alibaba组件版本关系
我用的是spring cloud Alibaba 2.2.1.RELEASE 、springboot 2.2.1.RELEASE、nacos 2.0.1、seata1.2.0,jdk1.8
seata 主要用于在分布式系统中对数据库进行事务回滚,保证全局事务的一致性。
seata的使用非常简单,主要是在涉及到跨服务调用并且要保证各服务事务的一致性的方法上添加注解@GlobalTransactional即可。重点和难点在seata的配置,因此本篇文章的重点也放在seata的配置上。
特别说明:seata和各组件版本对应关系非常重要,如果引用的seata版本跟springboot,cloud不匹配,会产生各种各样的问题,因此务必引起重视。
一、seata的下载和安装
下载地址:Releases · apache/incubator-seata · GitHub
找到1.2.0版本,拉到Assets部分展开,找到自己需要的安装包并下载。
windows的下载后解压即可。
下面说下docker的安装方式:
#拉取Seata镜像
docker pull seataio/seata-server:1.2.0#运行镜像
docker run --name seata-server -p 8091:8091 -d seataio/seata-server:1.2.0#复制seata的配置文件到主机
docker cp seata-server:/seata-server /root/seata#停止删除服务
docker stop seata-server
docker rm seata-server#重新运行镜像
docker run -d --restart always --name seata-server -p 8091:8091 -v /root/seata:/seata-server -e SEATA_IP=192.168.200.131 -e SEATA_PORT=8091 seataio/seata-server:1.2.0#注意:SEATA_IP、SEATA_PORT 一定要重新指定下,不然用docker自动分配的虚拟ip,服务是访问不到的
以下配置相同,无论是Windows、Linux还是docker,只是配置文件位置不同,下面以Windows为例。
1、修改seata-server-1.2.0\seata\conf\file.conf
文件,标红部分是需要修改的,其他部分可忽略或根据个人需要修改。
## transaction log store, only used in seata-server
store {
## store mode: file、db
mode = "db" #!!!!!!!!!这里改为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) etc.
datasource = "druid"
## mysql/oracle/postgresql/h2/oceanbase etc.
dbType = "mysql"
driverClassName = "com.mysql.jdbc.Driver"
url = "jdbc:mysql://192.168.200.131:3306/seata" #你的msyql地址
user = "root" #用户名
password = "root" #密码
minConn = 5
maxConn = 30
globalTable = "global_table"
branchTable = "branch_table"
lockTable = "lock_table"
queryLimit = 100
maxWait = 5000
}
}
2、修改seata-server-1.2.0\seata\conf\registry.conf
文件
registry {
# file 、nacos 、eureka、redis、zk、consul、etcd3、sofa
type = "nacos" #改为nacosnacos {
application = "seata-server"
serverAddr = "192.168.200.199" #nacos注册中心地址
namespace = ""
cluster = "default"
username = "nacos" #nacos用户名
password = "nacos" #nacos密码
}
eureka {
serviceUrl = "http://localhost:8761/eureka"
application = "default"
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 = "nacos" #!!!!!!!!!改为nacosnacos {
serverAddr = "192.168.200.199" #!!!!!!!!!配置中心地址
namespace = "" #配置所在的命名空间,不配置就是public
group = "SEATA_GROUP"
username = "nacos" #!!!!!!!!!用户名
password = "nacos" #!!!!!!!!!密码
}
consul {
serverAddr = "127.0.0.1:8500"
}
apollo {
appId = "seata-server"
apolloMeta = "http://192.168.1.204:8801"
namespace = "application"
}
zk {
serverAddr = "127.0.0.1:2181"
sessionTimeout = 6000
connectTimeout = 2000
username = ""
password = ""
}
etcd3 {
serverAddr = "http://localhost:2379"
}
file {
name = "file.conf"
}
}
3、首先启动Nacos服务,然后再启动Seata,查看nacos服务注册中心是否注册上了Seata
双击上图圈中的bat文件,如果一切正常,将会看到started的提示,如果双击后闪退,可采用cmd的方式打开,即可看到启动失败的原因。比如jdk版本和seata版本不对应也会导致闪退。我的jdk最初是17,跟seata不对应,后面提示信息后,调整为jdk8就可正常启动了。说句题外话,jdk17安装后无需进行环境变量配置,并且会将本机的jre设置为最新的版本17。
nacos查看seata是否注册进来:
默认在public的命名空间下,端口号是8091。
二、搭建Seata运行环境
1、在mysql数据库中创建名为seata的库,并创建以下3张数据表
-- -------------------------------- 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;
2、要在参与全局事务的每个数据库中都加入undo_log
这张表
-- 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`
(
`id` BIGINT(20) NOT NULL AUTO_INCREMENT COMMENT 'increment id',
`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 NOT NULL COMMENT 'create datetime',
`log_modified` DATETIME NOT NULL COMMENT 'modify datetime',
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';
3、从官方github仓库拿到参考配置做修改 incubator-seata/script/client/spring at develop · apache/incubator-seata · GitHub
加到你项目的application.yml中,红色部分需修改为自己的应用名。
seata:
enabled: true
application-id: shopping-mall
tx-service-group: my_test_tx_group
enable-auto-data-source-proxy: true
config:
type: nacos
nacos:
namespace:
serverAddr: 127.0.0.1:8848
group: SEATA_GROUP
userName: "nacos"
password: "nacos"
registry:
type: nacos
nacos:
application: seata-server
server-addr: 127.0.0.1:8848
namespace:
userName: "nacos"
password: "nacos"
4、由于Seata1.2.0支持从Nacos读取配置,所以我们还需要一个bootstrap.yml
读取配置信息
#Nacos同springcloud-config-样, 在项目初始化时,要保证先从配置中心进行配置拉取,拉取配置之后,才能保证项目的正常启动。
#springboot中配置文件的加载是存在优先级顺序的,bootstrap优先级高于application#当前服务端口号
server:
port: 2002spring:
application:
name: seata-storage-service #当前服务名称
main:
allow-bean-definition-overriding: true
cloud:
loadbalancer:
retry:
enabled: false
nacos:
discovery:
server-addr: 192.168.200.199 #通过虚拟IP访问Nginx主服务器,然后反向代理到其中一台nacos注册中心
config:
server-addr: 192.168.200.199 #通过虚拟IP访问Nginx主服务器,然后反向代理到其中一台nacos配置中心#设置feign客户端超时时间(OpenFeign默认支持ribbon)
ribbon:
ReadTimeout: 5000 #指的是建立连接所用的时间,适用于网络状况正常的情况下,两端连接所用的时间
connectTimeout: 5000 #指的是建立连接后从服务器读取到可用资源所用的时间
在需要用到seata的服务添加上依赖
<!--SpringCloudAlibaba的seata分布式事务管理-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-alibaba-seata</artifactId>
<version>2.2.0.RELEASE</version>
<!--需要排除掉自带的seata-spring-boot-starter,否则无法启动,并提示jar冲突-->
<exclusions>
<exclusion>
<groupId>io.seata</groupId>
<artifactId>seata-spring-boot-starter</artifactId>
</exclusion>
</exclusions>
</dependency>
<!--指定与安装的seata版本一致,重要!-->
<dependency>
<groupId>io.seata</groupId>
<artifactId>seata-spring-boot-starter</artifactId>
<version>1.2.0</version>
</dependency>
jar引用这里还需要注意,如果只引入下面的依赖,将不能实现事务回滚,seata 1.2版本需要引用spring-cloud-starter-alibaba-seata依赖。这个依赖里才有xid传递的功能。 seata-spring-boot-starter依赖并没有xid传递的功能。spring-cloud-alibaba-seata需要和seata-spring-boot-starter一起引入并注意它们的版本对应关系,并在此之前引入nacos注册和配置中心的依赖。
<dependency>
<groupId>io.seata</groupId>
<artifactId>seata-spring-boot-starter</artifactId>
<version>1.2.0</version>
</dependency>
6、将Seata项目克隆或下载到本地 GitHub - apache/incubator-seata: :fire: Seata is an easy-to-use, high-performance, open source distributed transaction solution.
重点是下面红色标记的文件:
下载到本地后进入\seata\script\config-center目录修改config.txt为以下内容:
service.vgroupMapping.my_test_tx_group=default
store.mode=db
store.db.datasource=druid
store.db.dbType=mysql
store.db.driverClassName=com.mysql.jdbc.Driver
store.db.url=jdbc:mysql://192.168.200.131:3306/seata?useUnicode=true
store.db.user=root
store.db.password=root
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
运行仓库中提供的nacos脚本,将以上信息提交到nacos控制台,如果有需要更改,可直接通过控制台更改。如果电脑中安装有git,双击后在弹出的窗口中选择使用git运行sh命令,就会看到seata的配置同步到nacos配置中心。
注意:
如果你的nacos地址不是本机,需要修改脚本nacos-config.sh
,他是默认使用本机nacos的,修改地方如下
导入完成后,在nacos配置中心,即可看到导入的配置文件,都归到SEATA_GROUP组下,足足有7页之多
三、测试事务是否成功
经过以上配置,就可以验证seata是否生效了
@GlobalTransactional
@Transactional
@Override
public SubmitOrderResponseVo submitOrder(OrderSubmitVo orderSubmitVo) {
System.out.println("seata分布式事务id:"+RootContext.getXID());
//用于在同一线程共享数据,减少方法传参
confirmVoThreadLocal.set(orderSubmitVo);
SubmitOrderResponseVo responseVo=new SubmitOrderResponseVo();
responseVo.setCode(0);
MemberRespVo memberRespVo = LoginUserInterceptor.localUser.get();
String redisOrderKey=OrderConstants.USER_TOKEN_PREFIX + memberRespVo.getId();
String redisToken = stringredisTemplate.opsForValue().get(redisOrderKey);
//验证提交的令牌与后台令牌是否一致
//验证的逻辑【验证令牌和删除令牌要保证原子性】
String orderToken = orderSubmitVo.getOrderToken();
// 使用lua脚本解锁,保证原子性 0 验证失败 1 验证并删除成功
String script = "if redis.call('get',KEYS[1]) == ARGV[1] then return redis.call('del',KEYS[1]) else return 0 end";
//原子验证令牌,若成功则删除令牌,保证订单提交的幂等性
Long result = stringredisTemplate.execute(new DefaultRedisScript<Long>(script, Long.class), Arrays.asList(redisOrderKey), orderToken);
if(result==0L){
//验证失败,直接返回
responseVo.setCode(1);
return responseVo;
}else {
//令牌验证成功,执行创建订单等业务流程
OrderCreateTo order = createOrder();
//验价
//1.后台计算的价格
BigDecimal payPrice = order.getOrder().getPayAmount();
//2.前端提交的价格
BigDecimal payPrice1 = orderSubmitVo.getPayPrice();
//不用比较相等,只需前端和后台价格相差的绝对值小于0.01元即1分,即视为相等
if(Math.abs(payPrice.subtract(payPrice1).doubleValue())<0.01){
//3、创建订单
saveOrder(order);
//订单创建成功后立即锁定库存,防止缺货
WareSkuLockVo lockVo=new WareSkuLockVo();
lockVo.setOrderSn(order.getOrder().getOrderSn());
List<OrderItemVo> orderItemVos = order.getOrderItems().stream().map(item -> {
OrderItemVo orderItemVo = new OrderItemVo();
orderItemVo.setSkuId(item.getSkuId());
orderItemVo.setTitle(item.getSkuName());
orderItemVo.setCount(item.getSkuQuantity());
return orderItemVo;
}).collect(Collectors.toList());
lockVo.setLocks(orderItemVos);
//4、远程锁库存
R r = wmsFeignService.lockStock(lockVo);
if(r.getCode()==0){
//锁定库存成功
responseVo.setOrder(order.getOrder());
int i=10/0;
return responseVo;
}else{
//锁定库存失败
throw new NoStockException(1L);
// responseVo.setCode(3);
// return responseVo;
}
}else{
//价格不相等返回2
responseVo.setCode(2);
return responseVo;
}
}
}
上面的代码中,我在订单服务中保存订单的方法中添加了@GlobalTransactional注解,该服务远程调用了库存服务的wmsFeignService.lockStock(lockVo)的方法,实现当订单创建成功扣减库存,为了模拟发生异常实现全局事务回滚,在扣减库存处添加了以下代码:
int i=10/0;
当前在订单服务,由于我们添加了@Transactional注解,异常发生时触发订单服务数据回滚,而库存服务属于远程调用的服务,数据在另一个数据库中,此时实现库存服务事务回滚依靠的就是seata了,通过引入@GlobalTransactional来实现。来看控制台,实现了库存服务的数据回滚。
这里还有一个地方需要注意,只需要在当前服务的方法上添加@GlobalTransactional注解,远程调用的库存服务无需添加该注解,只需要添加@Transactional即可。
seata回滚成功后会将业务数据恢复到方法调用前的样子,同时seata数据库中的global_table等表格中的数据也会清除,如果想查看seata操作数据的流程,可在业务方法上添加断点调试,就可看到中途的数据演变,下面是global_table在方法执行时seata插入的一条数据:
注意:上面的seata使用的是AT模式保证全局事务的一致性,在高并发情景下可能并不适用。
本文参考自:Seata1.2.0安装配置Nacos注册配置中心,以及实际运行案例_seata 1.2.0-CSDN博客
向原博主表示感谢!