springboot实现动态数据源,动态数据源原理与代码,租户系统动态数据源
一、springboot动态数据源原理
1. 继承AbstractRoutingDataSource得到数据源
抽象类AbstractRoutingDataSource,通过继承这个类实现根据不同的请求切换数据源。
AbstractRoutingDataSource继承自AbstractDataSource,如果声明一个类继承AbstractRoutingDataSource则这个类本身就是数据源。
2. 数据源的getConnection()方法
既然是数据源一定会用到getConnection()方法,下面看源码:
public Connection getConnection() throws SQLException {
return this.determineTargetDataSource().getConnection();
}
public Connection getConnection(String username, String password) throws SQLException {
return this.determineTargetDataSource().getConnection(username, password);
}
通过上面源码能分析得到数据库连接是由determineTargetDataSource()得来,下面继续分析determineTargetDataSource()方法。
3. 解析determineTargetDataSource方法
查看源码:
protected DataSource determineTargetDataSource() {
Assert.notNull(this.resolvedDataSources, "DataSource router not initialized");
Object lookupKey = this.determineCurrentLookupKey();
DataSource 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 + "]");
} else {
return dataSource;
}
}
通过这段源码能否得到数据源首先需要获取 lookupKey:
Object lookupKey = this.determineCurrentLookupKey();
然后通过这个key得到对应的数据源:
DataSource dataSource = (DataSource)this.resolvedDataSources.get(lookupKey);
4. 解析resolvedDataSources
看代码resolvedDataSources的属性,首先它是Map,通过Object可以得到DataSoure
@Nullable
private Map<Object, DataSource> resolvedDataSources;
然后这个属性的赋值代码:
public void afterPropertiesSet() {
if (this.targetDataSources == null) {
throw new IllegalArgumentException("Property 'targetDataSources' is required");
} else {
this.resolvedDataSources = new HashMap(this.targetDataSources.size());
this.targetDataSources.forEach((key, value) -> {
Object lookupKey = this.resolveSpecifiedLookupKey(key);
DataSource dataSource = this.resolveSpecifiedDataSource(value);
this.resolvedDataSources.put(lookupKey, dataSource);
});
if (this.defaultTargetDataSource != null) {
this.resolvedDefaultDataSource = this.resolveSpecifiedDataSource(this.defaultTargetDataSource);
}
}
}
可以看到是将targetDataSources对象的内容赋值给了它。
5. 实现动态数据源的脉络
- 首先继承AbstractRoutingDataSource这个类
- 通过Override 这个类的方法和给这个类的属性赋值的形式,来动态的获取数据源
- 赋值父类的setTargetDataSources属性,将所有的数据源保存以key->value形式保存
- Override方法determineCurrentLookupKey(),告诉本次请求使用哪个key对应的数据源
二、实现springboot动态数据源代码主要的三个类
1. DynamicDataSource类
这个类Override父级的几个方法,和赋值父类的属性来实现动态数据源
package com.example.datasource;
import com.alibaba.druid.pool.DruidDataSource;
import com.example.holder.TenantHolder;
import com.example.pojo.Tenant;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;
import org.springframework.stereotype.Component;
import javax.sql.DataSource;
import java.sql.Connection;
import java.sql.SQLException;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* @Author wangy
* @create 2021/7/12 17:18
* @Description 集成AbstractRoutingDataSource实现动态数据源
*/
@Component
public class DynamicDataSource extends AbstractRoutingDataSource {
//默认数据源
private static DataSource defaultDataSource;
//保存所有的数据源
private static Map<Object,Object> targetDataSources = new HashMap<>();
static {
//初始化默认数据源
DruidDataSource source = new DruidDataSource();
source.setUrl("jdbc:mysql://localhost/defaultDB?useUnicode=true&characterEncoding=UTF-8");
source.setUsername("root");
source.setPassword("123456");
source.setDriverClassName("com.mysql.cj.jdbc.Driver");
source.setInitialSize(2);
source.setMinIdle(2);
source.setMaxActive(5);
defaultDataSource = source;
}
/**
* 获取数据库连接
* @return
* @throws SQLException
*/
@Override
public Connection getConnection() throws SQLException {
return super.getConnection();
}
/**
* 设置默认数据源
* @param defaultTargetDataSource 存储所有租户数据源信息的数据源
*/
@Override
public void setDefaultTargetDataSource(Object defaultTargetDataSource) {
super.setDefaultTargetDataSource(defaultDataSource);
}
@Override
public void afterPropertiesSet() {
//初始化所有租户的数据源
initTargetDataSources();
//一些参数设置操作
super.afterPropertiesSet();
}
/**
* 初始化所有租户的数据源
*/
public void initTargetDataSources(){
//得到所有动态数据源
JdbcTemplate jdbcTemplate = new JdbcTemplate(defaultDataSource);
List<Map<String, Object>> list =
jdbcTemplate.queryForList("select tenant_name, tenant_code, db_url, db_user, db_password, tenant_introduce from t_tenant;");
Tenant tenant = null;
for (Map<String, Object> map : list) {
tenant = new Tenant();
tenant.setTenantName((String) map.get("tenant_name"));
tenant.setTenantCode((String) map.get("tenant_code"));
tenant.setDbUrl((String) map.get("db_url"));
tenant.setDbUser((String) map.get("db_user"));
tenant.setDbPassword((String) map.get("db_password"));
tenant.setTenantIntroduce((String) map.get("tenant_introduce"));
//创建租户的数据源
DataSource tenantDataSource = createDataSourceByTTenant(tenant);
//放到容器中
targetDataSources.put(tenant.getTenantCode(), tenantDataSource);
}
//设置所有的数据源Map
super.setTargetDataSources(targetDataSources);
}
/**
* 每次sql请求会获取使用哪个数据源对应的key
* @return
*/
@Override
protected Object determineCurrentLookupKey() {
return TenantHolder.getTenantCode();
}
/**
* 每次sql请求会决定使用哪个数据源
* @return
*/
@Override
protected DataSource determineTargetDataSource() {
DataSource dataSource = null;
//如果未获取到
if (null == determineCurrentLookupKey()) {
dataSource = defaultDataSource;
}else {
dataSource = (DataSource) targetDataSources.get(determineCurrentLookupKey());
}
return dataSource;
}
/**
* 根据表里数据源 初始化话数据源
* @param tenant
* @return
*/
public DataSource createDataSourceByTTenant(Tenant tenant){
DruidDataSource source = new DruidDataSource();
source.setUrl(tenant.getDbUrl());
source.setUsername(tenant.getDbUser());
source.setPassword(tenant.getDbPassword());
source.setDriverClassName("com.mysql.cj.jdbc.Driver");
source.setInitialSize(2);
source.setMinIdle(1);
source.setMaxActive(3);
return source;
}
}
2. TenantHolder类
每次请求都会是一个线程,怎么得到当前线程是哪个租户请求的,通过ThreadLocal来保存租户请求的标识
public class TenantHolder {
//通过拦截器字符串截取 host.split("\\.")[0] 来保存当前租户的标识
private static ThreadLocal<String> tenantCode = new ThreadLocal<>();
public static String getTenantCode() {
return tenantCode.get();
}
public static void setTenantCode(String code) {
tenantCode.set(code);
}
public static void remove(){
tenantCode.remove();
}
}
3. 实现Filter类
这个类作用是截取各个租户独有的域名标识,然后保存到当前线程的TenantHolder类属性中,获取动态数据源的时候即可通过Override方法determineCurrentLookupKey()返回这个标识从而得到对应租户的数据源。
@Component
@WebFilter(urlPatterns = "/**")
public class TenantHolderFilter extends OncePerRequestFilter {
private static final Logger log = LoggerFactory.getLogger(TenantHolderFilter.class);
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws ServletException, IOException {
log.info("+++++++++++++请求路径:[=>{}<=]+++请求时间:[{}]++++++++++++++++++++++++", request.getRequestURL(), LocalDateTime.now());
String host = request.getHeader("Host");
log.info("处理请求的线程是:[{}],host是:[{}]", Thread.currentThread().getName(), host);
String tenantCode = host.split("\\.")[0];
//设置tenant
TenantHolder.setTenantCode(tenantCode);
try {
chain.doFilter(request, response);
} catch (Exception e) {
//异常处理
log.info("{}", e.getMessage());
} finally {
//移除tenant
TenantHolder.remove();
}
}
}
三、实现效果演示
请求接口:http://zhangsan.localhost:8081/system/getSystemInfo
返回:
{
“id”: 1,
“keyName”: “张三的独立数据库”,
“keyValue”: “这是张三租户系统的数据源”,
“addTime”: “2021-07-12T15:58:14”,
“updateTime”: “2021-07-12T15:58:16”,
“deleted”: false
}
请求接口:http://lisi.localhost:8081/system/getSystemInfo
返回:
{
“id”: 1,
“keyName”: “这是李四的独立数据库”,
“keyValue”: “这是李四的租户系统的数据源”,
“addTime”: “2021-07-16T14:40:16”,
“updateTime”: “2021-07-16T14:40:19”,
“deleted”: false
}
四、本项目源码参考
1.本项目源码
说一千道一万,有个源码最划算,下面试本项目的源码,欢迎大家指正。
https://gitee.com/wangyue123com_admin/dynamic-datasource.git
2.参考
其他参考链接:
https://www.jianshu.com/p/edc282e6bbb9
某位前辈的视频原理解析:
https://www.bilibili.com/video/BV13y4y1g7zU?from=search&seid=5514249341295337338