【Mybatis技术专题】「夯实基本功系列」MybatisPlus自带强大功能之多租户插件实现原理和实战分析

MybatisPlus自带强大功能之多租户插件实现原理和实战分析

前提介绍

一般的程序应用当使用者访问不同,并且进入相对应的程序页面,则会把用户相关数据传输到后台这里。在传输的时候需要带上标识(租户ID),以便程序将数据进行隔离。当不同的租户使用同一个程序服务,这里就需要考虑一个数据隔离的情况。

什么是多租户技术

  • 多租户技术或称多重租赁技术,是一种软件架构技术,是实现如何在多用户环境下(此处的多用户一般是面向企业)共用相同的系统或程序组件,并且确保各用户间数据隔离性。

  • 在一台服务器上运行单个应用实例,它为多个租户(客户)提供服务。从定义中我们可以理解:多租户是一种架构,目的是为了让多用户环境下使用同一套程序,且保证用户间数据隔离。多租户的重点就是同程序下实现多用户数据的隔离。

数据隔离有三种方案:

  1. 独立数据库:简单来说就是一个租户使用一个数据库,这种数据隔离级别最高,安全性最好,但是提高成本。

  2. 共享数据库、隔离数据架构:多租户使用同一个数据裤,但是每个租户对应一个Schema(数据库user)。

  3. 共享数据库、共享数据架构:使用同一个数据库,同一个Schema,但是在表中增加了租户ID的字段,这种共享数据程度最高,隔离级别最低。

多租户具体实现

按照综合实际考虑,一般都会采用方案三,即共享数据库,共享数据架构,因为这种方案服务器成本最低,但是提高了开发成本。

MybatisPlus实现逻辑
Mybatis-plus实现多租户方案

Mybatis-plus就提供了一种多租户的解决方案,实现方式是基于分页插件(拦截器)进行实现的;

  • 第一步:应用添加维护一张tenant(租户表),需要进行隔离的数据表上新增租户id,例如,现在有数据库表(user)如下:
字段名 字段类型 描述
id Long 主键
tenantId Long 租户编码
other varchar(256) 其他属性

将tenantId视为租户ID,用来隔离租户与租户之间的数据,如果要查询当前服务商的用户,SQL大致如下:

sql 复制代码
SELECT * FROM table t WHERE  t.tenantId = 1;
  • 第二步:实现TenantHandler接口并实现它的方法:
java 复制代码
public interface TenantHandler {
  /**
   * 获取租户 ID 值表达式,支持多个 ID 条件查询
   * 支持自定义表达式,比如:tenant_id in (1,2) @since 2019-8-2
   * @param where 参数 true 表示为 where 条件 false 表示为 insert 或者 select 条件
   * @return 租户 ID 值表达式
   */
  Expression getTenantId(boolean where);

  /**
   * 获取租户字段名
   * @return 租户字段名
   */
  String getTenantIdColumn();

  /**
   * 根据表名判断是否进行过滤
   * @param tableName 表名
   * @return 是否进行过滤, true:表示忽略,false:需要解析多租户字段
   */
  boolean doTableFilter(String tableName);
}
PreTenantHandler实现TenantHandler
java 复制代码
@Slf4j
@Component
public class PreTenantHandler implements TenantHandler {

  @Autowired
  private PreTenantConfigProperties configProperties;
  /**
   * 租户Id
   * @return
   */
  @Override
  public Expression getTenantId(boolean where) {
    //可以通过过滤器从请求中获取对应租户id 
    Long tenantId = PreTenantContextHolder.getCurrentTenantId();
    log.debug("当前租户为{}", tenantId);
    if (tenantId == null) {
      return new NullValue();
    }
    return new LongValue(tenantId);
  }

  /**
   * 租户字段名
   * @return
   */
  @Override
  public String getTenantIdColumn() {
    return configProperties.getTenantIdColumn();
  }

  /**
   * 根据表名判断是否进行过滤
   * 忽略掉一些表:如租户表(sys_tenant)本身不需要执行这样的处理
   * @param tableName
   * @return
   */
  @Override
  public boolean doTableFilter(String tableName) {
    return configProperties.getIgnoreTenantTables().stream().anyMatch((e) -> e.equalsIgnoreCase(tableName));
  }
}
  • 第三步:配置mybatisPlus的分页插件配置

租户相关的表,我们都需要不厌其烦的加上AND t.tenantId = ?查询条件,稍不注意就会导致数据越界,数据安全问题让人担忧。好在有了MybatisPlus这个神器,可以极为方便的实现多租户SQL解析器。

核心配置:TenantSqlParser
复制代码
@Configuration
@MapperScan("com.wuwenze.mybatisplusmultitenancy.mapper")
public class MybatisPlusConfig {

    private static final String SYSTEM_TENANT_ID = "provider_id";
    private static final List<String> IGNORE_TENANT_TABLES = Lists.newArrayList("provider");

    @Autowired
    private ApiContext apiContext;

