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
配置类
java
@Data
@ConfigurationProperties(prefix = "app.datasource")
public class SystemDynamicDatasourceProperties {
private Map<String, DruidDataSource> mapDatasource;
}
mybatis
固定数据源配置
java
@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
方法,找到相关的数据源即可。在这个配置里无论添加多少数据源都可以,动态添加也是可以的。
- 动态数据源配置
java
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统一配置。
代码如下:
-
注解
java@Target({ElementType.TYPE, ElementType.METHOD}) public @interface OtherDatasource { String value() default ""; }
可以通过这个注解来充当切点,但是本次使用仅作为额外数据,获取指定的数据源使用。
-
切面
java@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不干预,原样传递。因为本次过程不修改什么参数。所以使用的是这个
- 带参数的:自然就是相反的,将替换掉调用时传递的参数,这时候方法里调用的就是切面里的参数。
因为目前业务需求问题,都是使用的参数进行传递,所以只能定义在方法参数上。像这个样子:
java
@Mapper
public interface TestMapper {
int insertTest(String env, Entity entity);
}
第一个参数就决定是哪个数据源,但是实际上业务并不采用。因为无法固定使用某个数据源的问题,只能以参数的方式传递。