springboot多数据源, 动态数据源实现
背景: 现在随着数据量,业务量的增多,很多情况下,单个数据库已无 法满足项目需求,此时可能需要配置不同的数据源来满足需求,下面介绍基于springboot的多数据源和动态数据源的实现
1. 多数据源
介绍: 基于springboot的多数据源配置,此处可以直接使用mp提供的方法来实现,简单便捷
- 引入pom依赖
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>dynamic-datasource-spring-boot-starter</artifactId>
<version>${version}</version>
</dependency>
- yml文件配置数据源
spring:
application:
name: my-manage-api-provider
datasource:
dynamic:
primary: manage
datasource:
manage:
url: jdbc:oracle:thin:@127.0.0.1:1521/database
username: xxxx
password: 123456
cs:
url: jdbc:oracle:thin:@127.0.0.1:1521/database
username: yyyy
password: 123456
druid:
db-type: oracle
- dao层类使用@DS(“数据源key”)即可切换,如@DS(“cs”), 不写就是用的primary设置的数据源
2. 动态数据源
介绍: 随着项目的扩展,例如项目中需要数据库连接信息动态获取,不能在yml文件配置固定了,多数据源可能已经无法满足需求了,所以这就需要我们动态的创建connection来实现
基于springboot的动态数据源,可以通过继承AbstractRoutingDataSource类实现,此处主要包括4个类:
DatasourceConfig
DynamicDataSourceContextHolder
DynamicDataSource
SwitchDb
- springboot启动类上添加
@SpringBootApplication(exclude = {DataSourceAutoConfiguration.class})
- 配置DatasourceConfig
@Configuration
public class DatasourceConfig {
@ConfigurationProperties(prefix = "spring.datasource.druid")
@Bean
public DataSource druidDataSource() {
return DruidDataSourceBuilder.create().build();
}
@Bean
@Primary
public DataSource dynamicDataSource() {
DataSource druidDataSource = druidDataSource();
DynamicDataSource dynamicDataSource = DynamicDataSource.getInstance();
dynamicDataSource.setDefaultTargetDataSource(druidDataSource);
Map<Object, Object> targetDataSources = new HashMap<>(1);
targetDataSources.put("default", druidDataSource);
dynamicDataSource.setTargetDataSources(targetDataSources);
dynamicDataSource.put("default",druidDataSource);
System.out.println("DataSourceConfig.dynamicDataSource");
return dynamicDataSource;
}
}
此处是向datasource添加默认的初始化数据源,其中设置setTargetDataSources()和setDefaultTargetDataSource()是两个最重要的方法
- DynamicDataSourceContextHolder
public class DynamicDataSourceContextHolder {
private static final ThreadLocal<String> CONTEXT_HOLDER = new ThreadLocal<String>() {
};
/**
* 数据源的 key集合,用于切换时判断数据源是否存在
*/
public static List<Object> dataSourceKeys = new ArrayList<>();
/**
* 切换数据源
* @param key
*/
public static void setDataSourceKey(String key) {
CONTEXT_HOLDER.set(key);
}
/**
* 获取数据源
* @return
*/
public static String getDataSourceKey() {
return CONTEXT_HOLDER.get();
}
/**
* 重置数据源
*/
public static void clearDataSourceKey() {
CONTEXT_HOLDER.remove();
}
/**
* 判断是否包含数据源
* @param key 数据源key
* @return
*/
public static boolean containDataSourceKey(String key) {
return dataSourceKeys.contains(key);
}
/**
* 添加数据源keys
* @param keys
* @return
*/
public static boolean addDataSourceKeys(Collection<? extends Object> keys) {
return dataSourceKeys.addAll(keys);
}
}
此处主要是创建用于存储数据源的threadlocal,包括设置,获取,及判断是否包含等方法
- DynamicDataSource
@Data
public class DynamicDataSource extends AbstractRoutingDataSource {
private final Logger logger = LoggerFactory.getLogger(getClass());
@Nullable
private static Map<String, DataSource> targetDataSources = new HashMap<>();
@Nullable
private DataSource defaultTargetDataSource;
/**
* 单例句柄
*/
private static DynamicDataSource instance;
private DynamicDataSource(){}
private static final byte[] LOCK=new byte[0];
public void put(String key,DataSource value){
targetDataSources.put(key,value);
}
public void putAll(Map<String, DataSource> target){
targetDataSources.putAll(target);
}
public DataSource get(String key){
return targetDataSources.get(key);
}
public void setDefaultTargetDataSource(DataSource defaultTargetDataSource) {
this.defaultTargetDataSource = defaultTargetDataSource;
}
@Override
public Connection getConnection() throws SQLException {
return this.determineTargetDataSource().getConnection();
}
@Override
public Connection getConnection(String username, String password) throws SQLException {
return this.determineTargetDataSource().getConnection(username,password);
}
@Override
protected DataSource determineTargetDataSource() {
String lookupKey = this.determineCurrentLookupKey();
DataSource dataSource = targetDataSources.get(lookupKey);
if (dataSource == null && lookupKey == null) {
dataSource = this.defaultTargetDataSource;
}
if (dataSource == null) {
throw new IllegalStateException("Cannot determine target DataSource for lookup key [" + lookupKey + "]");
} else {
return dataSource;
}
}
/**
* 获取当前数据源
* @return
*/
@Override
public String determineCurrentLookupKey() {
logger.info("Current DataSource is [{}]", DynamicDataSourceContextHolder.getDataSourceKey());
return DynamicDataSourceContextHolder.getDataSourceKey();
}
/**
* 单例方法
* @return
*/
public static synchronized DynamicDataSource getInstance(){
if(instance==null){
synchronized (LOCK){
if(instance==null){
instance=new DynamicDataSource();
}
}
}
return instance;
}
/**
* 是否存在当前key的 DataSource
* @param key
* @return 存在返回 true, 不存在返回 false
*/
public static boolean isExistDataSource(String key) {
return targetDataSources.containsKey(key);
}
/**
* 删除数据源
* @param datasourceCode
* @return
*/
public boolean delDatasource(String datasourceCode) {
assert targetDataSources != null;
if (targetDataSources.containsKey(datasourceCode)) {
DruidDataSource dataSource = (DruidDataSource) targetDataSources.get(datasourceCode);
targetDataSources.remove(datasourceCode);
dataSource.close();
return true;
} else {
return false;
}
}
}
该类需要继承AbstractRoutingDataSource,定义targetDataSources集合和默认的数据源defaultTargetDataSource
determineTargetDataSource是比较重要的一个方法,用来决定用哪个数据源
a) determineCurrentLookupKey() 用来获取当前数据源的key值,其调用DynamicDataSourceContextHolder中的threadlocal来获取当前的key
b)从目标数据源集合中获取上述获取的key对应的value的数据源,若不存在,则设置为默认的
c)若目标数据源集合不存在且默认的数据源不存在,抛出异常,否则正常返回数据源
- SwichDb
@Component
public class SwitchDb {
private static final Logger LOGGER = LoggerFactory.getLogger(SwitchDb.class);
@Value("${spring.application.name}")
private String applicationName;
@Reference(registry = "dynamic")
private DatasourceProvider datasourceProvider;
public void changeDefault(){
change("default");
}
/**
* 切换数据源
* @param dbKey key
*/
public void change(String dbKey) {
if(dbKey == null){
return;
}
if(! DynamicDataSource.isExistDataSource(dbKey) ){
creatDataSource(dbKey);
}
//获取当前连接的数据源对象的key
String currentKey = DynamicDataSourceContextHolder.getDataSourceKey();
if(currentKey == null){
DynamicDataSourceContextHolder.setDataSourceKey(dbKey);
return;
}
if(currentKey.equals(dbKey)){
return;
}
LOGGER.info(String.format("切换到数据源%s成功",dbKey));
System.out.println(String.format("切换到数据源%s成功",dbKey));
DynamicDataSourceContextHolder.setDataSourceKey(dbKey);
}
/**
* 创建数据源
* @param dbKey key
*/
private void creatDataSource(String dbKey){
//实现创建数据库,数据库连接名称唯一
AreaReq areaReq = new AreaReq();
areaReq.setAreaCode(dbKey);
areaReq.setApplicationName(applicationName);
AreaResp datasourceConfigByAreaCode = datasourceProvider.getDatasourceByAreaCodeAndApplicationName(areaReq);
DataSource dataSource = DatasourceUtil.parseToDataSource(datasourceConfigByAreaCode);
Map<String,DataSource> map = new HashMap<>(2);
map.put(dbKey, dataSource);
DynamicDataSource.getInstance().putAll(map);
System.out.println(String.format("创建数据源%s成功",areaReq.getAreaCode()));
}
}
其中将连接信息转换生成数据源的方法如下:
public static DataSource parseToDataSource(AreaResp areaResp) {
DruidDataSource bds = new DruidDataSource();
bds.setDriverClassName("com.microsoft.sqlserver.jdbc.SQLServerDriver");
bds.setUrl(areaResp.getUrl());
bds.setUsername(areaResp.getUsername());
bds.setPassword(areaResp.getPassword());
bds.setName(areaResp.getAreaCode());
// 默认为false
bds.setPoolPreparedStatements(true);
// 超过removeAbandonedTimeout时间后,是否进 行没用连接(废弃)的回收(默认为false,调整为true)
bds.setRemoveAbandoned(true);
// 超过时间限制,回收没有用(废弃)的连接(默认为 300秒)
bds.setRemoveAbandonedTimeout(300);
// 检查连接是否有效,每次间隔10分钟
bds.setTestWhileIdle(true);
bds.setTestOnBorrow(false);
bds.setTestOnReturn(false);
bds.setValidationQuery("select 1");
bds.setValidationQueryTimeout(3);
return bds;
}
配置实际调用的类SwichDb类, 其中最重要的是change()方法和createDataSource()方法;
- change(): 用于通过key切换数据源,先判断数据源集合中是否已经初始化过本key对应的数据源,若未初始化过,则调用createDataSource()创建,之后判断当前的key是否已经是需要切换的key,若不是则将threadlocal设置为本key
- createDataSource(): 创建数据源的方法,此处是通过key查询数据库的连接信息,然后创建对应数据库类型的datasource,将其放入targetDataSouces,此处以sqlserver为例, 不同类型的数据库只需修改parseToDataSource()方法中的driverClassName和validationQuery即可
- 具体调用
switchDb.change(key);
- 补充:
若一个项目想要兼容多个种类的数据源的sql语句语法,可通过在mapper文件配置databaseId实现
- 配置一个configuration
@Bean
public DatabaseIdProvider databaseIdProvider() {
VendorDatabaseIdProvider databaseIdProvider = new VendorDatabaseIdProvider();
Properties properties = new Properties();
properties.setProperty("Oracle","oracle");
properties.setProperty("MySQL","mysql");
properties.setProperty("SQL Server","sqlserver");
databaseIdProvider.setProperties(properties);
return databaseIdProvider;
}
- 在对应的mapper文件的标签上加上上述配置的的value,如下
<select id="getByName" databaseId="sqlserver" resultType="java.util.Map"></select>
<select id="getByName" databaseId="oracle" resultType="java.util.Map"></select>
以上是一些基于springboot的多数据源和动态数据源的实现,如果对你有帮助,记得留个赞~