官网下载seata1.3Windows或者linu版本,本教程使用的是linux版,
1,下载1.3版本的tar.gz文件,网址为: https://github.com/seata/seata/releases
2, 下载源码:
3,nacos安装下载,这个不多做描述,自行百度或者查阅官网
下载地址https://github.com/alibaba/nacos/releases
4,创建seata数据库,本文用的库名为wy_seata,找到seata\seata-1.3.0\script\server\db,在该文件下选择与你数据库对应的sql,
我这里是mysql.直接导入mysql.sql文件即可.
5,所有需要参与全局事务的库增加表undo_log,在源码中seata\seata-1.3.0\script\client\at\db\mysql.sql
6,本文用了三个库,order:存储订单的表;storage:存储库存的表;account:存储账户信息的表。
7,初始化业务表,order表
CREATE TABLE `order` (
`id` bigint(11) NOT NULL AUTO_INCREMENT,
`user_id` bigint(11) DEFAULT NULL COMMENT '用户id',
`product_id` bigint(11) DEFAULT NULL COMMENT '产品id',
`count` int(11) DEFAULT NULL COMMENT '数量',
`money` decimal(11,0) DEFAULT NULL COMMENT '金额',
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=7 DEFAULT CHARSET=utf8;
ALTER TABLE `wy_pay`.`order` ADD COLUMN `status` int(1) DEFAULT NULL COMMENT '订单状态:0:创建中;1:已完结' AFTER `money` ;
storage表
CREATE TABLE `storage` (
`id` bigint(11) NOT NULL AUTO_INCREMENT,
`product_id` bigint(11) DEFAULT NULL COMMENT '产品id',
`total` int(11) DEFAULT NULL COMMENT '总库存',
`used` int(11) DEFAULT NULL COMMENT '已用库存',
`residue` int(11) DEFAULT NULL COMMENT '剩余库存',
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8;
INSERT INTO `wy_item`.`storage` (`id`, `product_id`, `total`, `used`, `residue`) VALUES ('1', '1', '100', '0', '100');
account表
CREATE TABLE `account` (
`id` bigint(11) NOT NULL AUTO_INCREMENT COMMENT 'id',
`user_id` bigint(11) DEFAULT NULL COMMENT '用户id',
`total` decimal(10,0) DEFAULT NULL COMMENT '总额度',
`used` decimal(10,0) DEFAULT NULL COMMENT '已用余额',
`residue` decimal(10,0) DEFAULT '0' COMMENT '剩余可用额度',
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8;
INSERT INTO `wy_account`.`account` (`id`, `user_id`, `total`, `used`, `residue`) VALUES ('1', '1', '1000', '0', '1000');
分别放在wy_pay库(order表)wy_item(storage表)和wy_account(account表)
8,制造一个分布式事务问题
我们创建三个服务,一个订单服务,一个库存服务,一个账户服务。当用户下单时,会在订单服务中创建一个订单,然后通过远程调用库存服务来扣减下单商品的库存,再通过远程调用账户服务来扣减用户账户里面的余额,最后在订单服务中修改订单状态为已完成。该操作跨越三个数据库,有两次远程调用,很明显会有分布式事务问题。
9,修改seata file.conf文件
## transaction log store, only used in seata-server
#这里手动加入service模块
service {
#transaction service group mapping
#修改,可不改,my_test_tx_group随便起名字。
vgroup_mapping.my_test_tx_group = "default"
#only support when registry.type=file, please don't set multiple addresses
# 此服务的地址
default.grouplist = "192.168.1.154:8091"
#disable seata
disableGlobalTransaction = false
}
store {
## store mode: file、db、redis
#这里修改为db
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 = "druid"
## mysql/oracle/postgresql/h2/oceanbase etc.
#这里修改为mysql
dbType = "mysql"
#如果你是mysql8.0以上,则修改这里的驱动,不是则不修改
driverClassName = "com.mysql.jdbc.Driver"
#这里修改为你现在的建立的seata数据库
url = "jdbc:mysql://。。。。。。:3306/wy_seata"
user = "。。。"
password = "。。。"
minConn = 5
maxConn = 30
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
queryLimit = 100
}
}
10,修改registry.conf
registry {
# file 、nacos 、eureka、redis、zk、consul、etcd3、sofa
type = "nacos"
#修改这里为nacos
nacos {
application = "seata-server"
serverAddr = "192.168.1.154:8848"
group = "DEV_GROUP"
namespace = "d5e6e80b-2ca7-4c28-a905-5ab98d44e05d"
cluster = "default"
username = "nacos"
password = "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
#修改这里为nacos
type = "nacos"
nacos {
serverAddr = "192.168.1.154:8848"
namespace = "d5e6e80b-2ca7-4c28-a905-5ab98d44e05d"
group = "DEV_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"
}
}
11,引入seata依赖
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-alibaba-seata</artifactId>
<version>2.2.0.RELEASE</version>
<exclusions>
<exclusion>
<groupId>io.seata</groupId>
<artifactId>seata-spring-boot-starter</artifactId>
</exclusion>
<exclusion>
<groupId>io.seata</groupId>
<artifactId>seata-all</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>io.seata</groupId>
<artifactId>seata-spring-boot-starter</artifactId>
<version>1.3.0</version>
</dependency>
<dependency>
<groupId>io.seata</groupId>
<artifactId>seata-all</artifactId>
<version>1.3.0</version>
</dependency>
12,配置properties文件(ymal可以自己去网页转换)
#seata配置
seata.enabled=true
seata.application-id=pay
#这里的名字与file.conf中vgroup_mapping.my_test_tx_group = "default"相同
seata.tx-service-group=my_test_tx_group
seata.enable-auto-data-source-proxy=true
#这里的名字与file.conf中vgroup_mapping.my_test_tx_group = "default"相同
seata.service.vgroup-mapping.my_test_tx_group=default
#这里的名字与file.conf中default.grouplist = "127.0.0.1:8091"相同
seata.service.grouplist.default=你的ip:8091
seata.config.type=nacos
seata.config.nacos.namespace=d5e6e80b-2ca7-4c28-a905-5ab98d44e05d
#这里的地址就是你的nacos的地址,可以更换为线上
seata.config.nacos.serverAddr=你的ip:8848
#这里的名字就是registry.conf中 nacos的group名字
seata.config.nacos.group=DEV_GROUP
seata.config.nacos.userName=nacos
seata.config.nacos.password=nacos
seata.registry.type=nacos
seata.registry.nacos.application=seata-server
#这里的地址就是你的nacos的地址,可以更换为线上
seata.registry.nacos.server-addr=你的ip:8848
#这里的名字就是registry.conf中 nacos的group名字
seata.registry.nacos.group=DEV_GROUP
#为nacos的命令空间的id
seata.registry.nacos.namespace=d5e6e80b-2ca7-4c28-a905-5ab98d44e05d
seata.registry.nacos.userName=nacos
seata.registry.nacos.password=nacos
logging.level.io.seata= info
[img=https://img-bbs.csdn.net/upload/202011/18/1605687439_972904.jpg][/img]13,修改源码中seata\seata-1.3.0\script\config-center\config.txt的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
#这里的名字与file.conf中vgroup_mapping.my_test_tx_group = "default"相同
service.vgroupMapping.my_test_tx_group=default
#这里的名字与file.conf中default.grouplist = "127.0.0.1:8091"相同
service.default.grouplist=你的ip: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
#这里修改为db
store.mode=db
store.file.dir=file_store/data
store.file.maxBranchSessionSize=16384
store.file.maxGlobalSessionSize=512
store.file.fileWriteBufferCacheSize=16384
store.file.flushDiskMode=async
store.file.sessionReloadReadSize=100
store.db.datasource=druid
store.db.dbType=mysql
store.db.driverClassName=com.mysql.jdbc.Driver
store.db.url=jdbc:mysql://。。。:3306/wy_seata?useUnicode=true
store.db.user=。。。
store.db.password=。。。
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
14,将修改的config.txt 推送至你的nacos
执行源码seata\seata-1.3.0\script\config-center\nacos\nacos-config.sh 这个脚本
sh nacos-config.sh -h 192.168.4.49
笔者发现有很多人出现推上去失败的情况,举例几种可能的情况
1:nacos是否启动。2:config.txt文件可以选择将#注释去掉3:报错找不到文件,是因为config.txt文件在nacos-config.sh脚本的上一层目录
4:假如nacos像笔者这样建立了不同的命名空间(public/dev/test)那么脚本文件里面的相关内容也需要同步一起改
tenant的值和namespaceId的值一样
具体的可以参考nacos的导入配置这个接口
此为上传成功之后的截图
成功后提示init nacos config finished, please start seata-server,去上图看导入到自己对应的空间
验证则相当简单,源码后续上传,先附几张图片
笔者发现可以打断点调试库存服务,这个时候发现会往undo_log插入日志,断点跳过之后会将表清空,wy_seata库的三张表也一样会清空
问题1:解决:笔者发现feign会出现seata事务回滚不了,特此解决
package com.spring.pay.config;
import io.seata.core.context.RootContext;
import io.seata.core.exception.TransactionException;
import io.seata.tm.api.GlobalTransaction;
import io.seata.tm.api.GlobalTransactionContext;
import org.apache.commons.lang3.StringUtils;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.AfterThrowing;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.reflect.MethodSignature;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
import java.lang.reflect.Method;
/**
* 类描述: 用于处理程序调用发生异常的时候由于异常被处理以后无法触发事务,而进行的处理,使之可以正常的触发事务。
*/
@Aspect
@Component
public class WorkAspect {
private final static Logger logger = LoggerFactory.getLogger(WorkAspect.class);
@Before("execution(* com.spring.pay.service.*.*(..))")
public void before(JoinPoint joinPoint) throws TransactionException {
MethodSignature signature = (MethodSignature)joinPoint.getSignature();
Method method = signature.getMethod();
logger.info("拦截到需要分布式事务的方法," + method.getName());
// 此处可用redis或者定时任务来获取一个key判断是否需要关闭分布式事务
GlobalTransaction tx = GlobalTransactionContext.getCurrentOrCreate();
tx.begin(300000, "test-client");
logger.info("创建分布式事务完毕" + tx.getXid());
}
@AfterThrowing(throwing = "e", pointcut = "execution(* com.spring.pay.service.*.*(..))")
public void doRecoveryActions(Throwable e) throws TransactionException {
logger.info("方法执行异常:{}", e.getMessage());
if (!StringUtils.isBlank(RootContext.getXID())) {
GlobalTransactionContext.reload(RootContext.getXID()).rollback();
}
}
}
将这个类放到项目的config目录下
除此之外笔者发现还是回滚不了,后面发现feign的降级的类也要处理
package com.spring.pay.service.impl;
import com.spring.base.entity.pay.CommonResult;
import com.spring.pay.config.WorkAspect;
import com.spring.pay.service.StorageService;
import feign.hystrix.FallbackFactory;
import io.seata.core.exception.TransactionException;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
@Slf4j
@Component
public class StorageFeignFallBack implements FallbackFactory<StorageService> {
@Autowired
WorkAspect workAspect;
@Override
public StorageService create(Throwable throwable) {
return new StorageService() {
@Override
public CommonResult decrease(Long productId, Integer count) {
try {
workAspect.doRecoveryActions(throwable);
log.error("我被服务降级了:{}", productId+":"+count, throwable);
return new CommonResult("我被服务降级了",500);
} catch (TransactionException e) {
e.printStackTrace();
}
return null;
}
};
}
}
这样就可以解决feign调用失败导致回滚不了的问题了(每个服务都要WorkAspect 这个类)
问题2:笔者发现其他异常事务回滚不了,例如1/0这种数学异常,直接解决方案
package com.spring.pay.config;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;
import javax.validation.ConstraintViolationException;
/**
* 类描述:全局异常捕获处理
*/
@ControllerAdvice
public class GlobalExceptionsHandler {
private Logger log = LoggerFactory.getLogger(GlobalExceptionsHandler.class);
/**
* 功能描述:全局异常处理
*
* @param e
* @return 返回处理结果
* @throws Exception
*/
@ExceptionHandler(value = Exception.class)
@ResponseBody
public Object errorHandler(Exception e) throws Exception {
// 此处为属性级的错误日志的处理
if (e instanceof ConstraintViolationException) {
log.info("绑定错误日志为:{}", e.getMessage());
return "请求数据格式错误";
// 此处为方法级别的错误日志处理
} else if (e instanceof MethodArgumentNotValidException) {
log.info("方法级的绑定错误日志为:{}", e.getMessage());
return "请求数据格式错误";
// 此处为全局错误日志的处理
} else {
log.info("错误日志为:{}", e.getMessage());
return "全局异常错误给捕获了!";
}
}
}
参考文章:https://blog.csdn.net/linzhefeng89/article/details/103726590
https://juejin.im/post/6844904001528397831