Springboot+MyBatis-Plus实现多租户动态数据源模式
Spring DataSource 的工作原理
在说明动态切换数据源之前,我们需要先了解一下 spring 在单数据源情况下是如何工作的。我们先说一下什么是 DataSource? 有什么用呢?
请看 DataSource 接口定义:
package javax.sql;
public interface DataSource extends CommonDataSource, Wrapper {
Connection getConnection() throws SQLException;
Connection getConnection(String username, String password)
throws SQLException;
}
聪明的你肯定一下就明白了,原来 DataSource 就是一个获取数据库 connection 的工厂类。然后我们还发现它的包名是 javax.sql,也就是说它是一个标准。常见的 C3P0、DBCP、Hikari、Druid 等等数据库连接池都实现了这个接口。
ok, 数据源我们现在弄明白了,那数据源是如何被 spring 使用的呢?以我们现在用的最广泛的 springboot 为例,我们在 application.properties 中配置了数据库连接信息后,mybatis,spring-data-jpa 等等 orm 框架就可以直接工作了,why?
其实原理很简单,我猜你也想到了。spring 在初始化系统的过程中读取 application.properties 中的数据库配置信息,然后实例化一个 DataSource bean 对象,mybatis、spring-data-jpa 等想要操作数据库时获取这个 DataSource 对象,然后调用其 getConnection () 方法获得数据库连接,然后操作数据库。
AbstractRoutingDataSource 工作原理
我们搞明白了 DataSource 工作原理,那么 AbstractRoutingDataSource 又是如何工作的呢?
我们先抛开 spring 的设计,一起思考一下。由上面的 DataSource 原理我们知道,一个 DataSource 代表一个数据库。那么我们要实现切换数据库,只要每次执行 sql 之前,从不同的数据源获得连接就可以了。换言之,我们需要实例化多个不同的数据源,然后每次使用的时候取不同的数据源来用。
ok,进入正题,看看 spring 是如何设计的。
AbstractRoutingDataSource 是 spring 提供的一个抽象类,为了看的清楚,我们先看一下唯一一个需要被实现的方法:
public abstract class AbstractRoutingDataSource extends AbstractDataSource implements InitializingBean {
@Nullable
protected abstract Object determineCurrentLookupKey();
}
这个方法没有参数,并且返回一个 Object 值,这个值使干嘛的呢?我们暂且放一放,继续往下看(真源码)。(为了大家看的清晰,我删掉了一些无关紧要的内容,保留了主要逻辑)
public abstract class AbstractRoutingDataSource extends AbstractDataSource implements InitializingBean {
@Nullable
private Map<Object, Object> targetDataSources;
@Nullable
private Object defaultTargetDataSource;
@Nullable
private Map<Object, DataSource> resolvedDataSources;
@Nullable
private DataSource resolvedDefaultDataSource;
//设置需要切换的所有数据源(除了默认数据源)
public void setTargetDataSources(Map<Object, Object> targetDataSources) {
this.targetDataSources = targetDataSources;
}
//默认数据源
public void setDefaultTargetDataSource(Object defaultTargetDataSource) {
this.defaultTargetDataSource = defaultTargetDataSource;
}
public void afterPropertiesSet() {
this.resolvedDataSources = new HashMap(this.targetDataSources.size());
this.targetDataSources.forEach((lookupKey, dataSource) -> {
this.resolvedDataSources.put(lookupKey, dataSource);
});
if (this.defaultTargetDataSource != null) {
this.resolvedDefaultDataSource = defaultTargetDataSource;
}
}
protected DataSource determineTargetDataSource() {
Object lookupKey = this.determineCurrentLookupKey();
DataSource dataSource = (DataSource)this.resolvedDataSources.get(lookupKey);
if (dataSource == null) {
dataSource = this.resolvedDefaultDataSource;
}
return dataSource;
}
@Nullable
protected abstract Object determineCurrentLookupKey();
}
接下来我们来模拟一下 AbstractRoutingDataSource 的使用过程,并说明上面代码的工作原理。
class Demo {
public static void main(String args[]){
//初始化动态数据源
DemoDynamicDataSource dds = new DemoDynamicDataSource();
dds.setDefaultTargetDataSource(new DataSource());
HashMap<String,DataSource> targetDataSources = new HashMap<>();
targetDataSources.put("1",new DataSource());
targetDataSources.put("2",new DataSource());
dds.setTargetDataSources(targetDataSources);
//spring会在bean初始化最后调用实现了InitializingBean接口bean的afterPropertiesSet()
dds.afterPropertiesSet();
//使用动态数据源
DataSource datasource = dds.determineTargetDataSource();
datasource.getConnection().execute("select xx");
}
}
初始化过程的核心在:afterPropertiesSet (); 代码很简单,将 defaultDataSource 存起来,将 TargetDataSources 转存到一个 map 里。
而使用的核心在:dds.determineTargetDataSource (), 我们看到,首先调用了我们需要实现的 determineCurrentLookupKey () 方法,然后通过获取到的 key 到上一步初始化的 targetDataSource 中取对应的 datasource (取不到就使用默认的 defaultDataSource),然后返回 datasource。
ok,我们现在明白了,原来我们可以通过 determineCurrentLookupKey () 方法的返回值来控制我们使用哪个数据源。
一、先实现动态数据源上下文模式代码,保证在多租户模式下,能自动根据租户Id切换数据源
二、实现动态数据源添加和设置,并继承自AbstractRoutingDataSource类,实现其determineTargetDataSource和determineCurrentLookupKey方法
三、实现动态数据源切面拦截,并根据租户Id实现数据源的动态切换
四、实现动态数据源初始化,并将租户信息表中的数据库链接等查询出来一并初始化
五、配置Mybatis
六、租户表相关建表语句: 在默认数据库比如Test里建立租户表tenant_info,每个租户在mysql加一个用户,比如user123,user456, 每个用户建立一个同名的数据库
GRANT ALL PRIVILEGES ON user123.* to 'user123'@'%';
CREATE TABLE `tenant_info` ( `id` bigint(20) NOT NULL AUTO_INCREMENT, `TENANT_ID` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT NULL COMMENT '租户id', `TENANT_NAME` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT NULL COMMENT '租户名称', `DATASOURCE_URL` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT NULL COMMENT '数据源url', `DATASOURCE_USERNAME` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT NULL COMMENT '数据源用户名', `DATASOURCE_PASSWORD` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT NULL COMMENT '数据源密码', `DATASOURCE_DRIVER` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT NULL COMMENT '数据源驱动', `SYSTEM_ACCOUNT` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT NULL COMMENT '系统账号', `SYSTEM_PASSWORD` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT NULL COMMENT '账号密码', `SYSTEM_PROJECT` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT NULL COMMENT '系统PROJECT', `STATUS` tinyint(1) DEFAULT NULL COMMENT '是否启用(1是0否)', `CREATE_TIME` datetime DEFAULT NULL COMMENT '创建时间', `UPDATE_TIME` datetime DEFAULT NULL COMMENT '更新时间', PRIMARY KEY (`id`) USING BTREE ) ENGINE=InnoDB AUTO_INCREMENT=13 DEFAULT CHARSET=utf8 ROW_FORMAT=DYNAMIC; SET FOREIGN_KEY_CHECKS = 1;
CREATE TABLE `t_user` ( `id` int NOT NULL AUTO_INCREMENT, `Operator` varchar(255) DEFAULT NULL, `Status` varchar(255) DEFAULT NULL, `Id_Card` varchar(255) DEFAULT NULL, `Sex` varchar(255) DEFAULT NULL, `Dept_Id` varchar(255) DEFAULT NULL, `Birthday` datetime DEFAULT NULL, `Update_Time` datetime DEFAULT NULL, `Is_Admin` varchar(255) DEFAULT NULL, `User_Name` varchar(255) DEFAULT NULL, `E_Mail` varchar(255) DEFAULT NULL, `Real_Name` varchar(255) DEFAULT NULL, `Tel_Phone` varchar(255) DEFAULT NULL, `Entry_Time` datetime DEFAULT NULL, `Login_Time` datetime DEFAULT NULL, `Expire_Time` datetime DEFAULT NULL, `Position_Id` varchar(255) DEFAULT NULL, `Create_Time` datetime DEFAULT NULL, `Resignation_Time` datetime DEFAULT NULL, `Expire_Status` datetime DEFAULT NULL, `Password` varchar(255) DEFAULT NULL, PRIMARY KEY (`id`) ) ENGINE=InnoDB AUTO_INCREMENT=6
实现源码地址https://github.com/achievejia/springboot_saas (针对mybatis plus 3.2,最新的3.5.2不适用,要修改代码)
浙公网安备 33010602011771号