目录

背景

实现

Nacos配置 

Spring项目配置

Maven配置

Seata配置

数据源配置

HTTP工具header加事务ID

拦截器获取header中的事务ID 

全局异常捕获加事务回滚

配置文件

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,导致数据源切换的时候事务无法正常使用。

HTTP工具header加事务ID

// 例:使用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());
}

拦截器获取header中的事务ID 

<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.confregistry.conf两个配置文件复制到spring项目的resources目录下,这俩文件参考cloud版

使用

在service方法上加@GlobalTransactional注解

Logo

华为开发者空间,是为全球开发者打造的专属开发空间,汇聚了华为优质开发资源及工具,致力于让每一位开发者拥有一台云主机,基于华为根生态开发、创新。

更多推荐