springboot多数据源配置
思路
使用AOP的方式,在DAO层,指定数据源(即DataSource)
核心:通过继承spring提供的AbstractRoutingDataSource类,实现其determineCurrentLookupKey方法,可以动态切换数据源
代码实现
定义数据源
db1.url=jdbc:mysql://127.0.0.1:3306/db1?useUnicode=yes&characterEncoding=UTF-8&useSSL=false db1.username=root db1.password=1230 db1.jdbcDriver=com.mysql.jdbc.Driver db2.url=jdbc:mysql://127.0.0.1:3306/db2?useUnicode=yes&characterEncoding=UTF-8&useSSL=false db2.username=root db2.password=1230 db2.jdbcDriver=com.mysql.jdbc.Driver
定义数据源名称枚举
public enum DataSourceKey { db1, db2 }
通过使用ThreadLocal存储数据源key,保证每个线程获取各自的数据源,保证线程安全
public class DynamicDataSourceContextHolder { /** * 设置默认数据源的 key */ private static final ThreadLocal<String> contextHolder = ThreadLocal.withInitial(() -> "db1"); /** * 数据源的 key 集合,用于切换时判断数据源是否存在 */ public static List<Object> dataSourceKeys = new ArrayList<>(); /** * 转换数据源 * * @param key the key */ public static void setDataSourceKey(String key) { contextHolder.set(key); } /** * 获取当前数据源 * * @return data source key */ public static String getDataSourceKey() { return contextHolder.get(); } /** * 将数据源设置为默认数据源 */ public static void clearDataSourceKey() { contextHolder.remove(); } /** * 检查数据源是否在当前数据源列表 * * @param key the key * @return boolean boolean */ public static boolean containDataSourceKey(String key) { return dataSourceKeys.contains(key); } }
实现AbstractRoutingDataSource类
public class DynamicRoutingDataSource extends AbstractRoutingDataSource { private final Logger logger = LoggerFactory.getLogger(getClass()); /** * Set dynamic DataSource to Application Context * * @return */ @Override protected Object determineCurrentLookupKey() { logger.info("Current DataSource is [{}]", DynamicDataSourceContextHolder.getDataSourceKey()); return DynamicDataSourceContextHolder.getDataSourceKey(); } }
编写配置类 向spring容器注入数据源对象
@Configuration public class DataSourceConfigurer { @Bean(name = "db1") @Primary @ConfigurationProperties(prefix = "db1") public DataSource db1() { return DataSourceBuilder.create().build(); } @Bean(name = "db2") @ConfigurationProperties(prefix = "db2") public DataSource db2() { return DataSourceBuilder.create().build(); } /** * Dynamic data source. * * @return the data source */ @Bean(name="dynamicDataSource") public DataSource dynamicDataSource() { DynamicRoutingDataSource dynamicRoutingDataSource = new DynamicRoutingDataSource(); Map<Object, Object> dataSourceMap = new HashMap<>(2); dataSourceMap.put(DataSourceKey.db1.name(), db1()); dataSourceMap.put(DataSourceKey.db2.name(), db2()); dynamicRoutingDataSource.setDefaultTargetDataSource(db1()); dynamicRoutingDataSource.setTargetDataSources(dataSourceMap); DynamicDataSourceContextHolder.dataSourceKeys.addAll(dataSourceMap.keySet()); return dynamicRoutingDataSource; } @Bean @ConfigurationProperties(prefix = "mybatis") public SqlSessionFactoryBean sqlSessionFactoryBean() { SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean(); sqlSessionFactoryBean.setDataSource(dynamicDataSource()); return sqlSessionFactoryBean; } /** * 配置事务管理,如果使用到事务需要注入该 Bean,否则事务不会生效 * 在需要的地方加上 @Transactional 注解即可 * @return the platform transaction manager */ @Bean public PlatformTransactionManager transactionManager() { return new DataSourceTransactionManager(dynamicDataSource()); } }
编写注解类 用于放在DAO上 指明数据源的key
@Target({ElementType.METHOD, ElementType.TYPE}) @Retention(RetentionPolicy.RUNTIME) @Documented public @interface TargetDataSource { String value(); }
编写切面 用于拦截上面自定义的注解类 获取注解上指明的数据源的key 根据此key 将对应的数据源绑定到线程上去
@Aspect @Component public class DynamicDataSourceAspect { private static final Logger logger = LoggerFactory.getLogger(DynamicDataSourceAspect.class); /** * 切换数据源 * * @param point * @param targetDataSource */ @Before("@annotation(targetDataSource))") public void switchDataSource(JoinPoint point, TargetDataSource targetDataSource) { if (!DynamicDataSourceContextHolder.containDataSourceKey(targetDataSource.value())) { logger.error("DataSource [{}] doesn't exist, use default DataSource [{}]", targetDataSource.value()); } else { // 切换数据源 DynamicDataSourceContextHolder.setDataSourceKey(targetDataSource.value()); logger.info("Switch DataSource to [{}] in Method [{}]", DynamicDataSourceContextHolder.getDataSourceKey(), point.getSignature()); System.out.println("datasource:"+DynamicDataSourceContextHolder.getDataSourceKey()); } } /** * 重置为默认数据源 * * @param point * @param targetDataSource */ @After("@annotation(targetDataSource))") public void restoreDataSource(JoinPoint point, TargetDataSource targetDataSource) { // 将数据源置为默认数据源 DynamicDataSourceContextHolder.clearDataSourceKey(); logger.info("Restore DataSource to [{}] in Method [{}]", DynamicDataSourceContextHolder.getDataSourceKey(), point.getSignature()); } }
本实例源码:springboot多数据源配置
2018-5-23 补充遇到的一个问题
根据上面的配置,单线程下多数据源切换是可以正常工作的。。。
但是在多线程下会出现一个问题。。
情景如下:假如我们的系统有A、B两个数据源,默认数据源为A。。有一个用户请求我们的一个方法,此方法所绑定的数据源为B。在此方法里,需要创建多个线程去执行一个任务,此时,就会出现一个问题。。。即我们创建的线程使用的数据源是默认的数据源A,而不是此方法所绑定的数据源A。。
解析:因为我们是使用ThreadLocal变量为每个线程绑定多数据源,通过ThreadLocal保证每个线程之间切换数据源互不影响。。所以在方法中,我们显示创建的线程绑定的ThreadLocal都是使用默认的数据源,即A。
解决办法:在创建线程的时候,手动的为这个线程指定数据源。。。我是在线程的run方法处指定的。。代码如下:
@Override
public void run() {
//为每个线程指定数据源
DynamicDataSourceContextHolder.setDataSourceKey(DataSourceKey.duiluCore.name());
//TODO 实现你自己的业务
}