Spring Boot + MyBatis-Plus 插件(多租户架构实战)

Spring Boot + MyBatis-Plus 多租户

一、多租户架构概述

多租户(Multi-Tenancy)是 SaaS(软件即服务)模式的核心技术,旨在通过单一应用实例为多个租户提供服务,同时保证数据隔离。其实现方式主要分为三种:

  1. 独立数据库:每个租户拥有独立数据库,隔离性最强但成本高。
  2. 共享数据库独立 Schema:共享数据库实例但逻辑分离(如 PostgreSQL 的 Schema),平衡安全性与成本。
  3. 共享数据库共享表:通过 tenant_id 字段区分数据,成本最低但需依赖应用层过滤。

二、字段隔离模式(共享表)

1. 核心原理

在每张表中添加 tenant_id 字段,通过 MyBatis-Plus 的 TenantLineInnerInterceptor 插件自动注入租户条件。所有 SQL 操作自动附加 tenant_id = ? 过滤条件,实现数据隔离。

2. 实现步骤

(1) 添加依赖
<dependency>
    <groupId>com.baomidougroupId>
    <artifactId>mybatis-plus-boot-starterartifactId>
    <version>3.5.3.1version>
dependency>
(2) 定义租户上下文(注意使用TransmittableThreadLocal)

注意使用 @Async 执行异步任务时,由于异步任务运行在新线程或线程池线程中,ThreadLocal 变量的值无法自动传递到子线程,导致获取到的值为 null。阿里巴巴开源的 TransmittableThreadLocal 支持线程池场景下的上下文传递。

(2.1) 整合TransmittableThreadLocal,实现异步传递
  • 引入依赖:
<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>transmittable-thread-local</artifactId>
    <version>2.14.2</version>
</dependency>
  • TTL兼容的异步线程池,通过 ThreadPoolTaskExecutor 结合 Executors 装饰线程池:
@Configuration
@EnableAsync
public class AsyncConfig implements AsyncConfigurer {

    @Override
    public Executor getAsyncExecutor() {
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        executor.setCorePoolSize(4);
        executor.setMaxPoolSize(8);
        executor.setQueueCapacity(100);
        executor.setThreadNamePrefix("ttl-async-");
        executor.initialize();

        // 使用 TTL 装饰线程池
        return TtlExecutors.getTtlExecutor(executor.getThreadPoolExecutor());
    }
}
(2.2) 定义租户上下文
public class TenantContext {
    private static final TransmittableThreadLocal<String> CURRENT_TENANT= new TransmittableThreadLocal();

    public static void setTenantId(String tenantId) {
        CURRENT_TENANT.set(tenantId);
    }

    public static String getTenantId() {
        return CURRENT_TENANT.get();
    }

    public static void clear() {
        CURRENT_TENANT.remove();
    }
}
(3) 配置拦截器获取租户 ID
@Configuration
public class WebConfig implements WebMvcConfigurer {
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(new HandlerInterceptor() {
            @Override
            public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
                String tenantId = request.getHeader("X-Tenant-ID");
                TenantContext.setTenantId(tenantId);
                return true;
            }
        });
    }
}
(4) 配置 MyBatis-Plus 插件
@Configuration
public class MyBatisPlusConfig {
    @Bean
    public MybatisPlusInterceptor mybatisPlusInterceptor() {
        MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
        interceptor.addInnerInterceptor(new TenantLineInnerInterceptor(new TenantLineHandler() {
            @Override
            public Expression getTenantId() {
                return new StringValue(TenantContext.getTenantId());
            }

            @Override
            public String getTenantIdColumn() {
                return "tenant_id";  // 数据库字段名
            }

            @Override
            public boolean ignoreTable(String tableName) {
                return Arrays.asList("sys_config").contains(tableName);  // 忽略系统表
            }
        }));
        return interceptor;
    }
}
(5) 实体类标记租户字段
@Data
public class User {
    private Long id;
    private String name;
    @TableField(value = "tenant_id", fill = FieldFill.INSERT)
    private String tenantId;  // 自动填充租户ID
}
(6) 测试接口
@RestController
public class UserController {
    @Autowired
    private UserMapper userMapper;

    @GetMapping("/users")
    public List<User> listUsers() {
        return userMapper.selectList(new QueryWrapper<>());
    }
}

效果:查询 SELECT * FROM user WHERE tenant_id = 'tenant1',插入时自动填充 tenant_id


三、高级配置与优化

1. 混合模式支持

结合字段隔离和动态数据源,例如:

  • 主业务表使用独立数据库(动态数据源)
  • 日志表使用共享表(字段隔离)

2. 忽略租户过滤

通过 @InterceptorIgnore(tenantLine = "true") 注解跳过特定方法:

@InterceptorIgnore(tenantLine = "true")
public List<User> selectAll() {
    return userMapper.selectList(null);
}

3. 多租户数据源自动加载

从数据库加载租户数据源配置:

@Bean
public DataSource initialDataSource() {
    // 查询租户表获取数据源配置
    List<Tenant> tenants = tenantMapper.selectList(null);
    Map<Object, Object> dataSources = tenants.stream()
        .collect(Collectors.toMap(Tenant::getId, tenant -> createDataSource(tenant)));
    dynamicDataSource.setTargetDataSources(dataSources);
}

4. 性能优化

  • 连接池管理:使用 Druid 或 HikariCP 配置连接池,避免资源泄漏。
  • 缓存机制:缓存租户数据源配置,减少数据库查询频率。

五、常见问题与解决方案

问题 解决方案
多表关联查询未注入租户条件 升级 MyBatis-Plus 至 3.5.0+,修复关联查询的租户条件注入问题
插入时 tenant_id 重复 检查实体类 tenant_id 字段的自动填充策略,避免手动赋值
动态数据源切换失败 确保 DynamicDataSourceContextHolder 在异步线程中传递
租户ID未传递导致空指针 在拦截器中添加租户ID校验,返回明确错误提示

你可能感兴趣的:(spring,boot,mybatis,架构)