适用于多租户系统(SaaS)的数据隔离,核心是通过 SQL 自动注入租户 ID 条件。


一、核心概念

  1. 租户字段
    数据库表中标识租户的字段(如 tenant_id),用于数据隔离。
  2. 租户处理器(TenantHandler)
    动态提供当前租户 ID 和租户字段名。
  3. 租户插件(TenantLineInnerInterceptor)
    在 SQL 执行时自动追加租户条件(如 WHERE tenant_id = 1)。

二、详细操作步骤

1. 添加租户字段
  • 数据库表
    ALTER TABLE your_table ADD COLUMN tenant_id BIGINT NOT NULL DEFAULT 0 COMMENT '租户ID';
    
  • 实体类
    public class User {
        // 其他字段...
        private Long tenantId; // 字段名与数据库一致(tenant_id)
    }
    
2. 实现租户处理器
import com.baomidou.mybatisplus.extension.plugins.handler.TenantLineHandler;
import net.sf.jsqlparser.expression.Expression;
import net.sf.jsqlparser.expression.LongValue;

public class CustomTenantHandler implements TenantLineHandler {
    
    // 获取当前租户ID(从ThreadLocal、SecurityContext等获取)
    @Override
    public Expression getTenantId() {
        Long tenantId = 1L; // 实际项目中从上下文获取
        return new LongValue(tenantId);
    }

    // 租户字段名
    @Override
    public String getTenantIdColumn() {
        return "tenant_id";
    }

    // 忽略租户过滤的表(系统表、公共表)
    @Override
    public boolean ignoreTable(String tableName) {
        return "system_config".equalsIgnoreCase(tableName);
    }
}
3. 配置租户插件
import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.TenantLineInnerInterceptor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class MybatisPlusConfig {

    @Bean
    public MybatisPlusInterceptor mybatisPlusInterceptor() {
        MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
        // 添加租户插件
        interceptor.addInnerInterceptor(new TenantLineInnerInterceptor(new CustomTenantHandler()));
        return interceptor;
    }
}
4. 自动填充租户ID(插入数据时)
import com.baomidou.mybatisplus.core.handlers.MetaObjectHandler;
import org.apache.ibatis.reflection.MetaObject;
import org.springframework.stereotype.Component;

@Component
public class TenantMetaObjectHandler implements MetaObjectHandler {
    
    @Override
    public void insertFill(MetaObject metaObject) {
        // 自动填充租户ID
        this.strictInsertFill(metaObject, "tenantId", Long.class, getCurrentTenantId());
    }

    private Long getCurrentTenantId() {
        return 1L; // 实际从上下文获取
    }
}

三、常见错误与解决

  1. SQL 未添加租户条件

    • 检查 ignoreTable() 是否误过滤表。
    • 确认实体类字段名与数据库一致(如 tenant_idtenantId)。
  2. 插入数据时租户ID为null

    • 确保 TenantMetaObjectHandler 被 Spring 管理。
    • 实体类字段添加 @TableField(fill = FieldFill.INSERT)
  3. 多表联查漏租户条件

    • 手动在 SQL 中追加:AND table1.tenant_id = #{tenantId}
    • 使用 @SqlParser(filter = true)(已废弃)或 @InterceptorIgnore(tenantLine = "true") 关闭租户过滤。

四、注意事项

  1. 敏感操作过滤
    UPDATE/DELETE 全表操作必须强制包含租户条件,避免误删其他租户数据。

  2. 租户ID来源
    从用户 Token 或 Session 动态获取(如 Spring Security 的 SecurityContextHolder)。

  3. 忽略特定SQL
    在 Mapper 方法上添加注解跳过租户过滤:

    @InterceptorIgnore(tenantLine = "true")
    @Select("SELECT * FROM system_config")
    List<Config> getGlobalConfig();
    

五、使用技巧

  1. 动态租户ID

    public class CustomTenantHandler implements TenantLineHandler {
        @Override
        public Expression getTenantId() {
            return new LongValue(TenantContext.getCurrentTenantId());
        }
    }
    
  2. 多租户字段
    重写 getTenantIdColumn() 返回动态字段名(需特殊设计表结构)。

  3. 租户ID类型适配
    若租户ID为字符串,返回 new StringValue("tenant_001")


六、最佳实践与性能优化

  1. 索引优化
    所有租户字段必须加索引:

    ALTER TABLE your_table ADD INDEX idx_tenant_id (tenant_id);
    
  2. 分页查询优化

    • 避免 COUNT(1) 全表扫描,添加 tenant_id 条件。
    • 使用覆盖索引减少回表。
  3. 数据归档
    定期归档历史数据到独立表,减少主表数据量。

  4. 全局异常处理
    捕获 TenantLineException,统一返回权限错误。


七、完整流程图

graph TD
    A[发起SQL请求] --> B{租户插件拦截}
    B -->|解析SQL| C[调用TenantHandler]
    C -->|获取租户ID| D[动态追加WHERE条件]
    D -->|tenant_id = ?| E[执行修改后SQL]
    E --> F[返回结果]

通过以上步骤,可快速实现多租户数据隔离。关键点:

  1. 正确配置 TenantLineInnerInterceptorTenantHandler
  2. 插入数据时自动填充租户ID。
  3. 敏感 SQL 操作强制校验租户条件。