目录
背景
实现
Nacos配置?
Spring项目配置
Maven配置
Seata配置
数据源配置
全局异常捕获加事务回滚
配置文件
Nacos配置文件
使用
背景
本文示例主要实现两个springmvc服务之间通过Http调用接口,实现两个服务的分布式事务,关键点在于调用方在http请求头中添加seata的TX_XID请求头,被调用方发生异常时手动回滚全局事务
seata环境部署、nacos环境部署
参考上一篇springcloud版本Springcloud+Druid+Mybatis+Seata+Nacos动态切换多数据源,分布式事务的实现_殷长庆的博客-CSDN博客_springcloud 动态数据源
实现
Nacos配置?
需要注意的地方:
client.undo.logSerialization=kryo
默认是jackson,jackson在seata1.4.2版本中对数据库表字段为datetime的类型进行序列化操作会报系列化异常错误,官方在seata1.5版本以后修改了这个错误,因为我之前搭建的是1.4版本,所以需要修改一下这个值
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
service.vgroupMapping.cloud-luckserver=default
service.vgroupMapping.cloud-luckother=default
service.default.grouplist=10.0.3.171: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.tableMetaCheckerInterval=60000
client.rm.sqlParserType=druid
client.rm.reportSuccessEnable=false
client.rm.sagaBranchRegisterEnable=false
client.tm.commitRetryCount=5
client.tm.rollbackRetryCount=5
client.tm.defaultGlobalTransactionTimeout=60000
client.tm.degradeCheck=false
client.tm.degradeCheckAllowTimes=10
client.tm.degradeCheckPeriod=2000
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.cj.jdbc.Driver
store.db.url=jdbc:mysql://10.0.3.164:3306/seata?useUnicode=true&rewriteBatchedStatements=true
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.mode=single
store.redis.single.host=127.0.0.1
store.redis.single.port=6379
store.redis.maxConn=10
store.redis.minConn=1
store.redis.maxTotal=100
store.redis.database=0
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=kryo
client.undo.onlyCareUpdateColumns=true
server.undo.logSaveDays=7
server.undo.logDeletePeriod=86400000
client.undo.logTable=undo_log
client.undo.compress.enable=true
client.undo.compress.type=zip
client.undo.compress.threshold=64k
log.exceptionRate=100
transport.serialization=seata
transport.compressor=none
metrics.enabled=false
metrics.registryType=compact
metrics.exporterList=prometheus
metrics.exporterPrometheusPort=9898
Spring项目配置
Maven配置
<dependency>
<groupId>io.seata</groupId>
<artifactId>seata-all</artifactId>
<version>1.4.2</version>
</dependency>
<dependency>
<groupId>io.seata</groupId>
<artifactId>seata-serializer-kryo</artifactId>
<version>1.4.2</version>
</dependency>
Seata配置
package com.luck.config.seata;
import static io.seata.common.Constants.BEAN_NAME_FAILURE_HANDLER;
import static io.seata.common.Constants.BEAN_NAME_SPRING_APPLICATION_CONTEXT_PROVIDER;
import static io.seata.spring.annotation.datasource.AutoDataSourceProxyRegistrar.BEAN_NAME_SEATA_AUTO_DATA_SOURCE_PROXY_CREATOR;
import static io.seata.spring.annotation.datasource.AutoDataSourceProxyRegistrar.BEAN_NAME_SEATA_DATA_SOURCE_BEAN_POST_PROCESSOR;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.DependsOn;
import com.luck.utils.Constant;
import io.seata.common.DefaultValues;
import io.seata.config.springcloud.SpringApplicationContextProvider;
import io.seata.spring.annotation.GlobalTransactionScanner;
import io.seata.spring.annotation.datasource.SeataAutoDataSourceProxyCreator;
import io.seata.spring.annotation.datasource.SeataDataSourceBeanPostProcessor;
import io.seata.tm.api.DefaultFailureHandlerImpl;
import io.seata.tm.api.FailureHandler;
@Configuration
public class SeataConfiguration {
@Bean(BEAN_NAME_FAILURE_HANDLER)
public FailureHandler failureHandler() {
return new DefaultFailureHandlerImpl();
}
@Bean(BEAN_NAME_SPRING_APPLICATION_CONTEXT_PROVIDER)
public SpringApplicationContextProvider springApplicationContextProvider() {
return new SpringApplicationContextProvider();
}
@Bean
@DependsOn({ BEAN_NAME_SPRING_APPLICATION_CONTEXT_PROVIDER, BEAN_NAME_FAILURE_HANDLER })
public GlobalTransactionScanner globalTransactionScanner(FailureHandler failureHandler) {
String applicationId = Constant.getProperty("seata.applicationId", "");
String txServiceGroup = Constant.getProperty("seata.txServiceGroup", "");
return new GlobalTransactionScanner(applicationId, txServiceGroup, failureHandler);
}
/**
* The bean seataDataSourceBeanPostProcessor.
*/
@Bean(BEAN_NAME_SEATA_DATA_SOURCE_BEAN_POST_PROCESSOR)
public SeataDataSourceBeanPostProcessor seataDataSourceBeanPostProcessor() {
return new SeataDataSourceBeanPostProcessor(new String[] {}, DefaultValues.DEFAULT_DATA_SOURCE_PROXY_MODE);
}
/**
* The bean seataAutoDataSourceProxyCreator.
*/
@Bean(BEAN_NAME_SEATA_AUTO_DATA_SOURCE_PROXY_CREATOR)
public SeataAutoDataSourceProxyCreator seataAutoDataSourceProxyCreator() {
return new SeataAutoDataSourceProxyCreator(false, new String[] {}, DefaultValues.DEFAULT_DATA_SOURCE_PROXY_MODE);
}
}
要确保这个包被spring扫描到,需要在spring的xml中加入
<context:component-scan base-package="com.luck.config.seata" />
数据源配置
我是使用xml方式配置的数据源,java配置可以参考springcloud版,数据源切换的AOP和注解等跟cloud版一致,分布式事务的原理也一致,都是不同的业务使用不同的mapper扫描,保证业务可以正常提交、回滚事务,下面是数据源、mybatis部分xml配置
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="
http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context.xsd">
<!-- 扫码mapper -->
<context:component-scan base-package="com.luck.**.mapper" />
<!-- 开启Seata配置 -->
<context:component-scan base-package="com.luck.config.seata" />
<!-- 开启注解配置 -->
<context:annotation-config />
<!-- 配置数据源 -->
<bean id="first" class="com.alibaba.druid.pool.DruidDataSource" init-method="init" destroy-method="close">
<property name="driverClassName" value="${jdbc.driverClass}" />
<property name="url" value="${jdbc.url}" />
<property name="username" value="${jdbc.user}" />
<property name="password" value="${jdbc.password}" />
<property name="initialSize" value="${jdbc.initialSize}" />
<property name="minIdle" value="${jdbc.minIdle}" />
<property name="maxActive" value="${jdbc.maxActive}" />
<property name="maxWait" value="${jdbc.maxWait}" />
<property name="timeBetweenEvictionRunsMillis" value="${jdbc.timeBetweenEvictionRunsMillis}" />
<property name="minEvictableIdleTimeMillis" value="${jdbc.minEvictableIdleTimeMillis}" />
<property name="validationQuery" value="${jdbc.validationQuery}" />
<property name="testWhileIdle" value="${jdbc.testWhileIdle}" />
<property name="testOnBorrow" value="${jdbc.testOnBorrow}" />
<property name="testOnReturn" value="${jdbc.testOnReturn}" />
<property name="removeAbandoned" value="${jdbc.removeAbandoned}" />
<property name="removeAbandonedTimeout" value="${jdbc.removeAbandonedTimeout}" />
<property name="filters" value="${jdbc.filters}" />
</bean>
<bean id="second" class="com.alibaba.druid.pool.DruidDataSource" init-method="init" destroy-method="close">
<property name="driverClassName" value="${jdbc.driverClass}" />
<property name="url" value="${jdbc.second.url}" />
<property name="username" value="${jdbc.second.user}" />
<property name="password" value="${jdbc.second.password}" />
<property name="initialSize" value="${jdbc.initialSize}" />
<property name="minIdle" value="${jdbc.minIdle}" />
<property name="maxActive" value="${jdbc.maxActive}" />
<property name="maxWait" value="${jdbc.maxWait}" />
<property name="timeBetweenEvictionRunsMillis" value="${jdbc.timeBetweenEvictionRunsMillis}" />
<property name="minEvictableIdleTimeMillis" value="${jdbc.minEvictableIdleTimeMillis}" />
<property name="validationQuery" value="${jdbc.validationQuery}" />
<property name="testWhileIdle" value="${jdbc.testWhileIdle}" />
<property name="testOnBorrow" value="${jdbc.testOnBorrow}" />
<property name="testOnReturn" value="${jdbc.testOnReturn}" />
<property name="removeAbandoned" value="${jdbc.removeAbandoned}" />
<property name="removeAbandonedTimeout" value="${jdbc.removeAbandonedTimeout}" />
<property name="filters" value="${jdbc.filters}" />
</bean>
<bean id="firstDSProxy" class="io.seata.rm.datasource.DataSourceProxy">
<constructor-arg index="0" ref="first" />
</bean>
<bean id="secondDSProxy" class="io.seata.rm.datasource.DataSourceProxy">
<constructor-arg index="0" ref="second" />
</bean>
<bean id="dataSource" class="com.luck.datasources.DynamicDataSource">
<property name="defaultTargetDataSource" ref="firstDSProxy"></property>
<property name="targetDataSources">
<map key-type ="java.lang.String">
<entry key="first" value-ref="firstDSProxy"></entry>
<entry key="second" value-ref="secondDSProxy"></entry>
</map>
</property>
</bean>
<bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
<property name="dataSource" ref="firstDSProxy" />
<property name="configurationProperties">
<props>
<prop key="logImpl">STDOUT_LOGGING</prop>
<prop key="mapUnderscoreToCamelCase">true</prop>
</props>
</property>
<property name="mapperLocations">
<array>
<value>classpath*:mybatis/**/mapper/*.xml</value>
<value>classpath*:app/**/mapper/*.xml</value>
</array>
</property>
<property name="typeAliasesPackage" value="com.luck.**.domain" />
</bean>
<bean id="secondsqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
<property name="dataSource" ref="secondDSProxy" />
<property name="configurationProperties">
<props>
<prop key="logImpl">STDOUT_LOGGING</prop>
<prop key="mapUnderscoreToCamelCase">true</prop>
</props>
</property>
<property name="mapperLocations">
<array>
<value>classpath*:mybatis/**/mapper/*.xml</value>
<value>classpath*:app/**/mapper/*.xml</value>
</array>
</property>
<property name="typeAliasesPackage" value="com.luck.**.domain" />
</bean>
<bean id="firstMSConfigurer" class="tk.mybatis.spring.mapper.MapperScannerConfigurer">
<property name="sqlSessionFactoryBeanName" value="sqlSessionFactory"></property>
<property name="basePackage" value="com.luck.business1**.mapper" />
<property name="markerInterface" value="com.luck.app.base.mapper.BaseMapper" />
</bean>
<bean id="secondMSConfigurer" class="tk.mybatis.spring.mapper.MapperScannerConfigurer">
<property name="sqlSessionFactoryBeanName" value="secondsqlSessionFactory"></property>
<property name="basePackage" value="com.luck.business2.**.mapper" />
<property name="markerInterface" value="com.luck.app.base.mapper.BaseMapper" />
</bean>
<bean id="sqlSession" class="org.mybatis.spring.SqlSessionTemplate" scope="prototype">
<constructor-arg index="0" ref="sqlSessionFactory" />
</bean>
<bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">
<property name="dataSource" ref="dataSource"></property>
</bean>
</beans>
手动配置两个DataSourceProxy,因为发现seata没有主动创建second的proxy,导致数据源切换的时候事务无法正常使用。
// 例:使用hutool
HttpRequest request = HttpRequest.get("url");
try {
String xid = RootContext.getXID();
if (StringUtils.isNoneBlank(xid)) {
request.header(RootContext.KEY_XID, xid);
}
} catch (Exception e) {
LOGGER.error("request 添加 TX_XID 失败【{}】", e.getMessage());
}
<mvc:interceptors>
<!-- 配置Seata拦截器,注入xid -->
<mvc:interceptor>
<mvc:mapping path="/**" />
<bean class="io.seata.integration.http.TransactionPropagationInterceptor" />
</mvc:interceptor>
<!-- 配置其他拦截器 -->
<!-- ... --》
</mvc:interceptors>
?全局异常捕获加事务回滚
package com.luck.interceptor;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.apache.commons.lang3.StringUtils;
import org.springframework.web.servlet.handler.HandlerInterceptorAdapter;
import io.seata.core.context.RootContext;
import io.seata.tm.api.GlobalTransactionContext;
public class ExceptionInterceptor extends HandlerInterceptorAdapter {
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
// 事务回滚
if (!StringUtils.isBlank(RootContext.getXID())) {
GlobalTransactionContext.reload(RootContext.getXID()).rollback();
}
// do something
}
}
配置文件
# jdbc ....
# ....
# seata
seata.applicationId=cloud-luckserver
seata.txServiceGroup=cloud-luckserver
Nacos配置文件
把服务端的file.conf和registry.conf两个配置文件复制到spring项目的resources目录下,这俩文件参考cloud版
使用
在service方法上加@GlobalTransactional注解
|