Spring Boot整合Mybatis配置多数据源
- 前言
- 一、固定数据源配置
- 二、动态数据源
- 搞定收工!
前言
工作中难免需要做几个工具方便自己偷懒,加上之前的挡板,数据源肯定没法单一配置,所以需要多数据源配置。这里介绍两种配置:动态数据源和固定数据源模式。这两种我在目前的工作的工具开发中都有用到。
一、固定数据源配置
Mybatis是提供这种固定的多数据源配置的,需要分别配置包扫描(一般是不同的数据源扫描不同的包),事务处理器等。
yml 配置,主要是不要用Spring Boot 自带的数据库配置,spring.datasource ,或者其他数据源配置,改用自己的,这样其实Spring boot 的数据库自动配置DataSourceAutoConfiguration 其实是失效了的。
## 这个是第一个固定配置 spring: datasource: druid: db-type: com.alibaba.druid.pool.DruidDataSource driver-class-name: com.ibm.db2.jcc.DB2Driver url: jdbc:db2://***** username: **** password: **** initial-size: 1 min-idle: 1 max-active: 1 max-wait: 5000 time-between-eviction-runs-millis: 60000 min-evictable-idle-time-millis: 300000 max-evictable-idle-time-millis: 900000 connection-error-retry-attempts: 1 break-after-acquire-failure: true ## 这是第二个动态数据源配置 app: datasource: mapDatasource: TESTDATASOURCE1: db-type: com.alibaba.druid.pool.DruidDataSource driver-class-name: com.microsoft.sqlserver.jdbc.SQLServerDriver url: jdbc:sqlserver://**** username: **** password: **** initial-size: 5 min-idle: 5 max-active: 10 max-wait: 5000 time-between-eviction-runs-millis: 60000 min-evictable-idle-time-millis: 300000 max-evictable-idle-time-millis: 900000 connection-error-retry-attempts: 1 break-after-acquire-failure: true TESTDATASOURCE2: db-type: com.alibaba.druid.pool.DruidDataSource driver-class-name: com.microsoft.sqlserver.jdbc.SQLServerDriver url: jdbc:sqlserver://**** username: **** password: **** initial-size: 5 min-idle: 5 max-active: 10 max-wait: 5000 time-between-eviction-runs-millis: 60000 min-evictable-idle-time-millis: 300000 max-evictable-idle-time-millis: 900000 connection-error-retry-attempts: 1 break-after-acquire-failure: true TESTDATASOURCE3: db-type: com.alibaba.druid.pool.DruidDataSource driver-class-name: com.ibm.db2.jcc.DB2Driver url: jdbc:db2://**** username: **** password: **** initial-size: 5 min-idle: 5 max-active: 10 max-wait: 5000 time-between-eviction-runs-millis: 60000 min-evictable-idle-time-millis: 300000 max-evictable-idle-time-millis: 900000 connection-error-retry-attempts: 1 break-after-acquire-failure: true TESTDATASOURCE4: db-type: com.alibaba.druid.pool.DruidDataSource driver-class-name: com.ibm.db2.jcc.DB2Driver url: jdbc:db2://**** username: **** password: **** initial-size: 5 min-idle: 5 max-active: 10 max-wait: 5000 time-between-eviction-runs-millis: 60000 min-evictable-idle-time-millis: 300000 max-evictable-idle-time-millis: 900000 connection-error-retry-attempts: 1 break-after-acquire-failure: true
Spring Boot 配置类
@Data @ConfigurationProperties(prefix = "app.datasource") public class SystemDynamicDatasourceProperties { private Map<String, DruidDataSource> mapDatasource; }
mybatis 固定数据源配置
@Configuration public class DataSourceConfiguration { @Configuration @MapperScan(basePackages = "com.test.mapper.datasource1", sqlSessionTemplateRef = "source1SqlSessionTemplate") public static class source1DatasourceConfiguration { @Bean(name = "source1DataSource") @ConfigurationProperties(prefix = "spring.datasource.druid") public DruidDataSource source1DataSource(){ return new DruidDataSource(); } @Bean(name = "source1TransactionManager") public DataSourceTransactionManager source1TransactionManager(@Qualifier("source1DataSource") DataSource dataSource) { return new DataSourceTransactionManager(dataSource); } @Bean(name = "source1SqlSessionFactory") public SqlSessionFactory source1SqlSessionFactory(@Qualifier("source1DataSource") DataSource dataSource) throws Exception { SqlSessionFactoryBean bean = new SqlSessionFactoryBean(); bean.setDataSource(dataSource); bean.setMapperLocations(new PathMatchingResourcePatternResolver().getResources("classpath:mapper/source1/*.xml")); bean.setTypeAliasesPackage("com.source1.entity"); return bean.getObject(); } @Bean(name = "source1SqlSessionTemplate") public SqlSessionTemplate source1SqlSessionTemplate(@Qualifier("source1SqlSessionFactory") SqlSessionFactory sqlSessionFactory) { return new SqlSessionTemplate(sqlSessionFactory); } } @Configuration @EnableConfigurationProperties(SystemDynamicDatasourceProperties.class) @MapperScan(basePackages = "com.source1.source1web.mapper.other", sqlSessionTemplateRef = "otherSqlSessionTemplate") public static class DynamicDatasourceConfiguration { @Resource private SystemDynamicDatasourceProperties systemDynamicDatasourceProperties; @Bean(name = "otherDataSource") public SystemDynamicDatasource otherDataSource(){ HashMap<Object, Object> map = new HashMap<>(systemDynamicDatasourceProperties.getMapDatasource()); SystemDynamicDatasource systemDynamicDatasource = new SystemDynamicDatasource(map); return systemDynamicDatasource; } @Bean(name = "otherTransactionManager") public DataSourceTransactionManager otherTransactionManager(@Qualifier("otherDataSource") DataSource dataSource) { return new DataSourceTransactionManager(dataSource); } @Bean(name = "otherSqlSessionFactory") public SqlSessionFactory otherSqlSessionFactory(@Qualifier("otherDataSource") DataSource dataSource) throws Exception { SqlSessionFactoryBean bean = new SqlSessionFactoryBean(); bean.setDataSource(dataSource); bean.setMapperLocations(new PathMatchingResourcePatternResolver().getResources("classpath:mapper/other/*.xml")); bean.setTypeAliasesPackage("com.source1.source1web.entity"); return bean.getObject(); } @Bean(name = "otherSqlSessionTemplate") public SqlSessionTemplate otherSqlSessionTemplate(@Qualifier("otherSqlSessionFactory") SqlSessionFactory sqlSessionFactory) { return new SqlSessionTemplate(sqlSessionFactory); } } }
- 说明
这两种其实就已经是两种数据源的配置了,当使用com.test.mapper.datasource1 包下的Mapper 的时候,使用的是就Datasource1的数据源,包括事务管理器,当使用com.source1.source1web.mapper.other 包下的Mapper 的时候,就是第二种数据源。
但是,这种只适合于单数据库操作的事务,多数据库的事务属于分布式事务,不适于此,当一个数据库事务提交成功之后,另一个事务失败的话,无法回滚第一个。因为此项目只适用于查询和单数据库的插入,失败不做回滚。
二、动态数据源
其实就是在上面的基础上,上面已经配置好了数据源,和动态的配置,但是漏掉了一些配置的细节,就是动态数据源,其实
Mybatis 提供了动态数据源的抽象类AbstractRoutingDataSource ,我们只需要继承这个类并重写determineCurrentLookupKey 方法,找到相关的数据源即可。在这个配置里无论添加多少数据源都可以,动态添加也是可以的。
- 动态数据源配置
public class SystemDynamicDatasource extends AbstractRoutingDataSource { private Map<Object,Object> dataSourceMap; public static final ThreadLocal<String> DATA_SOURCE = new ThreadLocal<>(); public SystemDynamicDatasource(Map<Object, Object> dataSourceMap){ this.dataSourceMap = dataSourceMap; super.setTargetDataSources(dataSourceMap); super.afterPropertiesSet(); } public void setDataSource(Integer key, DataSource dataSource){ DruidDataSource oldDataSource = (DruidDataSource) dataSourceMap.put(key, dataSource); if (oldDataSource != null) { oldDataSource.close(); } afterPropertiesSet(); } public void removeDataSource(String key){ DruidDataSource oldDataSource = (DruidDataSource) dataSourceMap.remove(key); if (oldDataSource != null) { oldDataSource.close(); } afterPropertiesSet(); } public boolean isExist(String key){ return dataSourceMap.get(key) != null; } @Override protected Object determineCurrentLookupKey() { return DATA_SOURCE.get(); } public void setDataSource(String dataSource){ DATA_SOURCE.set(dataSource); } public static void removeDataSource(){ DATA_SOURCE.remove(); } }
说明:
- 线上使用进入多线程环境,其实主要区别就是需要确定当前线程使用的是哪个数据源。
Map 里面存储的就是多数据源,其中key 是每个数据源的key ,当某个线程需要确定使用哪个数据源的时候,就是靠这个key 来进行区分的。ThreadLocal 就是确定某个线程使用的是哪个key,这样保证了线程安全,不会相互影响,只要使用的时候注意remove即可。determineCurrentLookupKey 调用来决定哪个数据源。
-
AOP配置
这里最好使用AOP进行统一配置,不要在代码里写,在代码里写既添加了大量重复代码,而且与业务相关,代码可读性差,最好做成AOP统一配置。
代码如下:
- 注解
@Target({ElementType.TYPE, ElementType.METHOD}) public @interface OtherDatasource { String value() default ""; }
可以通过这个注解来充当切点,但是本次使用仅作为额外数据,获取指定的数据源使用。
- 切面
@Aspect @Component @Slf4j public class OtherDataSourceAspect { @Autowired private SystemDynamicDatasource systemDynamicDatasource; // @Pointcut("@annotation(com.ibank.im.app.aop.cache.annotation.SystemCacheable)") @Pointcut("execution(public * com.test.mapper.other.*.*(..))") public void pointcut(){} @Around("pointcut()") public Object systemCacheableAround(ProceedingJoinPoint joinPoint) throws Throwable { Class<?> targetCls=joinPoint.getTarget().getClass(); OtherDatasource annotation = targetCls.getAnnotation(OtherDatasource.class); String datasource = null; if (Objects.isNull(annotation) || !StringUtils.hasText(datasource = annotation.value())) { MethodSignature methodSignature=(MethodSignature)joinPoint.getSignature(); Method targetMethod= targetCls.getDeclaredMethod( methodSignature.getName(), methodSignature.getParameterTypes()); OtherDatasource methodAnnotation = targetMethod.getAnnotation(OtherDatasource.class); if (Objects.isNull(methodAnnotation) || !StringUtils.hasText(datasource = methodAnnotation.value())) { Object[] args = joinPoint.getArgs(); if (Arrays.isNullOrEmpty(args)) { throw new IllegalArgumentException("must have 1 param"); } if (!(args[0] instanceof String)) { throw new IllegalArgumentException("the first param must be databaseEnv"); } datasource = (String) args[0]; } } if (!systemDynamicDatasource.isExist(datasource)) { throw new IllegalArgumentException("databaseEnv does not exist"); } try{ systemDynamicDatasource.setDataSource(datasource); return joinPoint.proceed(); }finally { systemDynamicDatasource.removeDataSource(datasource); } } }
- 注解
说明:
- 本次数据源直接在
Mapper 层使用,不在Service 层使用,因为一个Service 可能要使用多个不同的数据源操作,比较麻烦,直接作用在Mapper 层比较合适。- 逻辑上,先判断这个类上有没有注解,有的话使用这个注解,如果没有在使用方法上的注解,方法上如果没有注解,就是用第一个
String 参数,在没有就会报错,在判断是否存在这个数据源。不存在直接报错。- 使用的时候,一定要用try包裹,使用完成
必须remove 掉当前的值,无论是否发生异常。不移除的话容易发生内存溢出 等问题。- 切面执行方法就是
ProceedingJoinPoint 类的proceed 方法,但是实际上这个方法有两个重载的函数,一个带参数一个不带参数,这里简要介绍一下:
- 不带参数的:表示调用时传递什么参数,就是什么参数,Advice不干预,原样传递。因为本次过程不修改什么参数。所以使用的是这个
- 带参数的:自然就是相反的,将替换掉调用时传递的参数,这时候方法里调用的就是切面里的参数。
因为目前业务需求问题,都是使用的参数进行传递,所以只能定义在方法参数上。像这个样子:
@Mapper public interface TestMapper { int insertTest(String env, Entity entity); }
第一个参数就决定是哪个数据源,但是实际上业务并不采用。因为无法固定使用某个数据源的问题,只能以参数的方式传递。