大多数SpringBoot应用,只有一个数据库,数据库信息配置在了配置文件中。一旦应用启动后就不能修改,
如果要修改数据源信息那么就需要修改配置,重新编译,打包,运行。
有时候需要这么一种场景,数据源并不是写在配置文件,而是存储在数据库(先有鸡还是先有蛋?)或者是内存中,可以随时的添加,删除。执行业务的时候通过参数指定当前使用的数据源。
例如多租户场景下,不同租户使用不同的数据源。但是又需要使用到Spring的声明式事务。
实现的大概逻辑
AbstractRoutingDataSource 类
自己写代码维护几个数据源并不是难事儿,主要是自己维护的数据源需要用上spring的声明式事务,这就需要使用Spring提供的工具类:org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource
它本身也是实现了 javax.sql.DataSource
接口。所以它可以作为 系统数据源,而且它本身提供了2个方法,可以实现运行时数据源的动态切换。
它维护了一个Map<Object, DataSource>
,里面可以存储多个数据源。
核心的2个方法
// 在Spring开启事务之前,会调用这个方法,它返回一个 DataSource 数据源对象
// Spring就会使用这个数据源进行接下来的事务操作
protected DataSource determineTargetDataSource()
// 数据源以 Map<Object, DataSource> 的形式存储。
// 该方法返回一个数据源的KEY,根据这个KEY到MAP中检索到当前使用的数据源
abstract protected Object determineCurrentLookupKey()
在简单理解了 AbstractRoutingDataSource
之后,就剩下的东西就很简单了。对于数据源的增加删除。其实就是对 AbstractRoutingDataSource
实例中的数据源Map进行添加和删除。
用户的每一个业务请求,都需要在参数携带数据源的标识,也就是KEY。这个KEY可以通过拦截器存储在ThreadLocal
中。用户的参数不同,那么操作的数据源也就不同了。
Demo
工程使用 spring-data-jpa
作为ORM框架,提供2个接口。
- 数据源的添加,删除,查询。
- 用户信息的添加,删除,查询。
数据库建表语句
CREATE TABLE `user` (
`id` int unsigned NOT NULL AUTO_INCREMENT COMMENT 'ID',
`balance` decimal(10,2) DEFAULT NULL COMMENT '账户余额',
`create_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`enabled` tinyint unsigned NOT NULL COMMENT '是否启用。0:禁用,1:启用',
`name` varchar(50) COLLATE utf8mb4_general_ci NOT NULL COMMENT '名字',
`update_at` timestamp NULL DEFAULT NULL COMMENT '修改时间',
PRIMARY KEY (`id`),
UNIQUE KEY `name` (`name`)
) ENGINE=InnoDB AUTO_INCREMENT=8 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='用户';
这里仅仅说明核心的代码,完整代码请在文末Github中获取
这里仅仅说明核心的代码,完整代码请在文末Github中获取
这里仅仅说明核心的代码,完整代码请在文末Github中获取
DynamicDataSource
数据源的实现
package io.springboot.demo.datasource;
import java.util.HashMap;
import java.util.Map;
import javax.sql.DataSource;
import org.springframework.beans.factory.DisposableBean;
import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;
import org.springframework.stereotype.Component;
import com.zaxxer.hikari.HikariDataSource;
import lombok.extern.slf4j.Slf4j;
@Component
@Slf4j
public class DynamicDataSource extends AbstractRoutingDataSource implements DisposableBean {
// 系統中维护的数据源
private final Map<Object, DataSource> dataSources = new HashMap<>();
/**
* 根据KEY获取到数据源
*/
protected DataSource determineTargetDataSource() {
DataSource dataSource = this.dataSources.get(this.determineCurrentLookupKey());
if (dataSource == null) {
// TODO 数据源不存在
}
return dataSource;
}
/**
* 获取当前的KEY
*/
@Override
protected Object determineCurrentLookupKey() {
return DataSourceKeyHolder.get();
}
public Map<Object, DataSource> getDataSources() {
return dataSources;
}
/**
* 复写父类的afterPropertiesSet,空实现。
* 父类会进行非空校验,会导致异常
*/
@Override
public void afterPropertiesSet() {
}
@Override
public void destroy() throws Exception {
this.dataSources.entrySet().forEach(db -> {
log.info("释放 {} 数据源资源", db.getKey());
((HikariDataSource) db.getValue()).close();
});
}
}
DataSourceController
数据源管理接口
package io.springboot.demo.controller;
import java.util.Collections;
import java.util.Map;
import javax.sql.DataSource;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import com.zaxxer.hikari.HikariDataSource;
import io.springboot.demo.datasource.DynamicDataSource;
import lombok.Data;
// 数据源Model
@Data
class DataSourceDTO {
private String key; // 唯一标识
private String url; // JDBC连接地址
private String username; // 用户名
private String password; // 密码
}
@RestController
@RequestMapping("/api/datasource")
public class DataSourceController {
@Autowired
private DynamicDataSource dataSource;
/**
* 获取系统中的所有数据源
* @return
*/
@GetMapping
public Object list () {
return this.dataSource.getDataSources().entrySet()
.stream()
.map(db -> Collections.singletonMap(db.getKey(), ((HikariDataSource) db.getValue()).getJdbcUrl()))
.toList();
}
/**
* 添加数据源
* @param payload
* @return
*/
@PostMapping(produces = "text/plain; charset=utf-8")
public Object add (@RequestBody DataSourceDTO payload) {
Map<Object, DataSource> dataSource = this.dataSource.getDataSources();
if (dataSource.containsKey(payload.getKey())) {
return "已经存在:" + payload.getKey();
}
HikariDataSource hikariDataSource = new HikariDataSource();
hikariDataSource.setJdbcUrl(payload.getUrl());
hikariDataSource.setUsername(payload.getUsername());
hikariDataSource.setPassword(payload.getPassword());
dataSource.put(payload.getKey(), hikariDataSource);
return "添加成功";
}
/**
* 删除数据源
* @param key
* @return
*/
@DeleteMapping("/{key}")
public Object delete (@PathVariable("key") String key) {
DataSource dataSource = this.dataSource.getDataSources().remove(key);
if (dataSource == null) {
return "数据源不存在:" + key;
}
((HikariDataSource) dataSource).close();
return "删除成功";
}
}
UserController
用户管理接口
package io.springboot.demo.controller;
import java.time.LocalDateTime;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import io.springboot.demo.entity.User;
import io.springboot.demo.service.UserService;
import lombok.extern.slf4j.Slf4j;
@RestController
@RequestMapping("/api/user")
@Slf4j
public class UserController {
@Autowired
private UserService userService;
@GetMapping
public Object list() {
return this.userService.findAll();
}
@PostMapping
public Object create (@RequestBody User user) {
user.setCreateAt(LocalDateTime.now());
log.info("添加用户: {}", user);
this.userService.save(user);
return user;
}
@DeleteMapping(value = "/{id}", produces = "text/plain; charset=utf-8")
public Object delete (@PathVariable("id") Integer id) {
this.userService.deleteById(id);
return "删除成功";
}
}
DataSourceInterceptor
从请求中解析出当前要使用的数据源的KEY
package io.springboot.demo.interceptor;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.util.StringUtils;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;
import io.springboot.demo.datasource.DataSourceKeyHolder;
import io.springboot.demo.datasource.DynamicDataSource;
import lombok.extern.slf4j.Slf4j;
@Slf4j
public class DataSourceInterceptor implements HandlerInterceptor {
@Autowired
private DynamicDataSource dataSource;
/**
* 从 Request 中解析出要使用的DataSourceKey
*/
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)throws Exception {
// 从Header 解析
String dataSourceKey = request.getHeader("x-db-key");
// 从查询参数解析
if (!StringUtils.hasText(dataSourceKey)) {
dataSourceKey = request.getParameter("dbKey");
}
log.info("DB标识: {}", dataSourceKey);
if (!StringUtils.hasText(dataSourceKey)) {
response.setContentType("text/plain");
response.setCharacterEncoding("utf-8");
response.setStatus(HttpServletResponse.SC_BAD_REQUEST);
response.getWriter().println("缺少DB标识");
return false;
}
if(!this.dataSource.getDataSources().containsKey(dataSourceKey)) {
response.setContentType("text/plain");
response.setCharacterEncoding("utf-8");
response.setStatus(HttpServletResponse.SC_BAD_REQUEST);
response.getWriter().println("数据源:" + dataSourceKey + ", 不存在");
return false;
}
DataSourceKeyHolder.set(dataSourceKey);
return true;
}
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
DataSourceKeyHolder.clear();
HandlerInterceptor.super.postHandle(request, response, handler, modelAndView);
}
}
测试
先启动系统
添加3个数据源
数据库中的3个数据源也就是这样
查看系统数据源
添加用户,指定数据源 demo1
查询用户
从 demo1 查询,可以获取到新增的结果
从 demo3 查询,结果为空
删除Demo3数据源
最后
这仅仅是一个实现数据源动态增删思路的一个Demo,它还有很多问题。例如数据源Map的并发问题,数据库资源的释放问题,数据库的初始化时间问题等等需要在实际应用的时候考虑到。
完整源码
https://github.com/KevinBlandy/springboot-dynamic-datasource