ChainedTransactionManager是Spring框架中的一个事务管理器实现,它可以将多个事务管理器组合在一起形成一个链式的事务管理器。
在实际应用中,可能需要在不同的数据源上进行事务管理。这时,可以使用多个数据源对应多个事务管理器,然后通过ChainedTransactionManager将它们组合成一个事务管理器,从而实现全局事务的管理。
ChainedTransactionManager的使用方式与普通的事务管理器类似,可以将其配置为Spring的事务管理器,然后通过@Transactional注解或编程式事务管理等方式来开启事务。
需要注意的是,ChainedTransactionManager只有在所有子事务管理器都支持嵌套事务时才能使用,否则会抛出异常。此外,ChainedTransactionManager的事务提交顺序与子事务管理器的顺序相反,即先提交最后一个子事务,最后提交第一个子事务。
从Spring Framework 5.3版本开始,Spring提供了一个新的事务管理器实现类CompositeTransactionManager,它可以像ChainedTransactionManager一样组合多个事务管理器,但具有更好的可扩展性和灵活性。因此,如果需要组合多个事务管理器时,可以考虑使用CompositeTransactionManager代替ChainedTransactionManager。
在 Spring 中操作多个数据源并保证事务的一致性,可以使用 Spring 的事务管理和多数据源配置。配置事务管理器:可以使用 Spring 的事务管理器来管理多个数据源的事务。例如,可以配置一个 ChainedTransactionManager 对象,将多个事务管理器(如 DataSourceTransactionManager)串联起来
一.application.properties配置数据源链接
spring.datasource.vw.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.vw.initialSize=10
spring.datasource.vw.maxActive=20
spring.datasource.vw.maxWait=60000
spring.datasource.vw.minIdle=1
spring.datasource.vw.password=
spring.datasource.vw.jdbc-url=jdbc:mysql://...:3306/y_water?useUnicode=true&characterEncoding=UTF-8
spring.datasource.vw.username=
mybatis.vw.mapper-locations=classpath*\:cc/eslink/villagewater/mapper/vw/*.xml
mybatis.vw.type-aliases-package=cc.eslink.villagewater.domain.entity
spring.datasource.bm.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.bm.initialSize=10
spring.datasource.bm.maxActive=20
spring.datasource.bm.maxWait=60000
spring.datasource.bm.minIdle=1
spring.datasource.bm.password=
spring.datasource.bm.jdbc-url=jdbc:mysql://...:3306/econdsupply?useUnicode=true&characterEncoding=UTF-8
spring.datasource.bm.username=
mybatis.bm.mapper-locations=classpath*\:cc/eslink/villagewater/mapper/bm/*.xml
mybatis.bm.type-aliases-package=cc.eslink.villagewater.domain.entity
二.编写MybatisConfig
BmMybatisConfig
package cc.eslink.villagewater.config;
import org.apache.ibatis.session.SqlSessionFactory;
import org.mybatis.spring.SqlSessionFactoryBean;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.jdbc.DataSourceBuilder;
import org.springframework.cloud.context.config.annotation.RefreshScope;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.annotation.EnableTransactionManagement;
import tk.mybatis.spring.annotation.MapperScan;
import javax.sql.DataSource;
/**
* 数据库配置
* BmMybatisConfig
*
* @author wxj
* @date 2023/1/13 10:31
**/
@Configuration
@MapperScan(basePackages = "cc.eslink.villagewater.dao.bm", sqlSessionFactoryRef = "bmSqlSessionFactory")
public class BmMybatisConfig extends BaseExtendMybatisConfig {
@Value("mybatis.bm.type-aliases-package")
private String typeAliasesPackageKey;
@Value("mybatis.bm.mapper-locations")
private String mapperLocationsKey;
@Bean("bmDataSource")
@ConfigurationProperties(prefix = "spring.datasource.bm")
@RefreshScope
@Override
public DataSource dataSource() {
return DataSourceBuilder.create().build();
}
@Bean("bmSqlSessionTemplate")
@Override
public JdbcTemplate jdbcTemplate(
@Qualifier("bmDataSource") DataSource dataSource) {
return new JdbcTemplate(dataSource);
}
@Bean("bmSqlSessionFactory")
@Override
public SqlSessionFactory sqlSessionFactory(@Qualifier("bmDataSource") DataSource dataSource) throws Exception {
return super.sqlSessionFactory(dataSource);
}
@Override
public String getMapperLocationsKey() {
return mapperLocationsKey;
}
@Override
public String getTypeAliasesPackageKey() {
return typeAliasesPackageKey;
}
}
VwMybatisConfig
package cc.eslink.villagewater.config;
import org.apache.ibatis.session.SqlSessionFactory;
import org.mybatis.spring.SqlSessionFactoryBean;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.jdbc.DataSourceBuilder;
import org.springframework.cloud.context.config.annotation.RefreshScope;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
import org.springframework.transaction.PlatformTransactionManager;
import tk.mybatis.spring.annotation.MapperScan;
import javax.sql.DataSource;
/**
* @author wxj
* @date 2023/1/13 10:31
**/
@Configuration
@MapperScan(basePackages = "cc.eslink.villagewater.dao.vw", sqlSessionFactoryRef = "vwSqlSessionFactory")
public class VwMybatisConfig extends BaseExtendMybatisConfig {
@Value("mybatis.vw.type-aliases-package")
private String typeAliasesPackageKey;
@Value("mybatis.vw.mapper-locations")
private String mapperLocationsKey;
@Bean("vwDataSource")
@ConfigurationProperties(prefix = "spring.datasource.vw")
@RefreshScope
@Override
public DataSource dataSource() {
return DataSourceBuilder.create().build();
}
@Bean("vwSqlSessionTemplate")
@Override
public JdbcTemplate jdbcTemplate(
@Qualifier("vwDataSource") DataSource dataSource) {
return new JdbcTemplate(dataSource);
}
@Bean("vwSqlSessionFactory")
@Override
public SqlSessionFactory sqlSessionFactory(@Qualifier("vwDataSource") DataSource dataSource) throws Exception {
return super.sqlSessionFactory(dataSource);
}
@Override
public String getMapperLocationsKey() {
return mapperLocationsKey;
}
@Override
public String getTypeAliasesPackageKey() {
return typeAliasesPackageKey;
}
}
BaseExtendMybatisConfig
package cc.eslink.villagewater.config;
import cc.eslink.util.BeanUtil;
import org.apache.ibatis.cache.Cache;
import org.apache.ibatis.io.VFS;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.session.Configuration;
import org.apache.ibatis.session.SqlSessionFactory;
import org.apache.ibatis.session.defaults.DefaultSqlSessionFactory;
import org.mybatis.spring.SqlSessionFactoryBean;
import org.mybatis.spring.boot.autoconfigure.SpringBootVFS;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.cloud.context.config.annotation.RefreshScope;
import org.springframework.core.env.Environment;
import org.springframework.core.io.Resource;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
import org.springframework.core.io.support.ResourcePatternResolver;
import org.springframework.core.type.classreading.CachingMetadataReaderFactory;
import org.springframework.core.type.classreading.MetadataReader;
import org.springframework.core.type.classreading.MetadataReaderFactory;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.util.ClassUtils;
import javax.sql.DataSource;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
/**
* 扩展数据源mybatis配置基类
* @author wxj
* @since 2023/01/13
*/
public abstract class BaseExtendMybatisConfig {
private final static Logger logger = LoggerFactory.getLogger(BaseExtendMybatisConfig.class);
static final String DEFAULT_RESOURCE_PATTERN = "/**/*.class";
@javax.annotation.Resource
private Environment env;
@RefreshScope
protected abstract DataSource dataSource();
protected abstract JdbcTemplate jdbcTemplate(DataSource dataSource);
/**
* 让type-aliases-package支持通配符
*
*/
protected static String setTypeAliasesPackage(String typeAliasesPackage) {
try {
ResourcePatternResolver resolver = new PathMatchingResourcePatternResolver();
MetadataReaderFactory metadataReaderFactory = new CachingMetadataReaderFactory(resolver);
typeAliasesPackage = ResourcePatternResolver.CLASSPATH_ALL_URL_PREFIX
+ ClassUtils.convertClassNameToResourcePath(typeAliasesPackage) + DEFAULT_RESOURCE_PATTERN;
List<String> result = new ArrayList<String>();
Resource[] resources = resolver.getResources(typeAliasesPackage);
if (resources != null && resources.length > 0) {
MetadataReader metadataReader = null;
for (Resource resource : resources) {
if (!resource.getURL().toString().contains("-facade-") && resource.isReadable()) {
metadataReader = metadataReaderFactory.getMetadataReader(resource);
result.add(
Class.forName(metadataReader.getClassMetadata().getClassName()).getPackage().getName());
}
}
}
if (result.size() > 0) {
HashSet<String> h = new HashSet<String>(result);
result.clear();
result.addAll(h);
typeAliasesPackage = String.join(",", result.toArray(new String[0]));
}
if (logger.isDebugEnabled()) {
logger.debug("扫描type-aliases-package结果: {}", typeAliasesPackage);
}
} catch (Exception e) {
logger.error("扫描type-aliases-package异常:", e);
}
return typeAliasesPackage;
}
protected abstract String getTypeAliasesPackageKey();
protected abstract String getMapperLocationsKey();
protected SqlSessionFactory sqlSessionFactory(DataSource dataSource) throws Exception {
// 改用SpringBootVFS,解决war包扫描bug
VFS.addImplClass(SpringBootVFS.class);
String typeAliasesPackage = env.getProperty(getTypeAliasesPackageKey());
String mapperLocations = env.getProperty(getMapperLocationsKey());
// 处理默认的typeAliasesPackage
String dealPackage = setTypeAliasesPackage(typeAliasesPackage);
final SqlSessionFactoryBean sessionFactory = new SqlSessionFactoryBean();
sessionFactory.setDataSource(dataSource);
sessionFactory.setTypeAliasesPackage(dealPackage);
sessionFactory.setMapperLocations(new PathMatchingResourcePatternResolver().getResources(mapperLocations));
DefaultSqlSessionFactory ssf = (DefaultSqlSessionFactory) sessionFactory.getObject();
assert ssf != null;
Configuration config = ssf.getConfiguration();
// 设置下划线转驼峰配置和默认的枚举处理
config.setMapUnderscoreToCamelCase(true);
config.setDefaultEnumTypeHandler(org.apache.ibatis.type.EnumOrdinalTypeHandler.class);
return ssf;
}
}
三、配置事务管理器
配置事务管理器:可以使用 Spring 的事务管理器来管理多个数据源的事务。例如,可以配置一个 ChainedTransactionManager 对象,将多个事务管理器(如 DataSourceTransactionManager)串联起来
其中,@EnableTransactionManagement 注解用于启用 Spring 的事务管理功能。
package cc.eslink.villagewater.config;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.transaction.ChainedTransactionManager;
import org.springframework.transaction.annotation.EnableTransactionManagement;
import org.springframework.context.annotation.Bean;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
import org.springframework.transaction.PlatformTransactionManager;
import javax.annotation.Resource;
import javax.sql.DataSource;
/**
* 配置事务管理器
*
* @Author : wxj
* @Date : 2023/3/9 10:59
*/
@Configuration
@EnableTransactionManagement
public class TransactionConfig {
@Resource
@Qualifier("vwDataSource")
private DataSource vwDataSource;
@Resource
@Qualifier("bmDataSource")
private DataSource bmDataSource;
@Bean
public PlatformTransactionManager transactionManager() {
DataSourceTransactionManager vwManager = new DataSourceTransactionManager(vwDataSource);
DataSourceTransactionManager bmManager = new DataSourceTransactionManager(bmDataSource);
return new ChainedTransactionManager(vwManager, bmManager);
}
}
四、dao和mapper目录结构
五.事务使用
@Transactional(rollbackFor = Exception.class)
@Override
public void dataReport(String ownership) {
List<VwDataAuditRecordDto> dataList = dataAuditRecordDao.queryNoReportData(ownership);
//插入审核数据
bmMonitorDataDao.batchInsert(dataList);
//插入站点信息,重复站点更新,新增站点插入
List<VwStation> stationList = vwStationDao.queryStationPageList(ownership,null,null,null,null,null);
bmStationDao.insertAndUpdate(stationList);
//更新上报状态为已上报
dataAuditRecordDao.updateReportFlag(dataList.stream().map(VwDataAuditRecordDto::getId).collect(Collectors.toList()));
}