SpringBoot多数据源配置 MybatisPlus配置类
Druid 可以说是国内使用最广泛的数据源连接池产品
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>1.1.17</version>
</dependency>
application.yml配置
spring:
datasource:
pay:
username: root
password: root
host: 192.168.37.133:3306
jdbc-url: jdbc:mysql://${spring.datasource.pay.host}/${spring.datasource.pay.schema-name}?useUnicode=true&characterEncoding=utf8&useSSL=false
schema-name: dlsms
order:
username: root
password: root
host: 192.168.37.134:3306
jdbc-url: jdbc:mysql://${spring.datasource.order.host}/${spring.datasource.order.schema-name}?useUnicode=true&characterEncoding=utf8&useSSL=false
schema-name: dlsms
方案一:手动切换数据源
适合没有分模块的功能,后期又需要加入多数据源
MyBatis-Plus配置类
作用:配置数据源和事务管理
import net.test.project.constants.DataSourceKey;
import com.alibaba.druid.pool.DruidDataSource;
import com.baomidou.mybatisplus.core.MybatisConfiguration;
import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor;
import com.baomidou.mybatisplus.extension.spring.MybatisSqlSessionFactoryBean;
import net.test.project.constants.DataSourceKey;
import org.apache.ibatis.session.SqlSessionFactory;
import org.apache.ibatis.type.JdbcType;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.core.io.Resource;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
import org.springframework.transaction.PlatformTransactionManager;
import javax.sql.DataSource;
import java.util.HashMap;
import java.util.Map;
@MapperScan({"net.test.project.dao.mapper.*"})
@Configuration
public class DataSourceProxyConfig {
/**
* 配置默认数据源
*
* @return
*/
@Bean(name = "originOrder") // 声明其为Bean实例
@ConfigurationProperties(prefix = "spring.datasource.order")
public DataSource dataSourceOrder() {
return new DruidDataSource();
}
@Bean(name = "originStorage") // 声明其为Bean实例
@ConfigurationProperties(prefix = "spring.datasource.storage")
public DataSource dataSourceStorage() {
return new DruidDataSource();
}
@Bean(name = "originPay") // 声明其为Bean实例
@ConfigurationProperties(prefix = "spring.datasource.pay")
public DataSource dataSourcePay() {
return new DruidDataSource();
}
/**
* 设置多数据源,配置动态路由数据源
*
* @return
*/
@Bean("dynamicDataSource")
public DataSource dynamicDataSource(@Qualifier("originOrder") DataSource dataSourceOrder,
@Qualifier("originStorage") DataSource dataSourceStorage,
@Qualifier("originPay") DataSource dataSourcePay) {
Map<Object, Object> dataSourceMap = new HashMap<>();
dataSourceMap.put(DataSourceKey.ORDER.name(), dataSourceOrder);
dataSourceMap.put(DataSourceKey.STORAGE.name(), dataSourceStorage);
dataSourceMap.put(DataSourceKey.PAY.name(), dataSourcePay);
//设置默认
DynamicDataSource dynamicRoutingDataSource = new DynamicDataSource();
dynamicRoutingDataSource.setDefaultTargetDataSource(dataSourceOrder);
dynamicRoutingDataSource.setTargetDataSources(dataSourceMap);
return dynamicRoutingDataSource;
}
/**
* 设置会话工厂
*
* @return
* @throws Exception
*/
@Bean(name = "sqlSessionFactory")
@Primary
public SqlSessionFactory mybatisSqlSessionFactoryBean(
@Qualifier("dynamicDataSource") DataSource dataSource) throws Exception {
MybatisSqlSessionFactoryBean sqlSessionFactoryBean = new MybatisSqlSessionFactoryBean();
sqlSessionFactoryBean.setDataSource(dataSource);
// 设置别名扫描包(实体类所在包)
sqlSessionFactoryBean.setTypeAliasesPackage("com.test.custom.entity");
// 设置 Mapper XML 文件位置(与 application.yml 中配置一致)
PathMatchingResourcePatternResolver resolver = new PathMatchingResourcePatternResolver();
Resource[] mapperLocations = resolver.getResources("classpath*:mapper/*.xml");
sqlSessionFactoryBean.setMapperLocations(mapperLocations);
MybatisConfiguration configuration = new MybatisConfiguration();
configuration.setJdbcTypeForNull(JdbcType.NULL);
configuration.setMapUnderscoreToCamelCase(true);
configuration.setCacheEnabled(false);
sqlSessionFactoryBean.setConfiguration(configuration);
sqlSessionFactoryBean.setPlugins(mybatisPlusInterceptor());
return sqlSessionFactoryBean.getObject();
}
/**
* 添加分页插件(可选)
*/
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
interceptor.addInnerInterceptor(new PaginationInnerInterceptor());
return interceptor;
}
/**
* 默认事务管理器配置,管理 DynamicDataSource
*
* <p>用于普通 {@code @Transactional} 注解,保持向后兼容。
* 跨库事务请使用 {@code @MultiTransactional} 注解。
* 使用 ConditionalOnMissingBean 避免与其他配置冲突。
*
* @return PlatformTransactionManager(@Primary)
*/
@Bean
@Primary
@ConditionalOnMissingBean(name = "transactionManager")
public PlatformTransactionManager txManager(@Qualifier("dataSources") DataSource multipleDataSource) {
return new DataSourceTransactionManager(multipleDataSource);
}
/*
* ============================================================
* 以下为每个物理数据源创建独立的 TransactionManager
* 用于 @MultiTransactional 跨库事务管理
*
* 注意:
* 1. 这些 TransactionManager 直接绑定到物理 DataSource,而非 DynamicDataSource
* 2. 它们不被 @Primary 标注,默认事务管理仍由 txManager 处理
* 3. 它们被 MultiDataSourceTxManagerRegistry 集中管理和注入
* ============================================================
*/
@Bean("orderTransactionManager")
public PlatformTransactionManager orderTransactionManager(@Qualifier("originOrder") DataSource ds) {
return new DataSourceTransactionManager(ds);
}
@Bean("storageTransactionManager")
public PlatformTransactionManager storageTransactionManager(@Qualifier("originStorage") DataSource ds) {
return new DataSourceTransactionManager(ds);
}
@Bean("payTransactionManager")
public PlatformTransactionManager payTransactionManager(@Qualifier("originPay") DataSource ds) {
return new DataSourceTransactionManager(ds);
}
}
由于是MyBatsi-Plus,所以配的是MybatisSqlSessionFactoryBean,如果是MyBatis,则应该是SqlSessionFactoryBean
实现选择目标数据源
import org.springframework.jdbc.datasource.DataSourceUtils;
import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;
import javax.sql.DataSource;
import java.sql.Connection;
import java.sql.SQLException;
/**
* 动态数据源路由
*
* <p>基于 Spring {@link AbstractRoutingDataSource} 实现多数据源切换。
* 重写 {@link #getConnection()} 方法,在获取连接时通过
* {@link DataSourceUtils#getConnection(DataSource)} 获取,确保能够感知
* Spring 事务管理器(包括 {@code @MultiTransactional})已绑定的连接资源。
*
* <p>原 {@code AbstractRoutingDataSource.getConnection()} 直接调用
* {@link DataSource#getConnection()},会绕过
* {@link org.springframework.transaction.support.TransactionSynchronizationManager}
* 中已绑定的事务连接,导致 MyBatis 操作与事务管理器使用不同的物理连接,致使跨库事务失效。
*
* @author huangtao
* @since 2019/7/24
*/
public class DynamicDataSource extends AbstractRoutingDataSource {
/**
* 取得当前使用哪个数据源
*
* @return 当前线程绑定的数据源 lookup key
*/
@Override
protected Object determineCurrentLookupKey() {
log.info("当前数据源 [{}]", DynamicDataSourceContextHolder.getDataSourceType());
return DynamicDataSourceContextHolder.getDataSourceType();
}
/**
* 获取数据库连接
*
* <p>先解析当前线程对应的目标物理 DataSource,再通过
* {@link DataSourceUtils#getConnection(DataSource)} 获取连接。
* 这样当 {@code MultiTransactionManager} 已将目标 DataSource 的
* 连接绑定到 {@code TransactionSynchronizationManager} 时,
* 会直接复用该事务连接,而非从连接池获取新连接。
*
* @return 参与当前 Spring 事务的连接(如有),否则为连接池新连接
* @throws SQLException 获取连接失败时抛出
*/
@Override
public Connection getConnection() throws SQLException {
DataSource targetDataSource = determineTargetDataSource();
return DataSourceUtils.getConnection(targetDataSource);
}
}
数据源字典
public enum DataSourceKey {
ORDER("order", "order数据库"),
// 定义数据源类型为BASIC,表示基础数据库
STORAGE("storage", "storage数据库"),
// 定义数据源类型为BILL,表示账单计费数据库
PAY("pay", "计费数据库"),
;
// 数据源的类型标识
private String type;
// 数据源的名称
private String name;
/**
* 构造函数,初始化数据源类型和名称
*
* @param type 数据源的类型标识
* @param name 数据源的名称
*/
DataSourceKey(String type, String name) {
this.type = type;
this.name = name;
}
/**
* 获取数据源的类型标识
*
* @return 数据源的类型标识
*/
public String getType() {
return type;
}
/**
* 设置数据源的类型标识
*
* @param type 数据源的类型标识
*/
public void setType(String type) {
this.type = type;
}
/**
* 获取数据源的名称
*
* @return 数据源的名称
*/
public String getName() {
return name;
}
/**
* 设置数据源的名称
*
* @param name 数据源的名称
*/
public void setName(String name) {
this.name = name;
}
/**
* 根据类型标识获取对应的枚举值
*
* @param type 数据源的类型标识
* @return 对应类型标识的枚举值,如果找不到匹配的,则返回NULL
*/
public static DataSourceKey getEnumByType(String type) {
// 默认返回值为NULL,即无数据源
DataSourceKey e = NULL;
// 如果传入的类型不为空,则遍历所有枚举值寻找匹配的类型
if (type != null) {
for (DataSourceKey o : values()) {
// 如果找到匹配的类型,则更新返回值并终止遍历
if (o.getType().equals(type)) {
e = o;
break;
}
}
}
// 返回找到的枚举值
return e;
}
}
切换数据源
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class DynamicDataSourceContextHolder {
private static final ThreadLocal contextHolder = new ThreadLocal<>();
private static final ThreadLocal afterContextHolder = new ThreadLocal<>();
public static void setAfterDbType(DataSourceKey dbTypeEnum) {
afterContextHolder.set(dbTypeEnum.getType());
}
public static String getAfterDbType() {
return (String) afterContextHolder.get();
}
public static void clearAfterDbType() {
afterContextHolder.remove();
}
/**
* 设置数据源
*
* @param dbTypeEnum
*/
public static void setDataSourceType(DataSourceKey dbTypeEnum) {
log.info("切换到{}数据源", dbTypeEnum.getName());
contextHolder.set(dbTypeEnum.getType());
}
/**
* 取得当前数据源
*
* @return
*/
public static String getDataSourceType() {
return (String) contextHolder.get();
}
/**
* 清除上下文数据
*/
public static void clearDataSourceType() {
contextHolder.remove();
}
}
使用方法:查前选库
DynamicDataSourceContextHolder.setDataSourceType(DataSourceKey.PAY);
多数据源下的事务问题
多数据源下使用事务时,需要在注解上明确是哪个数据源,类似下面这样,否则会报找不到事务管理实例的错误。
@Transactional(value = "transactionManager",rollbackFor = Exception.class)
方案二:
自动数据源切换
组织两个配置类。原理:mybatis多数据源的原理是根据不同包,调用不同的数据源,你只需要把你的mapper.java和mapper.xml写在某个package中,springboot自动帮你实现数据源切换。注意第二个配置类不需要@Primary
- @MapperScan注解中的basePackages指向的是指定的Dao层。
- @MapperScan注解中sqlSessionFactoryRef 用来指定使用某个SqlSessionFactory来操作数据源。
- setMapperLocations指向的是操作执行数据库的Mapper层。
import com.alibaba.druid.pool.DruidDataSource;
import com.baomidou.mybatisplus.core.MybatisConfiguration;
import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor;
import com.baomidou.mybatisplus.extension.spring.MybatisSqlSessionFactoryBean;
import javax.sql.DataSource;
import org.apache.ibatis.session.SqlSessionFactory;
import org.apache.ibatis.type.JdbcType;
import org.mybatis.spring.SqlSessionTemplate;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
@Configuration
@MapperScan(basePackages = "com.example.dao.crm", sqlSessionTemplateRef = "crmSqlSessionTemplate")
public class CrmDataSourceConfig {
@Bean(name = "crmDataSource")
@ConfigurationProperties(prefix = "spring.datasource.crm")
public DataSource crmDataSourceConfig() {
return new DruidDataSource();
}
@Bean(name = "crmSqlSessionFactory")
@Primary
public SqlSessionFactory crmSqlSessionFactory(@Qualifier("crmDataSource") DataSource dataSource) throws Exception {
MybatisSqlSessionFactoryBean sqlSessionFactory = new MybatisSqlSessionFactoryBean();
sqlSessionFactory.setDataSource(dataSource);
sqlSessionFactory.setMapperLocations(new PathMatchingResourcePatternResolver().getResources("classpath*:mapper/crm/*.xml"));
MybatisConfiguration configuration = new MybatisConfiguration();
configuration.setJdbcTypeForNull(JdbcType.NULL);
configuration.setMapUnderscoreToCamelCase(true);
configuration.setCacheEnabled(false);
sqlSessionFactory.setConfiguration(configuration);
sqlSessionFactory.setPlugins(mybatisPlusInterceptor());
return sqlSessionFactory.getObject();
}
/**
* 添加分页插件(可选)
*/
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
interceptor.addInnerInterceptor(new PaginationInnerInterceptor());
return interceptor;
}
@Bean(name = "crmTransactionManager")
@Primary
public DataSourceTransactionManager crmTransactionManager(@Qualifier("crmDataSource") DataSource dataSource) {
return new DataSourceTransactionManager(dataSource);
}
@Bean(name = "crmSqlSessionTemplate")
@Primary
public SqlSessionTemplate crmSqlSessionTemplate(@Qualifier("crmSqlSessionFactory") SqlSessionFactory sqlSessionFactory) {
return new SqlSessionTemplate(sqlSessionFactory);
}
}
切面自动切换
通过切面的包路径配置实现
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.aspectj.lang.annotation.After;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
@Component
@Aspect
@Order(-10000) // 这是为了保证AOP在事务注解之前生效,Order的值越小,优先级越高
@Slf4j
public class DataSourceSwitchAspect {
@Pointcut("execution(* net.est.test.api.service.order..*.*(..))")
private void db3Aspect() {
}
@Pointcut("execution(* net.est.test.api.service.pay..*.*(..))\"")
private void db5Aspect() {
}
@Before("db3Aspect()")
public void rpt() {
final String afterDbType = DynamicDataSourceContextHolder.getDataSourceType();
final DataSourceKey afterDB = DataSourceKey.getEnumByType(afterDbType);
DynamicDataSourceContextHolder.setAfterDbType(afterDB);
log.debug("切换到{}数据源...", DataSourceKey.ORDER.name());
DynamicDataSourceContextHolder.setDataSourceType(DataSourceKey.ORDER);
}
@Before("db5Aspect()")
public void smsConfig() {
final String afterDbType = DynamicDataSourceContextHolder.getDataSourceType();
final DataSourceKey afterDB = DataSourceKey.getEnumByType(afterDbType);
DynamicDataSourceContextHolder.setAfterDbType(afterDB);
log.debug("切换到{}数据源...", DataSourceKey.PAY.name());
DynamicDataSourceContextHolder.setDataSourceType(DataSourceKey.PAY);
}
@After("db3Aspect()||db5Aspect()")
public void back() {
final String afterDbType = DynamicDataSourceContextHolder.getAfterDbType();
log.debug("切回到{}数据源...", afterDbType);
if (StringUtils.isEmpty(afterDbType)) {
DynamicDataSourceContextHolder.setDataSourceType(DataSourceKey.PAY);
} else {
final DataSourceKey afterByType = DataSourceKey.getEnumByType(afterDbType);
DynamicDataSourceContextHolder.setDataSourceType(afterByType);
}
}
}
定义了一个基于 AOP 的数据源切换切面类 DataSourceSwitchAspect,该切面用于实现动态数据源切换,支持在不同业务模块访问不同数据库。其功能如下:
定义切点:分别针对 order 和 pay 包下的方法定义切点 db3Aspect 和 db5Aspect。
前置通知:
- 在执行 order 包方法前,将数据源切换为 ORDER。
- 在执行 pay 包方法前,将数据源切换为 PAY。
后置通知:在目标方法执行结束后,将数据源恢复为之前的状态。
方案三:推荐使用@DS
在 pom.xml 中引入 dynamic-datasource-spring-boot-starter 依赖:
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>dynamic-datasource-spring-boot-starter</artifactId>
<version>3.5.1</version> //选择合适的版本
</dependency>
在 application.yml 中配置多个数据源。以下是一个示例配置:
spring:
datasource:
dynamic:
primary: master # 默认数据源
datasource:
master:
url: jdbc:mysql://localhost:3306/master_db?useSSL=false&serverTimezone=UTC
username: root
password: root
driver-class-name: com.mysql.cj.jdbc.Driver
slave:
url: jdbc:mysql://localhost:3306/slave_db?useSSL=false&serverTimezone=UTC
username: root
password: root
driver-class-name: com.mysql.cj.jdbc.Driver
数据源切换
dynamic-datasource-spring-boot-starter 提供了 @DS 注解,这个注解可以直接用于 Service 层的方法、Mapper 接口的方法,甚至整个 Mapper 类上。
import com.baomidou.dynamic.datasource.annotation.DS;
import org.springframework.stereotype.Service;
@Service
public class UserService {
@DS("master") // 使用 master 数据源
public void addUser(User user) {
// 插入用户数据
}
@DS("slave") // 使用 slave 数据源
public User getUserById(Long id) {
// 查询用户数据
return userMapper.selectById(id);
}
}
根据您提供的 application.yml 配置文件中 spring.datasource.dynamic.datasource 下的配置
@DS("master") 很可能对应的是master数据源。
它简化了配置和切换逻辑,通过引入依赖,配置数据源和使用@DS注解,可以轻松实现数据源的动态切换
常见问题
druid抛出异常:javax.management.InstanceAlreadyExistsException: com.alibaba.druid:type=DruidDataSource,id=xxx
同一个domain里面的MBean要求name唯一,检查SpringBoot应用的application.properties等配置文件name配置
实现跨数据库事务
@Transactional注解只能控制单个数据源内的事务。要实现跨数据库事务,可以考虑以下几种方案:
方案一:使用JTA + Atomikos
方案二:使用消息队列实现最终一致性
对于性能要求较高、可以接受最终一致性的场景,可以考虑使用消息队列实现:
- 在主数据库执行操作并提交事务
- 发送消息到消息队列
- 消费者从消息队列获取消息,在其他数据库执行相应操作
方案三:Seata AT 模式(分布式事务框架)
需要部署 Seata Server(新基础设施)
方案四:TCC模式
对于复杂业务场景,可以使用TCC模式:
- Try阶段:预留资源
- Confirm阶段:确认操作
- Cancel阶段:取消操作
方案五:自研分布式事物
- 优点:零新依赖,仅Spring原生能力。可配置化,高内聚低耦合
- 缺陷:MultiTransactionManager.commit() 按 CCOD → BMS → ACCOUNT 顺序提交,如果 BMS 提交失败,CCOD 已提交无法回滚,数据就永远不一致了
┌──────────────────────────────────────────────────┐
│ @MultiTransactional({CCOD, BMS, ACCOUNTCENTER}) │
│ │
│ MultiTransactionManager │
│ ├── CCOD TxManager → TransactionStatus 1 │
│ ├── BMS TxManager → TransactionStatus 2 │
│ └── ACCOUNT TxManager → TransactionStatus 3 │
│ │
│ 关键:AOP 拦截 → 开启多个独立事务 → 统一提交/回滚 │
│ 附加:支持配置化绑定(Service方法 → 数据源列表) │
└──────────────────────────────────────────────────┘
改造文件列表

