欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  IT编程

基于mybatis-plus实现的多租户架构

程序员文章站 2022-06-27 20:01:39
整体概述多租户技术或称多重租赁技术,简称SaaS,是一种软件架构技术,是实现如何在多用户环境下(多用户一般是面向企业用户)共用相同的系统或程序组件,并且可确保各用户间数据的隔离性。多租户在数据存储上主要存在三种方案,独立数据库、共享数据库,独立Schema、共享数据库,共享 Schema,共享数据表。独立数据库即一个租户一个数据库,这种方案的用户数据隔离级别最高,安全性最好,但成本较高。优点:为不同的租户提供独立的数据库,有助于简化数据模型的扩展设计,满足不同租户的独特需求;如果出现故....

整体概述

多租户技术或称多重租赁技术,简称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 操作智能分析阻断,也可自定义拦截规则,预防误操作
  • 多数据源切换数据源分组,适用于多种场景:纯粹多库、读写分离、一主多从、混合模式,提供对DruidMybatis-PlusP6syJndi的快速集成使用spel动态参数解析数据源,如从sessionheader或参数中获取数据源。

多租户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;
}

初始化数据源

基于mybatis-plus实现的多租户架构

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中处理对应租户数据。

整体架构图

基于mybatis-plus实现的多租户架构

注:以上架构来自狐小E智慧办公平台,企业数字化建设的全景攻略 https://www.hixiaoe.com

本文地址:https://blog.csdn.net/u011076500/article/details/108869045

相关标签: java spring