springBoot配置多个数据源, 自定义策略动态切换数据源.
本篇以mysql数据库主从同步,读写分离场景为例, 有两个数据源: 主数据源source,用于数据更新(update,insert,delete); 从数据源replica1用于数据查询
基于springBoot2.4.0, 使用默认数据库连接池hikari
国际运动的影响, 当前各开源项目已逐步清理master,salve,blacklist,whitelist等术语, mysql分别使用source,replica,blocklist,allowlist替换,所以本例中也同样如此
application.yml
增加自定义数据源配置
dynamic:
defaultDsKey: replica1
datasources:
source:
username: root
password: root
driver-class-name: com.mysql.cj.jdbc.Driver
jdbc-url: jdbc:mysql://localhost:3306/yuan?useUnicode=true&characterEncoding=UTF-8&serverTimezone=UTC
# 连接池配置
# 最大连接数,默认为10
maximum-pool-size: 2
# 连接池名字,默认以HikariPool-1,HikariPool-2形式增长
pool-name: yuan-source
replica1:
username: root
password: root
driver-class-name: com.mysql.cj.jdbc.Driver
jdbc-url: jdbc:mysql://localhost:3306/yuanbak?useUnicode=true&characterEncoding=UTF-8&serverTimezone=UTC
# 连接池配置
maximum-pool-size: 2
pool-name: yuan-replica1
- 定义实体类接收上面的配置. 很多教程教你自定义一个数据源配置properties对应的实体类, 我觉得没有必要, 复用spring原本的就挺好( 在
application.yml
中spring.datasource 配置对应的实体类是org.springframework.boot.autoconfigure.jdbc.DataSourceProperties
, spring.datasource.hikari池配置对应的实体类是com.zaxxer.hikari.HikariDataSource
)
import com.zaxxer.hikari.HikariDataSource;
import lombok.Getter;
import lombok.Setter;
import org.springframework.boot.context.properties.ConfigurationProperties;
import java.util.Map;
@ConfigurationProperties(prefix = "dynamic")
@Getter
@Setter
public class DynamicDataSourceProperties {
private Map<String, HikariDataSource> datasources;
// 默认数据源
private String defaultDsKey;
}
- 重点: 继承
AbstractRoutingDataSource
抽象类并注册为bean
import com.zaxxer.hikari.HikariDataSource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;
import javax.annotation.Resource;
import java.util.HashMap;
import java.util.Map;
@Slf4j
@Configuration
// 启用DynamicDataSourceProperties配置
@EnableConfigurationProperties(DynamicDataSourceProperties.class)
public class DynamicDataSourceConfig {
@Resource
private DynamicDataSourceProperties dynamicDataSourceProperties;
@Bean
public AbstractRoutingDataSource dataSource() {
// 实现AbstractRoutingDataSource的determineCurrentLookupKey方法, 该方法会返回当前要使用的数据源对应的dsKey
AbstractRoutingDataSource abstractRoutingDataSource = new AbstractRoutingDataSource() {
@Override
protected Object determineCurrentLookupKey() {
return DsKeyThreadLocal.getDsKey();
}
};
Map<String, HikariDataSource> datasources = dynamicDataSourceProperties.getDatasources();
log.info("all datasource key: {}", datasources.keySet());
// 必须配置默认数据源
String defaultDsKey = dynamicDataSourceProperties.getDefaultDsKey();
if (!datasources.containsKey(defaultDsKey)) {
throw new IllegalArgumentException("must config default datasource");
}
log.info("default datasource key: {}", defaultDsKey);
// 设置所有数据源
Map<Object, Object> dataSourceMap = new HashMap<>();
datasources.forEach((dsKey, hikariDataSource) -> {
dataSourceMap.put(dsKey, hikariDataSource);
});
abstractRoutingDataSource.setTargetDataSources(dataSourceMap);
// 设置默认数据源 当dsKey找不到对应的数据源或没有设置数据源时, 使用默认数据源
abstractRoutingDataSource.setDefaultTargetDataSource(dataSourceMap.get(defaultDsKey));
// afterPropertiesSet()方法调用时用来将targetDataSources的属性写入resolvedDataSources中的
abstractRoutingDataSource.afterPropertiesSet();
return abstractRoutingDataSource;
}
}
DsKeyThreadLocal
用于存储本次会话使用的数据源
// 使用threadLocal存储当前会话使用的数据源对应的dsKey
public final class DsKeyThreadLocal {
private static ThreadLocal<String> DS_KEY = new ThreadLocal<>();
private DsKeyThreadLocal() {
}
public static void setDsKey(String dsKey) {
DS_KEY.set(dsKey);
}
public static String getDsKey() {
return DS_KEY.get();
}
}
- 测试: 在两个数据源中分别造一条id为1,其他字段不同的记录. 查询的结果与对应的数据源记录一致则证明成功
import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
import javax.annotation.Resource;
@SpringBootTest(classes = App.class)
@Slf4j
public class DictDaoTest {
@Resource
DictDao dictDao;
@Test
public void dynamicDataSourceTest() {
// 使用主数据源
DsKeyThreadLocal.setDsKey("source");
Dict dict = dictDao.selectById(1);
log.info("dict : {}", dict);
// 使用从数据源
DsKeyThreadLocal.setDsKey("replica1");
dict = dictDao.selectById(1);
log.info("dict : {}", dict);
}
}
-
定义动态切换数据源的策略, 可以使用filter或者aop实现, 在业务代码之前设置本次会话要使用的数据源dsKey.
下面是一种简单的策略: 众所周知, 在restful风格的接口设计中, GET请求用来获取资源, POST请求用来更新资源. 所以可以制定这样一种简单的策略: GET请求使用从数据源, POST请求使用主数据源
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;
import javax.annotation.Resource;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
@Slf4j
@Component
public class DsFilter extends OncePerRequestFilter {
@Resource
private DynamicDataSourceProperties dynamicDataSourceProperties;
// 此方法能保证在单个请求线程中每个请求只调用一次
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
String method = request.getMethod();
// GET请求使用从库, POST请求使用主库, 应增加else覆盖所有情况
if (method.equals("GET")) {
DsKeyThreadLocal.setDsKey("replica1");
} else if (method.equals("POST")) {
DsKeyThreadLocal.setDsKey("source");
} else {
DsKeyThreadLocal.setDsKey(dynamicDataSourceProperties.getDefaultDsKey());
}
log.info("datasource route to {}", DsKeyThreadLocal.getDsKey());
filterChain.doFilter(request, response);
}
}
cook
-
较低版本的springBoot配置自定义多数据源应该先要排除数据源自动配置
-
动态切换数据源的策略应覆盖所有情况( 即给予当前会话的数据源一个初始值) ,否则会出现当前会话使用的数据源是处理线程在上次会话中使用的. 本来可以设计成获取数据源的
DsKeyThreadLocal#getDsKey
方法调用一次后自动清除DS_KEY, 但是考虑到一次会话有多个查询语句的情况,所以不能如此. -
如果你看日志会发现每个数据源都会创建一个连接池
-
单数据源时配置参考如下:
spring: datasource: # type: com.zaxxer.hikari.HikariDataSource username: root password: root driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/yuan?useUnicode=true&characterEncoding=UTF-8&serverTimezone=UTC hikari: maximum-pool-size: 2 pool-name: yuan-hikari