若依框架中@DataSource注解实现多数据源动态切换的实战解析

张开发
2026/4/15 22:47:40 15 分钟阅读

分享文章

若依框架中@DataSource注解实现多数据源动态切换的实战解析
1. 为什么需要多数据源动态切换在实际开发中我们经常会遇到需要同时操作多个数据库的场景。比如电商系统中订单数据可能存储在MySQL主库而用户行为日志可能存放在从库又或者财务系统需要同时连接Oracle和MySQL两种不同类型的数据库。传统单数据源架构在这种情况下就显得力不从心了。若依框架提供的DataSource注解就是为了解决这类多数据源切换的痛点。我在实际项目中就遇到过这样的需求一个后台管理系统需要同时操作业务数据库和日志数据库如果每次操作都手动切换连接不仅代码臃肿还容易出错。而使用DataSource注解后只需要在方法或类上简单标注就能自动完成数据源切换大大提升了开发效率和代码可维护性。2. DataSource注解的核心原理2.1 注解定义解析先来看看DataSource注解的源码定义Target({ ElementType.METHOD, ElementType.TYPE }) Retention(RetentionPolicy.RUNTIME) Documented Inherited public interface DataSource { public DataSourceType value() default DataSourceType.MASTER; }这个定义有几个关键点需要注意注解可以用在方法和类上ElementType.METHOD和ElementType.TYPE运行时生效RetentionPolicy.RUNTIME默认使用主库DataSourceType.MASTER我在实际使用中发现这个设计非常灵活。你可以在类级别指定默认数据源然后在特定方法上覆盖这个设置。比如DataSource(DataSourceType.SLAVE) Service public class UserServiceImpl implements UserService { DataSource(DataSourceType.MASTER) public User getImportantUser(Long id) { // 这个方法会使用主库 } public ListUser listUsers() { // 这个方法会使用从库 } }2.2 数据源类型枚举若依框架内置了一个简单的数据源类型枚举public enum DataSourceType { MASTER, SLAVE }这个枚举值需要和配置文件中的数据源名称严格对应。我在项目中曾经踩过一个坑把枚举值写成了Master首字母大写结果导致切换失败。记住这里的枚举值必须全部大写与配置文件中的key完全一致。3. 多数据源配置实战3.1 基础配置在application.yml中配置多数据源spring: datasource: type: com.alibaba.druid.pool.DruidDataSource druid: master: url: jdbc:mysql://localhost:3306/main_db username: root password: 123456 slave: enabled: true url: jdbc:mysql://localhost:3307/log_db username: log_user password: 123456这里有几个注意事项主从库的配置要分开从库可以通过enabled开关控制是否启用建议为不同数据源使用不同的数据库用户方便权限管理3.2 Druid配置类DruidConfig配置类负责初始化数据源Configuration public class DruidConfig { Bean ConfigurationProperties(spring.datasource.druid.master) public DataSource masterDataSource(DruidProperties druidProperties) { return druidProperties.dataSource(DruidDataSourceBuilder.create().build()); } Bean ConfigurationProperties(spring.datasource.druid.slave) ConditionalOnProperty(prefix spring.datasource.druid.slave, name enabled, havingValue true) public DataSource slaveDataSource(DruidProperties druidProperties) { return druidProperties.dataSource(DruidDataSourceBuilder.create().build()); } Bean(name dynamicDataSource) Primary public DynamicDataSource dataSource(DataSource masterDataSource) { MapObject, Object targetDataSources new HashMap(); targetDataSources.put(DataSourceType.MASTER.name(), masterDataSource); setDataSource(targetDataSources, DataSourceType.SLAVE.name(), slaveDataSource); return new DynamicDataSource(masterDataSource, targetDataSources); } }这里特别要注意的是Primary注解它确保了当有多个DataSource bean时Spring会优先使用这个动态数据源。我曾经因为漏掉这个注解导致系统始终使用默认数据源排查了半天才发现问题。4. 动态切换的实现机制4.1 AOP切面处理DataSourceAspect是整个动态切换的核心Aspect Order(1) Component public class DataSourceAspect { Pointcut(annotation(com.ruoyi.common.annotation.DataSource) || within(com.ruoyi.common.annotation.DataSource)) public void dsPointCut() {} Around(dsPointCut()) public Object around(ProceedingJoinPoint point) throws Throwable { DataSource dataSource getDataSource(point); if (StringUtils.isNotNull(dataSource)) { DynamicDataSourceContextHolder.setDataSourceType(dataSource.value().name()); } try { return point.proceed(); } finally { DynamicDataSourceContextHolder.clearDataSourceType(); } } private DataSource getDataSource(ProceedingJoinPoint point) { MethodSignature signature (MethodSignature) point.getSignature(); DataSource dataSource AnnotationUtils.findAnnotation(signature.getMethod(), DataSource.class); if (Objects.nonNull(dataSource)) { return dataSource; } return AnnotationUtils.findAnnotation(signature.getDeclaringType(), DataSource.class); } }这个切面做了几件重要的事情拦截所有带有DataSource注解的方法或类从注解中获取要使用的数据源类型在执行方法前设置数据源方法执行完成后清理数据源设置4.2 线程本地变量存储DynamicDataSourceContextHolder使用ThreadLocal来保存当前线程的数据源类型public class DynamicDataSourceContextHolder { private static final ThreadLocalString CONTEXT_HOLDER new ThreadLocal(); public static void setDataSourceType(String dsType) { CONTEXT_HOLDER.set(dsType); } public static String getDataSourceType() { return CONTEXT_HOLDER.get(); } public static void clearDataSourceType() { CONTEXT_HOLDER.remove(); } }ThreadLocal是保证多线程环境下数据源隔离的关键。我在性能测试时发现如果不及时调用clearDataSourceType()可能会导致内存泄漏和错误的数据源继承。4.3 动态数据源路由DynamicDataSource继承自AbstractRoutingDataSource负责实际的数据源路由public class DynamicDataSource extends AbstractRoutingDataSource { public DynamicDataSource(DataSource defaultTargetDataSource, MapObject, Object targetDataSources) { super.setDefaultTargetDataSource(defaultTargetDataSource); super.setTargetDataSources(targetDataSources); super.afterPropertiesSet(); } Override protected Object determineCurrentLookupKey() { return DynamicDataSourceContextHolder.getDataSourceType(); } }这个实现非常简洁核心就是determineCurrentLookupKey()方法它从ThreadLocal中获取当前应该使用的数据源key。5. 实际应用中的注意事项5.1 事务管理问题在多数据源环境下事务管理需要特别注意。Spring的Transactional注解默认只对单个数据源有效。如果需要跨数据源事务就需要引入分布式事务解决方案如Seata。我曾经遇到过一个典型问题在一个事务方法中切换数据源结果发现所有操作都使用了第一个获取到的数据源。这是因为事务管理器在方法开始时就已经确定了数据源。解决方案有两种避免在事务方法中切换数据源使用支持多数据源的事务管理器5.2 性能考量动态数据源切换虽然方便但也有性能开销。根据我的测试每次切换大约会有1-3ms的额外开销。对于高频调用的方法建议尽量减少不必要的切换对性能敏感的方法固定使用某个数据源考虑使用连接池调优参数5.3 监控与排查多数据源环境下问题排查会更复杂。建议为每个数据源配置独立的监控在日志中记录数据源切换情况使用Druid的监控界面查看各数据源状态可以在DataSourceAspect中添加详细的日志记录Around(dsPointCut()) public Object around(ProceedingJoinPoint point) throws Throwable { String method point.getSignature().toShortString(); DataSource dataSource getDataSource(point); if (StringUtils.isNotNull(dataSource)) { logger.debug(准备切换数据源方法{}目标数据源{}, method, dataSource.value()); DynamicDataSourceContextHolder.setDataSourceType(dataSource.value().name()); } // ... }6. 扩展与定制6.1 支持更多数据源若依默认只支持MASTER和SLAVE两种数据源但我们可以轻松扩展在DataSourceType枚举中添加新类型在配置文件中添加对应的数据源配置在DruidConfig中添加新的Bean方法例如添加一个REPORT数据源public enum DataSourceType { MASTER, SLAVE, REPORT }然后在配置类中添加Bean ConfigurationProperties(spring.datasource.druid.report) public DataSource reportDataSource(DruidProperties druidProperties) { return druidProperties.dataSource(DruidDataSourceBuilder.create().build()); }6.2 自定义切换策略默认是基于注解的静态切换但有时我们需要更动态的策略。比如根据查询参数决定使用哪个数据源。这时可以扩展DataSourceAspectAround(dsPointCut()) public Object around(ProceedingJoinPoint point) throws Throwable { Object[] args point.getArgs(); if (args.length 0 args[0] instanceof QueryParam) { QueryParam param (QueryParam) args[0]; if (param.isNeedFreshData()) { DynamicDataSourceContextHolder.setDataSourceType(DataSourceType.MASTER.name()); } } // ... }6.3 与MyBatis整合在多数据源环境下MyBatis的mapper也需要特殊处理。通常的做法是为每个数据源创建独立的SqlSessionFactory使用MapperScan指定每个factory对应的mapper包在service中注入正确的mapper例如Configuration MapperScan(basePackages com.ruoyi.master.mapper, sqlSessionFactoryRef masterSqlSessionFactory) public class MasterMyBatisConfig { Bean public SqlSessionFactory masterSqlSessionFactory(Qualifier(masterDataSource) DataSource dataSource) throws Exception { SqlSessionFactoryBean sessionFactory new SqlSessionFactoryBean(); sessionFactory.setDataSource(dataSource); return sessionFactory.getObject(); } }这样就能确保不同的mapper使用不同的数据源。

更多文章