0%

Spring+Mybatis 多数据源动态切换

Spring+Mybatis配置多数据源并实现动态切换,目前网上已经有很多教程来实现该功能,但主要实现方式还是通过Spring提供的AbstractRoutingDataSource和AOP来实现,而自定义注解的方式往往会产生重复劳动,且让DAL的逻辑侵入到BLL,显然不够优雅。本文旨在探讨基于Mybatis插件或动态代理的方式来改进网络上现有的方案。

基本环境配置

本文将基于SpringBoot+Mybatis来探讨

引入依赖

1
2
3
4
5
6
7
8
9
10
11
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
<version>${spring.boot.version}</version>
</dependency>

<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>${mybatis-spring-boot-starter.version}</version>
</dependency>

定义数据源常量

1
2
3
4
5
6
7
8
public class DataSources {

public static final String DB1 = "db1";

public static final String DB2 = "db2";

public static final String DB3 = "db3";
}

定义数据源上下文

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class DataSourceContext {

/**
* 使用ThreadLocal保存当前线程所需要的数据源
*/
private static final ThreadLocal<String> CONTEXT_HOLDER = new ThreadLocal<>();

public static void set(String dataSource) {
CONTEXT_HOLDER.set(dataSource);
}

public static String get() {
return CONTEXT_HOLDER.get();
}

public static void clear() {
CONTEXT_HOLDER.remove();
}
}

自定义动态数据源实现

1
2
3
4
5
6
7
8
9
import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;

public class DynamicDataSource extends AbstractRoutingDataSource {

@Override
protected Object determineCurrentLookupKey() {
return DataSourceContext.get();
}
}

配置数据源

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
@Bean
public DataSource db1() {
HikariDataSource dataSource = new HikariDataSource();
dataSource.setDriverClassName("com.mysql.jdbc.Driver");
dataSource.setJdbcUrl("jdbc:mysql://xxx");
dataSource.setUsername("xxx");
dataSource.setPassword("xxx");
return dataSource;
}

@Bean
public DataSource db2() {
HikariDataSource dataSource = new HikariDataSource();
dataSource.setDriverClassName("com.mysql.jdbc.Driver");
dataSource.setJdbcUrl("jdbc:mysql://xxx");
dataSource.setUsername("xxx");
dataSource.setPassword("xxx");
return dataSource;
}

@Bean
public DataSource db3() {
HikariDataSource dataSource = new HikariDataSource();
dataSource.setDriverClassName("com.mysql.jdbc.Driver");
dataSource.setJdbcUrl("jdbc:mysql://xxx");
dataSource.setUsername("xxx");
dataSource.setPassword("xxx");
return dataSource;
}

/**
* 使用@Primary声明该数据源为主要
*/
@Bean
@Primary
public DataSource dynamicDataSource(@Qualifier(DataSources.DB1) DataSource db1,
@Qualifier(DataSources.DB2) DataSource db2,
@Qualifier(DataSources.DB3) DataSource db3) {
Map<Object, Object> map = new HashMap<>(4);
map.put(DataSources.DB1, db1);
map.put(DataSources.DB2, db2);
map.put(DataSources.DB3, db3);
DynamicDataSource dynamicDataSource = new DynamicDataSource();
// 设置多数据源
dynamicDataSource.setTargetDataSources(map);
// 设置默认数据源为DB1
dynamicDataSource.setDefaultTargetDataSource(DataSources.DB1);
return dynamicDataSource;
}

开始使用

其实配置到这里,就可以通过上面定义的DataSourceContext类在业务代码中动态的设置数据源了;或者按照主流教程,使用自定义注解+AOP的方式来实现数据源自动切换

弊端

数据源切换本应该是DAL实现的,BLL不应该关心自己需要切换到哪个数据源,而Mybatis框架是通过接口+动态代理实现对数据库操作的封装,无法使用Spring的AOP在Mapper中织入切换数据源的逻辑,DAL的注解就会出现在BLL。

当然也可以通过增加Repo层来完成这个逻辑,但是Repo层往往内容单薄且会产生很多重复的逻辑操作,大大降低编码效率

自定义注解

1
2
3
4
5
6
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface RoutingDataSource {

String value() default DataSources.DB1;
}

方案一 Mybatis插件实现

修改注解定义

1
2
3
4
5
6
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface RoutingDataSource {

String value() default DataSources.DB1;
}

