适用于多租户系统(SaaS)的数据隔离,核心是通过 SQL 自动注入租户 ID 条件。
一、核心概念
- 租户字段
数据库表中标识租户的字段(如tenant_id
),用于数据隔离。 - 租户处理器(TenantHandler)
动态提供当前租户 ID 和租户字段名。 - 租户插件(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; // 实际从上下文获取
}
}
三、常见错误与解决
SQL 未添加租户条件
- 检查
ignoreTable()
是否误过滤表。 - 确认实体类字段名与数据库一致(如
tenant_id
→tenantId
)。
- 检查
插入数据时租户ID为null
- 确保
TenantMetaObjectHandler
被 Spring 管理。 - 实体类字段添加
@TableField(fill = FieldFill.INSERT)
。
- 确保
多表联查漏租户条件
- 手动在 SQL 中追加:
AND table1.tenant_id = #{tenantId}
。 - 使用
@SqlParser(filter = true)
(已废弃)或@InterceptorIgnore(tenantLine = "true")
关闭租户过滤。
- 手动在 SQL 中追加:
四、注意事项
敏感操作过滤
UPDATE
/DELETE
全表操作必须强制包含租户条件,避免误删其他租户数据。租户ID来源
从用户 Token 或 Session 动态获取(如 Spring Security 的SecurityContextHolder
)。忽略特定SQL
在 Mapper 方法上添加注解跳过租户过滤:@InterceptorIgnore(tenantLine = "true") @Select("SELECT * FROM system_config") List<Config> getGlobalConfig();
五、使用技巧
动态租户ID
public class CustomTenantHandler implements TenantLineHandler { @Override public Expression getTenantId() { return new LongValue(TenantContext.getCurrentTenantId()); } }
多租户字段
重写getTenantIdColumn()
返回动态字段名(需特殊设计表结构)。租户ID类型适配
若租户ID为字符串,返回new StringValue("tenant_001")
。
六、最佳实践与性能优化
索引优化
所有租户字段必须加索引:ALTER TABLE your_table ADD INDEX idx_tenant_id (tenant_id);
分页查询优化
- 避免
COUNT(1)
全表扫描,添加tenant_id
条件。 - 使用覆盖索引减少回表。
- 避免
数据归档
定期归档历史数据到独立表,减少主表数据量。全局异常处理
捕获TenantLineException
,统一返回权限错误。
七、完整流程图
graph TD A[发起SQL请求] --> B{租户插件拦截} B -->|解析SQL| C[调用TenantHandler] C -->|获取租户ID| D[动态追加WHERE条件] D -->|tenant_id = ?| E[执行修改后SQL] E --> F[返回结果]
通过以上步骤,可快速实现多租户数据隔离。关键点:
- 正确配置
TenantLineInnerInterceptor
和TenantHandler
。 - 插入数据时自动填充租户ID。
- 敏感 SQL 操作强制校验租户条件。