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不适用,要修改代码)

posted on 2022-10-11 11:42  Gu  阅读(1035)  评论(0编辑  收藏  举报