SaaS化多租户实现的两种方法
SaaS系统的定义 SaaS,全称为Software-as-a-Service(软件即服务),是一种基于云计算的软件交付模式。而SaaS系统,即是通过这种模式提供给用户的软件系统。即多租户系统,每个租户独立,只能看到自己数据。
一、租户id隔离
这种方法比较简单,在每张表里添加一个字段tenant_id,给每个企业(租户)一个唯一tenant_id,那么在SQL的一切增删改查都带上tenant_id,即可实现租户隔离。
如何自动带上租户id,无需每次都在sql上添加tenant_id?
使用mybatis-plugin可以做到
步骤一、写一个拦截器
@Intercepts({
@Signature(type = StatementHandler.class, method = "prepare", args = {Connection.class,
Integer.class})
})
public class CustomerInterceptor implements Interceptor {
@Override
public Object intercept(Invocation invocation) throws Throwable {
//todo 拦截逻辑
System.out.println("");
StatementHandler statementHandler = (StatementHandler) invocation.getTarget();
String originalSql = statementHandler.getBoundSql().getSql();
//实际开发中从登录用户去获取他的tenant_id
String modifiedSql = originalSql + " AND tenant_id = '" + tenant_id + "'";
ReflectUtil.setFieldValue(statementHandler.getBoundSql(), "sql", modifiedSql);
return invocation.proceed();
}
@Override
public Object plugin(Object target) {
return Plugin.wrap(target,this);
}
@Override
public void setProperties(Properties properties) {
//设置属性
}
}
步骤二、注册插件
@Configuration
public class MybatisConfig {
@Bean
public String myInterceptor(SqlSessionFactory sqlSessionFactory) {
sqlSessionFactory.getConfiguration().addInterceptor(new CustomerInterceptor());
return "interceptor";
}
}
二、动态数据源(重点)
本文重点要讲的是使用动态数据源实现动态切换数据库,来实现多租户自由切换
本文使用的是mybatis-flex
步骤一、注册租户和数据源到数据库
即,把租户的唯一信息和分配给租户的数据源一一对应,存入数据库,例如:
CREATE TABLE `datasource` (
`id` bigint NOT NULL AUTO_INCREMENT,
`display_name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '显示名称',
`db_name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '链接默认数据库',
`schema_name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '数据库schema',
`pool_name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '连接池名称必须唯一',
`description` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '描述',
`db_host` varchar(25) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '数据库地址',
`db_port` int NOT NULL COMMENT '数据库端口',
`db_user` varchar(25) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '用户',
`db_password` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '密码',
`db_driver` varchar(25) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '驱动',
`connect_params` text CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci COMMENT '连接参数',
`create_time` timestamp NULL DEFAULT NULL,
`update_time` timestamp NULL DEFAULT NULL,
`create_by` timestamp NULL DEFAULT NULL,
`update_by` timestamp NULL DEFAULT NULL,
PRIMARY KEY (`id`) USING BTREE,
UNIQUE KEY `datasource_pool_name_uindex` (`pool_name`) USING BTREE,
KEY `database_creator_id_index` (`user_id`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=159 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci ROW_FORMAT=DYNAMIC;
示例数据:
步骤二、程序启动完成把数据连接信息加载到JVM
@Component
@Order(1)
public class InitialDataSource implements CommandLineRunner {
public static final String DATASOURCE_MYSQL_COMMON_PARAMS_URL = "jdbc:%s://%s:%s/%s?%s";
public static final String DATASOURCE_PGSQL_COMMON_PARAMS_URL = "jdbc:%s://%s:%s/%s?%s&%s";
//这是数据源(步骤1提到的)表的mapper接口
@Resource
private DatasourceMapper datasourceMapper;
@Override
public void run(String... args) throws Exception {
//1.清空内存中的数据源
DataSourceKey.clear();
//2.把数据库的datasource查询出来
List<Datasource> fillSubmittals = datasourceMapper.selectAll();
//3.动态添加新的数据源 FlexDataSource来自于mybatis-flex
FlexDataSource flexDataSource = FlexGlobalConfig.getDefaultConfig().getDataSource();
fillSubmittals.forEach(item -> addDatasourceItem(flexDataSource, item));
}
/**
* 组装-添加数据源
*
* @param flexDataSource
* @param item
*/
public void addDatasourceItem(FlexDataSource flexDataSource, Datasource item) {
DruidDataSource druidDataSource = buildDruidDataSource(item);
//数据源信息加载到内存
addIntoJVMDynamicPool(flexDataSource, item.getPoolName(),druidDataSource);
}
/**
* 数据源信息加载到内存
* @param flexDataSource
* @param poolName
* @param druidDataSource
* @return
*/
public void addIntoJVMDynamicPool(FlexDataSource flexDataSource, String poolName, DruidDataSource druidDataSource){
flexDataSource.addDataSource(poolName,druidDataSource);
}
public DruidDataSource buildDruidDataSource(Datasource item){
DruidDataSource druidDataSource = new DruidDataSource();
druidDataSource.setUrl(getCommonUrl(item));
druidDataSource.setDriverClassName(DriverEnum.findByEnumDescription(item.getDbDriver()).getDriverClass());
druidDataSource.setUsername(item.getDbUser());
druidDataSource.setPassword(item.getDbPassword());
druidDataSource.setValidationQuery("select 1");
return druidDataSource;
}
public String getCommonUrl(Datasource datasource){
String url = null;
if(DriverEnum.MYSQL.getDescription().equals(datasource.getDbDriver())){
url = String.format(DATASOURCE_MYSQL_COMMON_PARAMS_URL,
datasource.getDbDriver(),
datasource.getDbHost(),
datasource.getDbPort(),
StringUtils.hasText(datasource.getDbName()) ? datasource.getDbName() : "",
StringUtils.hasText(datasource.getConnectParams()) ? datasource.getConnectParams() : "");
}else if(DriverEnum.POSTGRES.getDescription().equals(datasource.getDbDriver())){
url =String.format(DATASOURCE_PGSQL_COMMON_PARAMS_URL,
datasource.getDbDriver(),
datasource.getDbHost(),
datasource.getDbPort(),
StringUtils.hasText(datasource.getDbName()) ? datasource.getDbName() : "",
StringUtils.hasText(datasource.getSchemaName()) ? "currentSchema="+datasource.getSchemaName() : "",
StringUtils.hasText(datasource.getConnectParams()) ? datasource.getConnectParams() : "");
}
return url;
}
}
用到的枚举:
@Getter
public enum DriverEnum {
/**
* pg
*/
POSTGRES(0, "postgresql", "org.postgresql.Driver", DbType.postgresql, "postgres"),
/**
* mysql
*/
MYSQL(1, "mysql", "com.mysql.cj.jdbc.Driver", DbType.mysql,"mysql"),
/**
* ck
*/
CLICK_HOUSE(2, "clickhouse", "com.clickhouse.jdbc.ClickHouseDriver", DbType.clickhouse, "clickhouse");
private final int index;
// bi 记录驱动
private final String description;
private final String driverClass;
// metabase 记录驱动
private final String engine;
private final DbType analysisType;
DriverEnum(int index, String description, String driverClass, DbType analysisType, String engine){
this.index = index;
this.description = description;
this.driverClass = driverClass;
this.analysisType = analysisType;
this.engine = engine;
}
private static final Map<String, DriverEnum> DESCRIPTION_ENUMS_MAP = Map.of(
DriverEnum.POSTGRES.getDescription(), DriverEnum.POSTGRES,
DriverEnum.MYSQL.getDescription(), DriverEnum.MYSQL,
DriverEnum.CLICK_HOUSE.getDescription(), DriverEnum.CLICK_HOUSE
);
private static final Map<String, DbType> ANALYSISTYPE_ENUMS_MAP = Map.of(
DriverEnum.POSTGRES.getDescription(), DbType.postgresql,
DriverEnum.MYSQL.getDescription(), DbType.mysql,
DriverEnum.CLICK_HOUSE.getDescription(), DbType.clickhouse
);
/**
* 判断参数合法性
*/
public static boolean isValidName(String name) {
for (DriverEnum cardStatus : DriverEnum.values()) {
if (cardStatus.getDescription().equals(name)) {
return true;
}
}
return false;
}
/**
* 根据描述查找枚举
* @param description 描述
* @return 枚举
*/
public static DriverEnum findByEnumDescription(String description){
return DESCRIPTION_ENUMS_MAP.getOrDefault(description, DriverEnum.POSTGRES);
}
/**
* 根据描述查找SQL解析器
* @param description 描述
* @return 枚举
*/
public static DbType findAnalysisTypeByDescription(String description){
return ANALYSISTYPE_ENUMS_MAP.getOrDefault(description, DbType.postgresql);
}
}
步骤三、业务使用(只列出核心)
@Resource
private JdbcTemplate jdbcTemplate;
public List<FillSubmittal> queryAll() {
String sql1 = "select * from datasource";
//设置数据库pool-name 与步骤一的表里的pool_name对应
DataSourceKey.use("c-1");
executeSql(sql1);
String sql2 = "select * from nc_fill_table_24_4lg0aa20f4rw9r";
DataSourceKey.use("c-2");
executeSql(sql2);
String sql3 = "select * from user_info";
DataSourceKey.use("c-3");
executeSql(sql3);
String sql4 = "select * from sys_role";
DataSourceKey.use("c-4");
executeSql(sql4);
return null;
}
public void executeSql(String sql){
List<Map<String, Object>> list =jdbcTemplate.queryForList(sql);
log.info(JSON.toJSONString(list));
}
这样,每个租户注册的时候就分配一个数据源,在使用时,根据租户灯笼裤信息获取到他对应的数据源信息,就可以通过DataSourceKey.use设置当前租户要用的数据源,实现动态切换
如图: 这里只画了程序启动,把数据加载到JVM中,和使用时,根据不用的pool_name(与租户一一对应)切换到对应的数据源。那么执行sql得到的就是对应数据源的数据,注意:这里执行sql使用的是jdbcTemplate了