    @Bean
    public PaginationInterceptor paginationInterceptor() {
        PaginationInterceptor paginationInterceptor = new PaginationInterceptor();
        // SQL解析处理拦截:增加租户处理回调。
        TenantSqlParser tenantSqlParser = new TenantSqlParser()
                .setTenantHandler(new TenantHandler() {
                    @Override
                    public Expression getTenantId() {
                     // 从当前系统上下文中取出当前请求的服务商ID,通过解析器注入到SQL中。
                        Long currentProviderId = apiContext.getCurrentProviderId();
                        if (null == currentProviderId) {
                            throw new RuntimeException("#1129 getCurrentProviderId error.");
                        }
                        return new LongValue(currentProviderId);
                    }
                    @Override
                    public String getTenantIdColumn() {
                        return SYSTEM_TENANT_ID;
                    }
                    @Override
                    public boolean doTableFilter(String tableName) {
                        // 忽略掉一些表:如租户表(provider)本身不需要执行这样的处理。
                        return IGNORE_TENANT_TABLES.stream().anyMatch((e) -> e.equalsIgnoreCase(tableName));
                    }
                });
        paginationInterceptor.setSqlParserList(Lists.newArrayList(tenantSqlParser));
        return paginationInterceptor;
    }
    @Bean(name = "performanceInterceptor")
    public PerformanceInterceptor performanceInterceptor() {
        return new PerformanceInterceptor();
    }
}

配置好之后,不管是查询、新增、修改删除方法,MP都会自动加上租户ID的标识,测试如下:

java 复制代码
@Test
public void select(){
  List<User> users = userMapper.selectList(Wrappers.<User>lambdaQuery().eq(User::getAge, 18));
  users.forEach(System.out::println);
}
运行sql实例:
复制代码
DEBUG==> Preparing: SELECT id, login_name, name, password, 
      email, salt, sex, age, phone, user_type, status,
     organization_id, create_time, update_time, version,
     tenant_id FROM sys_user 
   WHERE sys_user.tenant_id = '001' AND is_delete = '0' AND age = ?

注:特定SQL过滤,如果在程序中,有部分SQL不需要加上租户ID的表示,需要过滤特定的sql,可以通过如下两种方式:

在配置分页插件中加上配置ISqlParserFilter解析器,配置SQL很多,比较麻烦,不建议;

java 复制代码
paginationInterceptor.setSqlParserFilter(new ISqlParserFilter() {
      @Override
      public boolean doFilter(MetaObject metaObject) {
        MappedStatement ms = SqlParserHelper.getMappedStatement(metaObject);
        // 对应Mapper、dao中的方法
        if("com.example.demo.mapper.UserMapper.selectList".equals(ms.getId())){
          return true;
        }
        return false;
      }
});

通过租户注解 @SqlParser(filter = true) 的形式,目前只能作用于Mapper的方法上:

java 复制代码
public interface UserMapper extends BaseMapper<User> {
  
  /**
   * 自定Wrapper修改
   *
   * @param userWrapper 条件构造器
   * @param user    修改的对象参数
   * @return
   */
  @SqlParser(filter = true)
  int updateByMyWrapper(@Param(Constants.WRAPPER) Wrapper<User> userWrapper, @Param("user") User user);
}
ApiContext
java 复制代码
@Component
public class ApiContext {
    private static final String KEY_CURRENT_PROVIDER_ID = "KEY_CURRENT_PROVIDER_ID";
    private static final Map<String, Object> mContext = Maps.newConcurrentMap();

    public void setCurrentProviderId(Long providerId) {
        mContext.put(KEY_CURRENT_PROVIDER_ID, providerId);
    }

    public Long getCurrentProviderId() {
        return (Long) mContext.get(KEY_CURRENT_PROVIDER_ID);
    }
}

总结

MybatisPlus 是一款在 Mybatis 的基础上增加了许多便利功能的 ORM 框架。其中,多租户插件是 MybatisPlus 的一个重要特性,用于实现多租户的逻辑分离,使得不同的租户可以共享同一套数据库,但数据是隔离的。以下是 MybatisPlus 多租户插件的实现原理和实战分析:

实现原理

  • 多租户识别:插件通过拦截器机制,在 SQL 语句执行之前,自动识别出当前请求的租户 ID。
  • SQL 语句动态修改:通过动态 SQL 拼接,在原有 SQL 语句中加入租户 ID 的条件,实现数据的隔离。

性能影响

由于需要在 SQL 语句中动态加入租户 ID 的条件,可能会对性能产生一定影响。特别是在高并发的场景下,需要关注 SQL 语句的优化和缓存策略。

相关推荐
沸材17 分钟前
Redis——实现消息队列
数据库·redis·消息队列
しかし11811427 分钟前
C语言队列的实现
c语言·开发语言·数据结构·数据库·经验分享·链表
⁤⁢初遇1 小时前
MySQL---数据库基础
数据库
wolf犭良1 小时前
27、Python 数据库操作入门(SQLite)从基础到实战精讲
数据库·python·sqlite
画扇落汗1 小时前
Python 几种将数据插入到数据库的方法(单行插入、批量插入,SQL Server、MySQL,insert into)
数据库·python·sql·mysql
银河系的一束光1 小时前
mysql的下载和安装2025.4.8
数据库·mysql
Full Stack Developme1 小时前
SQL 查询中使用 IN 导致性能问题的解决方法
数据库·sql
magic 2452 小时前
MyBatis的缓存、逆向工程、使用PageHelper、使用PageHelper
java·spring·maven·mybatis
神经星星2 小时前
【vLLM 学习】API 客户端
数据库·人工智能·机器学习
小光学长3 小时前
基于flask+vue框架的助贫公益募捐管理系统1i6pi(程序+源码+数据库+调试部署+开发环境)带论文文档1万字以上,文末可获取,系统界面在最后面。
数据库