前面章节讲解了 如何在 MyBatis + Druid 自定义多数据源,前面是静态配置,就是有几个数据源就配置几个 MyBatis 的配置,本章会将前面的第二个数据源配置改成动态数据源,也就是会在 2 个数据源之间进行按需切换,同一套 mapper 可以在这两个数据源上运行,它们的表结构是一致的。
其实这个动态数据源的核心原理就是:在获取数据库连接前,会有一个动作是 获取当前的数据源,那么获取当前数据源这个操作其实是有一个术语叫做 路由
路由数据源核心原理
spring jdbc 提供了一个扩展数据源 org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource
它可以实现选择指定的数据源产生连接
public abstract class AbstractRoutingDataSource extends AbstractDataSource implements InitializingBean {
....
@Override
public Connection getConnection() throws SQLException {
return determineTargetDataSource().getConnection();
}
@Override
public Connection getConnection(String username, String password) throws SQLException {
return determineTargetDataSource().getConnection(username, password);
}
protected DataSource determineTargetDataSource() {
Assert.notNull(this.resolvedDataSources, "DataSource router not initialized");
// 获取当前要使用的路由数据源的 key
Object lookupKey = determineCurrentLookupKey();
// 然后从 resolvedDataSources 中获取真正的数据源对象
DataSource dataSource = this.resolvedDataSources.get(lookupKey);
if (dataSource == null && (this.lenientFallback || lookupKey == null)) {
dataSource = this.resolvedDefaultDataSource;
}
if (dataSource == null) {
throw new IllegalStateException("Cannot determine target DataSource for lookup key [" + lookupKey + "]");
}
return dataSource;
}
/**
* Determine the current lookup key. This will typically be
* implemented to check a thread-bound transaction context.
* <p>Allows for arbitrary keys. The returned key needs
* to match the stored lookup key type, as resolved by the
* {@link #resolveSpecifiedLookupKey} method.
*/
@Nullable
protected abstract Object determineCurrentLookupKey();
}
可以看到,在获取 数据库连接前,会调用 determineTargetDataSource()
获取要路由的数据源对象,最后会调用 determineCurrentLookupKey()
方法去返回一个 lookupKey,然后在准备好的真正的数据源中获取与之对应的 lookupKey 的数据源。
动态路由的实现
这里先实现 AbstractRoutingDataSource 实现
package cn.mrcode.autoconfig.mybatis;
import lombok.Getter;
import lombok.Setter;
import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;
/**
* 路由数据源
*/
public class Db02DataSourceRouter extends AbstractRoutingDataSource {
@Getter
@Setter
private volatile String currentKey = "ds1";
@Override
protected Object determineCurrentLookupKey() {
// 返回当前的数据源 key
/*
一般的动态数据源做法会使用拦截器,去查找 mapper 上的自定义注解写的是数据源名称,
然后使用 ThreadLocal 方式,设置获取到的 key
在这里从 ThreadLocal 中获取返回
在切面中执行完目标方法之后,再从 ThreadLocal 中清除掉
而我这里的方式采用统一按需进行切换,所以只需要在该类成员上定义当前使用哪一个数据源即可
可以按自己的业务场景触发变更该成员变量的值来达到切换数据源的目的
*/
return currentKey;
}
}
然后改造 MyBatis 数据源的配置代码
package cn.mrcode.autoconfig.mybatis.mls;
import com.alibaba.druid.spring.boot.autoconfigure.DruidDataSourceBuilder;
import org.mybatis.spring.SqlSessionFactoryBean;
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.core.io.support.PathMatchingResourcePatternResolver;
import org.springframework.core.io.support.ResourcePatternResolver;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
import tk.mybatis.spring.annotation.MapperScan;
import javax.sql.DataSource;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
@Configuration
@MapperScan(
value = {
"cn.mrcode.repo.mapper.db02"
},
sqlSessionFactoryRef = "db02SqlSessionFactoryBean")
public class MlsMyBatisConfigurer {
@Bean("db0201DataSource")
@ConfigurationProperties(prefix = "spring.datasource.db0201")
public DataSource dataSource1() {
return DruidDataSourceBuilder.create().build();
}
@Bean("db0202DataSource")
@ConfigurationProperties(prefix = "spring.datasource.db0202")
public DataSource dataSource2() {
return DruidDataSourceBuilder.create().build();
}
/**
* 目标数据源集合,方便在路由里面选择
*
* @param ds1
* @param ds2
* @return
*/
@Bean("db02TargetDataSources")
public Map<Object, Object> targetDataSources(
@Qualifier("mlsDb1DataSource") DataSource ds1,
@Qualifier("mlsDb2DataSource") DataSource ds2
) {
HashMap<Object, Object> map = new HashMap<>();
map.put("ds1", ds1);
map.put("ds2", ds2);
return map;
}
@Bean("db02DataSource")
public DataSource dataSource(@Qualifier("db02TargetDataSources") Map<Object, Object> targetDataSources) {
Db02DataSourceRouter sourceRouter = new Db02DataSourceRouter();
sourceRouter.setTargetDataSources(targetDataSources);
// 该方法会被 bean ioc 容器调用
// 同时,如果更改了 targetDataSources 里面的内容,也可以手动调用该方法使 router 里面的相关成员得到更新
// sourceRouter.afterPropertiesSet();
return sourceRouter;
}
/**
* 配置 mybatis
*/
@Bean("db02SqlSessionFactoryBean")
public SqlSessionFactoryBean sqlSessionFactoryBean(@Qualifier("db02DataSource") DataSource dataSource) throws IOException {
SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();
sqlSessionFactoryBean.setDataSource(dataSource);
// 这里需要一个 Resource 可变数组,如何写?
/* 其实这个可以通过查看他的自动配置源码是如何写的
mybatis:
mapper-locations: /mapper/*.xml
*/
ResourcePatternResolver resourceResolver = new PathMatchingResourcePatternResolver();
sqlSessionFactoryBean.setMapperLocations(resourceResolver.getResources("/mapper/db02/**/*.xml"));
return sqlSessionFactoryBean;
}
@Bean("db02DataSourceTransactionManager")
public DataSourceTransactionManager transactionManager(@Qualifier("db02DataSource") DataSource dataSource) {
return new DataSourceTransactionManager(dataSource);
}
}
可以看到,上面改动的有以下几点:
- 将原来直接对应到数据库中的数据源,这里说的是 SqlSessionFactoryBean 中需要的数据源,替换成了 Db02DataSourceRouter 这个路由类
- 就是构建 Db02DataSourceRouter 这个类了,里面需要对应数据库的普通数据源
下面来看看 yaml 中的配置变成了什么样子
spring:
datasource:
druid:
# 让 druid 的自动配置生效,配置监控相关功能
# 配置 DruidStatFilter
web-stat-filter:
enabled: true
url-pattern: "/*"
exclusions: "*.js,*.gif,*.jpg,*.bmp,*.png,*.css,*.ico,/druid/*"
# 配置DruidStatViewServlet
stat-view-servlet:
enabled: true
url-pattern: "/druid/*"
# IP白名单(没有配置或者为空,则允许所有访问)
# allow: 127.0.0.1,192.168.163.1
allow: ""
# IP黑名单 (存在共同时,deny优先于allow)
# deny: 192.168.1.73
# 禁用HTML页面上的“Reset All”功能
reset-enable: false
# 登录名 和 密码
login-username: admin
login-password: 123456
# 多数据源配置
db01:
name: DB-01
url: jdbc:mysql://127.0.0.1:3307/test1?serverTimezone=Asia/Shanghai&useUnicode=true&characterEncoding=utf-8&useSSL=true
username: root
password: root
# 连接池的配置信息
# 初始化大小,最小,最大
initialSize: 5
minIdle: 5
maxActive: 20
# 配置获取连接等待超时的时间
maxWait: 60000
# 配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒
timeBetweenEvictionRunsMillis: 60000
# 配置一个连接在池中最小生存的时间,单位是毫秒
minEvictableIdleTimeMillis: 300000
validationQuery: SELECT 1 FROM DUAL
testWhileIdle: true
testOnBorrow: false
testOnReturn: false
# 打开PSCache,并且指定每个连接上PSCache的大小
poolPreparedStatements: true
maxPoolPreparedStatementPerConnectionSize: 20
# 配置监控统计拦截的filters,去掉后监控界面sql无法统计,'wall'用于防火墙
filters: stat,wall,slf4j
# 通过connectProperties属性来打开mergeSql功能;慢SQL记录
connectionProperties: druid.stat.mergeSql=true;druid.stat.slowSqlMillis=5000
# 多数据源配置
db0201:
name: DB-02-01
url: jdbc:mysql://127.0.0.1:3307/test2?serverTimezone=Asia/Shanghai&useUnicode=true&characterEncoding=utf-8&useSSL=true
username: root
password: root
# 连接池的配置信息
# 初始化大小,最小,最大
initialSize: 5
minIdle: 5
maxActive: 20
# 配置获取连接等待超时的时间
maxWait: 60000
# 配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒
timeBetweenEvictionRunsMillis: 60000
# 配置一个连接在池中最小生存的时间,单位是毫秒
minEvictableIdleTimeMillis: 300000
validationQuery: SELECT 1 FROM DUAL
testWhileIdle: true
testOnBorrow: false
testOnReturn: false
# 打开PSCache,并且指定每个连接上PSCache的大小
poolPreparedStatements: true
maxPoolPreparedStatementPerConnectionSize: 20
# 配置监控统计拦截的filters,去掉后监控界面sql无法统计,'wall'用于防火墙
filters: stat,wall,slf4j
# 通过connectProperties属性来打开mergeSql功能;慢SQL记录
connectionProperties: druid.stat.mergeSql=true;druid.stat.slowSqlMillis=5000
# 多数据源配置
db0202:
name: DB-02-02
url: jdbc:mysql://127.0.0.1:3307/test3?serverTimezone=Asia/Shanghai&useUnicode=true&characterEncoding=utf-8&useSSL=true
username: root
password: root
# 连接池的配置信息
# 初始化大小,最小,最大
initialSize: 5
minIdle: 5
maxActive: 20
# 配置获取连接等待超时的时间
maxWait: 60000
# 配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒
timeBetweenEvictionRunsMillis: 60000
# 配置一个连接在池中最小生存的时间,单位是毫秒
minEvictableIdleTimeMillis: 300000
validationQuery: SELECT 1 FROM DUAL
testWhileIdle: true
testOnBorrow: false
testOnReturn: false
# 打开PSCache,并且指定每个连接上PSCache的大小
poolPreparedStatements: true
maxPoolPreparedStatementPerConnectionSize: 20
# 配置监控统计拦截的filters,去掉后监控界面sql无法统计,'wall'用于防火墙
filters: stat,wall,slf4j
# 通过connectProperties属性来打开mergeSql功能;慢SQL记录
connectionProperties: druid.stat.mergeSql=true;druid.stat.slowSqlMillis=5000
测试
这里的测试就比较简单了,比如可以写一个 controller,只要改变 Db02DataSourceRouter 中的 currentKey 参数即可
@Autowired
@Qualifier("db02DataSource")
private DataSource db02DataSourceRouter;
@ApiOperation("数据源配置切换测试")
@PostMapping("ds-switch")
public Result dsSwitch(int index) {
Db02DataSourceRouter router = (Db02DataSourceRouter) mlsDataSourceRouter;
// 设置当前生效的数据源
router.setCurrentKey(index == 1 ? "ds1" : "ds2");
return ResultHelper.ok(router.getCurrentKey());
}
遗留的问题
配置打印 myabtis 相关的日志信息
mybatis:
configuration:
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
在获取链接的时候能看到如下 debug 的日志信息
was not registered for synchronization because synchronization is not active
Creating a new SqlSession
以上报错信息是在 org.mybatis.spring.SqlSessionUtils#registerSessionHolder
中 debug 信息,暂时不明白具体是什么原因导致的,但是不影响功能的使用。后续有时间,或则有谁知道的请给我留言。