定义Mybatis插件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
/**
* mybatis插件,支持在mapper上直接定义数据源路由
*/
@Bean
@Intercepts(value = {
@Signature(type = Executor.class, method = "update", args = {MappedStatement.class, Object.class}),
@Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class}),
@Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class, CacheKey.class, BoundSql.class}),
@Signature(type = Executor.class, method = "queryCursor", args = {MappedStatement.class, Object.class, RowBounds.class}),
})
public class DataSourceInterceptor implements Interceptor, ApplicationListener<ContextRefreshedEvent> {

/**
* [mapper, datasource] 通过监听ContextRefreshedEvent事件,提前写入Mybatis的Statement缓存
*/
private final Map<String, String> CACHE = new HashMap<>(128);

@Override
public Object intercept(Invocation invocation) throws Throwable {
String dataSource = CACHE.get(((MappedStatement) invocation.getArgs()[0]).getId());
if (dataSource == null) {
dataSource = DataSources.DB1;
}
DataSourceContext.set(dataSource);
try {
return invocation.proceed();
} finally {
DataSourceContext.remove();
}
}

@Override
public Object plugin(Object target) {
return Plugin.wrap(target, this);
}

@Override
public void setProperties(Properties properties) {
}

@Override
public void onApplicationEvent(ContextRefreshedEvent event) {
Map<String, SqlSessionFactory> factories = event.getApplicationContext().getBeansOfType(SqlSessionFactory.class);
if (factories.isEmpty()) {
return;
}
for (SqlSessionFactory factory : factories.values()) {
Collection<Class<?>> mappers = factory.getConfiguration().getMapperRegistry().getMappers();
for (Class<?> mapper : mappers) {
RoutingDataSource annotation = mapper.getAnnotation(RoutingDataSource.class);
String dataSource = annotation == null ? DataSources.DB1 : annotation.value();
Method[] methods = mapper.getMethods();
for (Method method : methods) {
CACHE.put(mapper.getName() + "." + method.getName(), dataSource);
}
}
}
}
}

实现效果

1
2
3
@RoutingDataSource(DataSources.DB1)
public interface Db1BizMapper {
}
1
2
3
@RoutingDataSource(DataSources.DB2)
public interface Db2BizMapper {
}
1
2
3
4
5
6
7
8
9
10
11
12
@Service
public class PaperMoonService {
@Resource
private Db1BizMapper db1BizMapper;
@Resource
private Db2BizMapper db2BizMapper;

public void service() {
db1BizMapper.doSomeThing();
db1BizMapper.doSomeThing();
}
}

方案二 动态代理实现

定义InvocationHandler

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class MapperInvocationHandler implements InvocationHandler {

private final Object target;
private final String dataSource;

public MapperInvocationHandler(Object target, String dataSource) {
this.target = target;
this.dataSource = dataSource;
}

@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
DataSourceContext.set(dataSource);
try {
return method.invoke(target, args);
} finally {
DataSourceContext.remove();
}
}
}

继承Mybatis的FactoryBean并重写getObject()方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class MyMapperFactoryBean<T> extends org.mybatis.spring.mapper.MapperFactoryBean<T> {

public MyMapperFactoryBean() {
}

public MyMapperFactoryBean(Class<T> mapperInterface) {
super(mapperInterface);
}

@Override
@SuppressWarnings("unchecked")
public T getObject() throws Exception {
Class<T> mapper = super.getMapperInterface();
RoutingDataSource annotation = mapper.getAnnotation(RoutingDataSource.class);
String dataSource = annotation == null ? DataSources.DB1 : annotation.value();

return (T) Proxy.newProxyInstance(
mapper.getClassLoader(), new Class[]{mapper}, new MapperInvocationHandler(super.getObject(), dataSource)
);
}
}

替换成自定义的FactoryBean

1
2
3
4
5
6
7
8
@MapperScan(basePackages = "com.xxx.dao.mapper", factoryBean = MyMapperFactoryBean.class)
@SpringBootApplication(exclude = DataSourceAutoConfiguration.class)
public class Application {

public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}

实现效果

同方案一,且性能和可靠性更好,当然JDK默认的动态代理是通过反射调用的目标方法;如果追求性能,可替换为Cglib等方案来实现。

遗留问题

Spring的@Transactional注解无法使用,一个事务内无法切换数据源

解决方案

在针对多使用多数据源进行数据库操作时,使用编程式事务手动处理;或者弃用Spring的事务管理自定义实现

方案实现参考

https://github.com/yhzdys/litchi