基于mybatis-plus实现的多租户架构
整体概述
多租户技术或称多重租赁技术,简称SaaS,是一种软件架构技术,是实现如何在多用户环境下(多用户一般是面向企业用户)共用相同的系统或程序组件,并且可确保各用户间数据的隔离性。
多租户在数据存储上主要存在三种方案,独立数据库、共享数据库,独立Schema、共享数据库,共享 Schema,共享数据表。
独立数据库
即一个租户一个数据库,这种方案的用户数据隔离级别最高,安全性最好,但成本较高。
- 优点:为不同的租户提供独立的数据库,有助于简化数据模型的扩展设计,满足不同租户的独特需求;如果出现故障,恢复数据比较简单。
- 缺点:增多了数据库的安装数量,随之带来维护成本和购置成本的增加。
共享数据库,独立Schema、共享数据库
共同使用一个数据库,使用表进行数据隔离,多个或所有租户共享Database,但是每个租户一个Schema(也可叫做一个user)。底层库比如是DB2、ORACLE等,一个数据库下可以有多个SCHEMA。
- 优点:为安全性要求较高的租户提供了一定程度的逻辑数据隔离,并不是完全隔离;每个数据库可支持更多的租户数量。
- 缺点:如果出现故障,数据恢复比较困难,因为恢复数据库将牵涉到其他租户的数据;
方案选择
免费租户(体验租户)
免费租户或者体验租户,考虑到维护和购置成本,使用共享数据库,共享 Schema,共享数据表方案,通过tenant_id区分不同租户数据,实现数据隔离。
付费租户
收费租户为了获得更好的用户体验,结合MySql数据库,采用独立数据库方案,不同租户对应不同数据库,数据隔离级别最高,安全性最好。
租户升级
由免费租户(体验租户)升级到付费租户,不仅要为该租户创建独立数据库,还要同步历史数据,在不影响用户体验的情况,升级操作可以考虑异步定时处理。
技术选型
Mybatis-Plus简介
MyBatis-Plus(简称 MP)是一个 MyBatis 的增强工具,在 MyBatis 的基础上只做增强不做改变,为简化开发、提高效率而生。
- 无侵入:只做增强不做改变,引入它不会对现有工程产生影响,如丝般顺滑
- 损耗小:启动即会自动注入基本 CURD,性能基本无损耗,直接面向对象操作
- 强大的 CRUD 操作:内置通用 Mapper、通用 Service,仅仅通过少量配置即可实现单表大部分 CRUD 操作,更有强大的条件构造器,满足各类使用需求
- 支持 Lambda 形式调用:通过 Lambda 表达式,方便的编写各类查询条件,无需再担心字段写错
- 支持主键自动生成:支持多达 4 种主键策略(内含分布式唯一 ID 生成器 - Sequence),可*配置,完美解决主键问题
- 支持 ActiveRecord 模式:支持 ActiveRecord 形式调用,实体类只需继承 Model 类即可进行强大的 CRUD 操作
- 支持自定义全局通用操作:支持全局通用方法注入( Write once, use anywhere )
- 内置代码生成器:采用代码或者 Maven 插件可快速生成 Mapper 、 Model 、 Service 、 Controller 层代码,支持模板引擎,更有超多自定义配置等您来使用
- 内置分页插件:基于 MyBatis 物理分页,开发者无需关心具体操作,配置好插件之后,写分页等同于普通 List 查询
- 分页插件支持多种数据库:支持 MySQL、MariaDB、Oracle、DB2、H2、HSQL、SQLite、Postgre、SQLServer 等多种数据库
- 内置性能分析插件:可输出 Sql 语句以及其执行时间,建议开发测试时启用该功能,能快速揪出慢查询
- 内置全局拦截插件:提供全表 delete 、 update 操作智能分析阻断,也可自定义拦截规则,预防误操作
- 多数据源切换:数据源分组,适用于多种场景:纯粹多库、读写分离、一主多从、混合模式,提供对Druid,Mybatis-Plus,P6sy,Jndi的快速集成。使用spel动态参数解析数据源,如从session,header或参数中获取数据源。
多租户SQL解析器
通过PaginationInterceptor拦截特定sql,加上tenant_id。具体代码如下:
@Bean
public PaginationInterceptor paginationInterceptor() {
PaginationInterceptor paginationInterceptor = new PaginationInterceptor();
// SQL解析处理拦截:增加租户处理回调。
List<ISqlParser> sqlParserList = new ArrayList<>();
TenantSqlParser tenantSqlParser = new TenantSqlParser();
tenantSqlParser.setTenantHandler(new TenantHandler() {
@Override
public Expression getTenantId(boolean where) {
HttpServletRequest request = null;
// 从当前系统上下文中取出当前请求的服务商ID,通过解析器注入到SQL中。
if (null != RequestContextHolder.getRequestAttributes()) {
request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
}
Long currentTenantId;
try {
String tenantId;
if (null == request) {
tenantId = TenantIdContext.peek();
} else {
tenantId = request.getHeader("tenantId");
if (StringUtils.isBlank(tenantId)) {
HttpSession session = request.getSession();
tenantId = session.getAttribute(tenantId) == null ? null : session.getAttribute(tenantId).toString();
}
if (StringUtils.isBlank(tenantId)) {
tenantId = TenantIdContext.peek();
}
}
currentTenantId = Long.valueOf(tenantId);
} catch (Exception e) {
throw new RuntimeException("getTenantId error.");
}
return new LongValue(currentTenantId);
}
@Override
public String getTenantIdColumn() {
return SYSTEM_TENANT_ID;
}
@Override
public boolean doTableFilter(String tableName) {
// 这里可以判断是否过滤表
return false;
}
});
sqlParserList.add(tenantSqlParser);
paginationInterceptor.setSqlParserList(sqlParserList);
paginationInterceptor.setSqlParserFilter(new ISqlParserFilter() {
@Override
public boolean doFilter(MetaObject metaObject) {
/**
* 如果在程序中,有部分SQL不需要加上租户ID的表示,需要过滤特定的sql
*/
MappedStatement ms = SqlParserHelper.getMappedStatement(metaObject);
String msId = ms.getId();
// 不是meeting下面的sql都不需要加租户ID
if (msId.startsWith("com.hixiaoe.meeting.persistence.meeting.dao")) {
return false;
}
return true;
}
});
return paginationInterceptor;
}
初始化数据源
DynamicRoutingDataSource继承了AbstractRoutingDataSource抽象类,实现了InitializingBean, DisposableBean接口,内部维护了一个LinkedHashMap,用于存放当前所有数据源。通过DynamicDataSourceCreator创建未初始化的数据源,并添加数据源到当前数据源map中,以供动态切换使用,具体实现如下:
public static void addDataSource(String dsName, String username, String password, String driverName, String url) {
DataSourceProperty dataSourceProperty = new DataSourceProperty();
dataSourceProperty.setPollName(dsName);
dataSourceProperty.setDriverClassName(driverName);
dataSourceProperty.setUrl(url);
dataSourceProperty.setUsername(username);
dataSourceProperty.setPassword(password);
DruidConfig druidConfig = new DruidConfig();
druidConfig.setInitialSize(1);
dataSourceProperty.setDruid(druidConfig);
Map<String, DataSource> currentDataSources = dsUtil.dynamicRoutingDataSource.getCurrentDataSources();
if (!currentDataSources.containsKey(dsName)) {
DataSource ds = dsUtil.dynamicDataSourceCreator.createDruidDataSource(dataSourceProperty);
dsUtil.dynamicRoutingDataSource.addDataSource(dsName, ds);
}
}
数据源切换
使用 @DS ("dsName")切换数据源,@DS 可以注解在方法上和类上,同时存在,方法注解优先于类上注解。强烈建议只注解在service实现上,没有@DS,则采用默认数据源。dsName可以为组名也可以为具体某个库的名称(可以从session,header或参数中获取数据源)。
存在会话
http请求header中,或者httpsession设置tenantId,通过sql解析器在特定sql中处理对应租户数据。
异步线程
通过ThreadLocal设置当前线程tenantId,以供sql解析器在特定sql中处理对应租户数据。
整体架构图
注:以上架构来自狐小E智慧办公平台,企业数字化建设的全景攻略 https://www.hixiaoe.com
本文地址:https://blog.csdn.net/u011076500/article/details/108869045
推荐阅读
-
MybatisPlus 多租户架构(Multi-tenancy)实现详解
-
MybatisPlus 多租户架构(Multi-tenancy)实现详解
-
基于mybatis高级映射多对多查询的实现
-
基于C#实现的三层架构实例
-
Yii2 基于RESTful架构的 advanced版API接口开发 配置、实现、测试,yii2restful
-
基于.NET实现的DDD、CQRS与微服务架构的演示案例
-
基于WebUploader实现单图片和多图片上传,上传回显,编辑加载,图片删除,位置切换以及基于PhotoSwipe框架的图片预览功能
-
基于matlab对比度和结构提取的多模态解剖图像融合实现
-
基于C#实现的多生产者多消费者同步问题实例
-
NopCommerce架构分析之(四)基于路由实现灵活的插件机制