MyBatis 所有 Mapper 操作都经过 DynamicDataSource,这是连接复用的前提。
sqlSessionFactoryBean.setDataSource(dataSource); // ← dataSource = DynamicDataSource 实例
DynamicDataSource.getConnection() 覆写让MyBatis与事务管理器使用同一个物理连接
MultiTransactional 注解
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* 跨数据库事务注解
*
* <p>用于声明一个方法需要在多个数据源上执行事务操作。
* 被标注的方法中的所有数据库操作将统一提交或回滚,
* 遵循"Best Efforts 1PC"模式(非XA,极端断电场景有风险)。
*
* <p>设计模式:声明式事务管理 + AOP切面拦截
*
* <p>使用示例:
* <pre>{@code
* @MultiTransactional({DataSourceKey.ORDER, DataSourceKey.STORAGE, DataSourceKey.PAY})
* public Object workConfigSave(BasicUserInfo basicUserInfo, AgentChangeNewDto agentChangeDto) {
* // 方法内部仍然使用 DynamicDataSourceContextHolder.setDataSourceType() 切换数据源
* // 事务由 MultiTransactionalAspect 统一管理提交和回滚
* }
* }</pre>
*
* <p>注意事项:
* <ul>
* <li>方法内部仍需通过 {@code DbContextHolder.setDbType()} 切换MyBatis数据源路由</li>
* <li>不支持嵌套调用(同一线程中重复开启多个MultiTransactional)</li>
* <li>各数据源事务独立,提交顺序为数组声明顺序,回滚顺序为逆序</li>
* <li>与 {@code @Transactional} 互斥,不要同时使用</li>
* </ul>
*
* @author team
* @since 1.0
*/
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface MultiTransactional {
/**
* 参与事务的数据源类型列表
*
* <p>事务开启顺序与该数组声明顺序一致,
* 提交顺序与声明顺序一致,回滚顺序为逆序。
*
* @return 数据源类型数组,至少包含一个数据源
*/
DataSourceKey[] value();
}
MultiTransactionContext 事物状态
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.TransactionStatus;
import java.util.LinkedHashMap;
import java.util.Map;
/**
* 跨库事务上下文
*
* <p>持有当前跨库事务中所有数据源的事务状态映射,
* 由 {@link MultiTransactionManager} 创建和管理。
*
* <p>线程安全:当前设计为单线程使用(跟随方法调用栈),不跨线程共享。
*
* @author team
* @since 1.0
*/
public class MultiTransactionContext {
/**
* 数据源类型 → (事务管理器, 事务状态) 的有序映射
*
* <p>使用 LinkedHashMap 保证开启顺序,回滚时使用逆序
*/
private final LinkedHashMap<DataSourceKey, TransactionEntry> transactionMap;
/**
* 构造事务上下文
*
* @param initialCapacity 预期参与事务的数据源数量,用于避免Map扩容
*/
MultiTransactionContext(int initialCapacity) {
this.transactionMap = new LinkedHashMap<>(initialCapacity);
}
/**
* 注册一个数据源的事务信息
*
* @param datasource 数据源类型
* @param txManager 该数据源的事务管理器
* @param txStatus 已开启的事务状态
*/
void register(DataSourceKey datasource, PlatformTransactionManager txManager, TransactionStatus txStatus) {
transactionMap.put(datasource, new TransactionEntry(txManager, txStatus));
}
/**
* 获取所有事务条目按开启顺序
*
* @return 事务条目Map(只读视图)
*/
Map<DataSourceKey, TransactionEntry> getTransactionMap() {
return transactionMap;
}
/**
* 获取已注册的数据源数量
*
* @return 数据源数量
*/
int size() {
return transactionMap.size();
}
/**
* 事务条目:封装事务管理器和事务状态
*/
static class TransactionEntry {
private final PlatformTransactionManager txManager;
private final TransactionStatus txStatus;
TransactionEntry(PlatformTransactionManager txManager, TransactionStatus txStatus) {
this.txManager = txManager;
this.txStatus = txStatus;
}
PlatformTransactionManager getTxManager() {
return txManager;
}
TransactionStatus getTxStatus() {
return txStatus;
}
}
}
MultiTransactionManager 生命周期管理
import org.springframework.stereotype.Component;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.TransactionStatus;
import org.springframework.transaction.support.DefaultTransactionDefinition;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
/**
* 跨数据库事务管理器
*
* <p>核心职责:
* <ol>
* <li>为多个数据源独立开启事务(每个数据源使用各自的 TransactionManager)</li>
* <li>统一提交所有事务(按声明顺序)</li>
* <li>统一回滚所有事务(按声明逆序)</li>
* </ol>
*
* <p>实现原理:利用 Spring 的 {@code TransactionSynchronizationManager} 为
* 不同 DataSource 绑定独立的 Connection 资源,实现同一线程中多个物理数据库
* 的独立事务管理。
*
* <p>事务模式:Best Efforts 1PC(非 XA 分布式事务)
* <ul>
* <li>优势:零额外依赖,性能好,适合绝大多数业务场景</li>
* <li>局限:极端宕机场景可能出现部分提交(概率极低)</li>
* </ul>
*
* <p>使用方式:通过 {@code @MultiTransactional} 注解声明,由 {@code MultiTransactionalAspect} 自动调用。
*
* @author team
* @since 1.0
*/
@Slf4j
@Component
public class MultiTransactionManager {
private final MultiDataSourceTxManagerRegistry registry;
private final ThreadLocal<MultiTransactionContext> currentContext = new ThreadLocal<>();
public MultiTransactionManager(MultiDataSourceTxManagerRegistry registry) {
this.registry = registry;
}
/**
* 为指定的数据源列表开启跨库事务
*
* <p>按数组声明顺序依次为每个数据源开启独立事务。
* 每个事务的传播行为为 PROPAGATION_REQUIRES_NEW,确保独立性。
*
* @param datasources 需要参与事务的数据源类型数组
* @return 跨库事务上下文,包含所有数据源的事务状态
* @throws IllegalStateException 如果当前线程已有未完成的跨库事务
*/
public MultiTransactionContext begin(DataSourceKey... datasources) {
if (currentContext.get() != null) {
throw new IllegalStateException("当前线程已存在未完成的跨库事务,不支持嵌套调用");
}
if (datasources == null || datasources.length == 0) {
throw new IllegalArgumentException("跨库事务至少需要指定一个数据源");
}
MultiTransactionContext context = new MultiTransactionContext(datasources.length);
Exception firstException = null;
int successCount = 0;
for (DataSourceKey datasource : datasources) {
try {
PlatformTransactionManager txManager = registry.getTransactionManager(datasource);
/*
* 切换 DynamicDataSourceContextHolder 确保 DataSourceTransactionManager
* 从正确的物理数据源获取连接
*/
DynamicDataSourceContextHolder.setDataSourceKey(datasource);
DefaultTransactionDefinition definition = new DefaultTransactionDefinition();
definition.setPropagationBehavior(DefaultTransactionDefinition.PROPAGATION_REQUIRES_NEW);
TransactionStatus txStatus = txManager.getTransaction(definition);
context.register(datasource, txManager, txStatus);
successCount++;
log.debug("跨库事务开启成功: datasource={}, 已开启 {}/{}", datasource.getType(), successCount, datasources.length);
} catch (Exception e) {
log.error("跨库事务开启失败: datasource={}, 共开启 {} 个后发生异常", datasource.getType(), successCount, e);
firstException = e;
break;
}
}
/*
* 如果部分事务开启失败,需要回滚已开启的事务
*/
if (firstException != null) {
rollbackPartially(context, successCount);
throw new RuntimeException("跨库事务开启失败,已回滚已开启的事务", firstException);
}
currentContext.set(context);
return context;
}
/**
* 提交当前跨库事务中的所有数据源事务
*
* <p>按注册顺序依次提交。如果某个数据源提交失败,
* 已提交的数据源无法回滚,需记录严重错误日志并触发告警。
*
* @param context 跨库事务上下文
*/
public void commit(MultiTransactionContext context) {
if (context == null) {
return;
}
ArrayList<DataSourceKey> committedList = new ArrayList<>();
for (Map.Entry<DataSourceKey, MultiTransactionContext.TransactionEntry> entry : context.getTransactionMap().entrySet()) {
DataSourceKey datasource = entry.getKey();
MultiTransactionContext.TransactionEntry txEntry = entry.getValue();
try {
txEntry.getTxManager().commit(txEntry.getTxStatus());
committedList.add(datasource);
log.debug("跨库事务提交成功: datasource={}", datasource.getType());
} catch (Exception e) {
/*
* 提交失败的严重处理:
* 已提交的数据源无法回滚,这是 Best Efforts 1PC 的固有局限。
* 需要记录完整的上下文信息,便于后续人工补偿。
*/
log.error("跨库事务提交失败! datasource={}, 已提交的数据源={}, 剩余未提交的数据源待人工检查", datasource.getType(), committedList.stream().map(DataSourceKey::getType).toList(), e);
throw new RuntimeException(String.format("跨库事务提交失败: datasource=%s, 已提交=%s", datasource.getType(), committedList.stream().map(DataSourceKey::getType).toList()), e);
}
}
}
/**
* 回滚当前跨库事务中的所有数据源事务
*
* <p>按注册顺序的逆序回滚。每个数据源回滚时出现异常不会阻止后续回滚,
* 但会记录错误日志。
*
* @param context 跨库事务上下文
*/
public void rollback(MultiTransactionContext context) {
if (context == null) {
return;
}
/*
* 转换为数组以便逆序遍历
*/
List<Map.Entry<DataSourceKey, MultiTransactionContext.TransactionEntry>> entries = new ArrayList<>(context.getTransactionMap().entrySet());
for (int i = entries.size() - 1; i >= 0; i--) {
Map.Entry<DataSourceKey, MultiTransactionContext.TransactionEntry> entry = entries.get(i);
DataSourceKey datasource = entry.getKey();
MultiTransactionContext.TransactionEntry txEntry = entry.getValue();
try {
txEntry.getTxManager().rollback(txEntry.getTxStatus());
log.debug("跨库事务回滚成功: datasource={}", datasource.getType());
} catch (Exception e) {
log.error("跨库事务回滚失败: datasource={}, 需人工检查该数据源状态", datasource.getType(), e);
}
}
}
/**
* 清理当前线程的跨库事务上下文
*
* <p>必须在事务提交或回滚后调用,防止 ThreadLocal 内存泄漏。
*
* @param context 跨库事务上下文
*/
public void cleanup(MultiTransactionContext context) {
if (context != null) {
currentContext.remove();
}
}
/**
* 获取当前线程的跨库事务上下文
*
* @return 当前上下文,如果不存在则返回 null
*/
MultiTransactionContext getCurrentContext() {
return currentContext.get();
}
/**
* 回滚已部分开启的事务(用于开启过程中发生异常的场景)
*
* @param context 事务上下文
* @param successCount 已成功开启的事务数量
*/
private void rollbackPartially(MultiTransactionContext context, int successCount) {
if (context == null || successCount <= 0) {
return;
}
List<Map.Entry<DataSourceKey, MultiTransactionContext.TransactionEntry>> entries = new ArrayList<>(context.getTransactionMap().entrySet());
for (int i = successCount - 1; i >= 0; i--) {
Map.Entry<DataSourceKey, MultiTransactionContext.TransactionEntry> entry = entries.get(i);
try {
entry.getValue().getTxManager().rollback(entry.getValue().getTxStatus());
log.debug("部分开启回滚成功: datasource={}", entry.getKey().getType());
} catch (Exception e) {
log.error("部分开启回滚失败: datasource={}", entry.getKey().getType(), e);
}
}
}
}
MultiDataSourceTxManagerRegistry 多数据源事务管理器注册中心
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.stereotype.Component;
import org.springframework.transaction.PlatformTransactionManager;
import java.util.EnumMap;
import java.util.Map;
/**
* 多数据源事务管理器注册中心
*
* <p>管理每个数据源对应的独立 {@link PlatformTransactionManager} 实例,
* 供 {@code MultiTransactionManager} 开启各数据源的独立事务。
*
* <p>设计模式:注册表模式(Registry Pattern),集中管理所有数据源事务管理器。
*
* <p>扩展指南:
* <ol>
* <li>在 {@link DataSourceProxyConfig} 中创建新的数据源事务管理器 Bean</li>
* <li>在本类构造函数中添加对应的注入参数</li>
* <li>在 {@link DataSourceKey} 枚举中确保有对应项</li>
* </ol>
*
* @author team
* @since 1.0
*/
@Slf4j
@Component
public class MultiDataSourceTxManagerRegistry {
/**
* 数据源类型 → 事务管理器映射
*
* <p>使用 EnumMap 因为键是枚举类型,性能优于 HashMap
*/
private final Map<DataSourceKey, PlatformTransactionManager> registry;
/**
* 构造注册中心,注入所有数据源对应的独立事务管理器
*
* <p>注意:每个数据源的事务管理器都是独立创建的,绑定到各自的物理 DataSource,
* 与 DataSourceProxyConfig 中的 DynamicDataSource 事务管理器互不影响。
*/
public MultiDataSourceTxManagerRegistry(@Qualifier("orderTransactionManager") PlatformTransactionManager orderTxManager, @Qualifier("storageTransactionManager") PlatformTransactionManager storageTxManager, @Qualifier("payTransactionManager") PlatformTransactionManager payTxManager) {
this.registry = new EnumMap<>(DataSourceKey.class);
registry.put(DataSourceKey.ORDER, orderTxManager);
registry.put(DataSourceKey.STORAGE, storageTxManager);
registry.put(DataSourceKey.PAY, payTxManager);
log.info("多数据源事务管理器注册中心初始化完成,已注册 {} 个数据源的事务管理器", registry.size());
}
/**
* 根据数据源类型获取对应的事务管理器
*
* @param DataSourceKey 数据源类型枚举
* @return 该数据源的事务管理器
* @throws IllegalArgumentException 如果指定数据源类型未注册
*/
public PlatformTransactionManager getTransactionManager(DataSourceKey DataSourceKey) {
PlatformTransactionManager txManager = registry.get(DataSourceKey);
if (txManager == null) {
throw new IllegalArgumentException(String.format("数据源 [%s] 的事务管理器未注册,请检查配置", DataSourceKey.getType()));
}
return txManager;
}
/**
* 检查指定数据源是否已注册
*
* @param DataSourceKey 数据源类型
* @return true 表示已注册
*/
public boolean contains(DataSourceKey DataSourceKey) {
return registry.containsKey(DataSourceKey);
}
}
开启分布式事物
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.aop.support.AopUtils;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
import java.lang.reflect.Method;
/**
* 跨库事务 AOP 切面
*
* <p>拦截标注了 {@link MultiTransactional} 注解的方法,自动管理
* 跨多个数据库的事务开启、提交和回滚。
*
* <p>执行流程:
* <ol>
* <li>解析 {@code @MultiTransactional} 注解获取参与事务的数据源列表</li>
* <li>调用 {@link MultiTransactionManager#begin(DataSourceKey...)} 开启所有事务</li>
* <li>执行目标方法(业务逻辑通过 {DynamicDataSourceContextHolder.setDataSourceType()} 切换数据源)</li>
* <li>方法正常返回 → 提交所有事务</li>
* <li>方法抛出异常 → 回滚所有事务</li>
* <li>清理 ThreadLocal 上下文</li>
* </ol>
*
* <p>优先级说明:{@code @Order(0)} 确保本切面在 {@code DataSourceSwitchAspect}(Order=-10000)
* 的 {@code @Before} 之后执行,保证数据源上下文已初始化后再开启事务。
*
* @author team
* @since 1.0
*/
@Slf4j
@Aspect
@Component
@Order(0)
public class MultiTransactionalAspect {
private final MultiTransactionManager multiTransactionManager;
public MultiTransactionalAspect(MultiTransactionManager multiTransactionManager) {
this.multiTransactionManager = multiTransactionManager;
}
/**
* 拦截 @MultiTransactional 注解的方法
*
* <p>使用 {@code @annotation} 切点表达式匹配方法上的注解,
* 同时支持类级别和方法级别的注解(方法级别优先)。
*
* @param joinPoint 切点,用于执行目标方法
* @return 目标方法的返回值
* @throws Throwable 目标方法抛出的异常
*/
@Around("@annotation(net.test.project.annotation.MultiTransactional)")
public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
Method interfaceMethod = signature.getMethod();
/*
* 使用 AopUtils.getMostSpecificMethod() 获取目标实现类上的具体方法,
* 兼容 JDK 动态代理(接口方法)和 CGLIB(实现类方法)两种代理模式。
* 直接使用 signature.getMethod() 在 JDK 代理下返回接口方法,
* 接口方法上没有 @MultiTransactional 注解,会导致注解查找失败、事务静默失效。
*/
Method method = AopUtils.getMostSpecificMethod(interfaceMethod, joinPoint.getTarget().getClass());
MultiTransactional annotation = method.getAnnotation(MultiTransactional.class);
if (annotation == null) {
/*
* 尝试从类级别获取注解
*/
annotation = joinPoint.getTarget().getClass().getAnnotation(MultiTransactional.class);
}
if (annotation == null) {
log.warn("MultiTransactionalAspect 未找到注解,直接执行方法: {}", method.getName());
return joinPoint.proceed();
}
DataSourceKey[] datasources = annotation.value();
if (datasources == null || datasources.length == 0) {
log.warn("@MultiTransactional 未指定数据源,直接执行方法: {}", method.getName());
return joinPoint.proceed();
}
String methodName = joinPoint.getTarget().getClass().getSimpleName() + "." + method.getName();
log.info("跨库事务开始: method={}, datasources={}", methodName, datasources);
MultiTransactionContext context = null;
try {
/*
* 阶段1: 为所有声明的数据源开启独立事务
* MultiTransactionManager.begin() 内部会切换 DynamicDataSourceContextHolder 为
* 每个数据源开启事务,确保 DataSourceTransactionManager 从正确的
* 物理数据源获取连接
*/
context = multiTransactionManager.begin(datasources);
/*
* 阶段2: 执行业务方法
* 业务方法内部通过 DynamicDataSourceContextHolder.setDataSourceType() 切换 MyBatis 路由,
* Spring 的 TransactionSynchronizationManager 会自动将各数据源的
* DB 操作绑定到对应的事务连接上
*/
Object result = joinPoint.proceed();
/*
* 阶段3: 业务成功,提交所有事务
*/
multiTransactionManager.commit(context);
log.info("跨库事务提交成功: method={}", methodName);
return result;
} catch (Throwable e) {
/*
* 阶段4: 业务失败,回滚所有事务
*/
log.error("跨库事务回滚: method={}, 异常信息={}", methodName, e.getMessage());
if (context != null) {
multiTransactionManager.rollback(context);
}
throw e;
} finally {
/*
* 阶段5: 清理 ThreadLocal,防止内存泄漏
*/
multiTransactionManager.cleanup(context);
}
}
}
DAMO开发者矩阵,由阿里巴巴达摩院和中国互联网协会联合发起,致力于探讨最前沿的技术趋势与应用成果,搭建高质量的交流与分享平台,推动技术创新与产业应用链接,围绕“人工智能与新型计算”构建开放共享的开发者生态。
更多推荐


所有评论(0)