SpringBoot + 多租户数据隔离(Schema/字段级):一套代码服务百家企业客户

多租户系统的挑战

在我们的日常开发工作中,经常会遇到这样的需求:

  • 一套系统需要服务多家企业客户,每个客户的数据要完全隔离
  • 客户A不能看到客户B的数据,哪怕是一个字节都不行
  • 需要灵活支持新客户的接入,不能因为客户数量增长而影响性能
  • 要支持客户数据的独立备份和迁移

传统的单租户架构显然无法满足这些需求,而多租户架构的实现方式也各有优劣。今天我们就来聊聊如何用SpringBoot构建一个高效、安全的多租户系统。

多租户实现方案对比

1. Schema隔离方案

每个租户拥有独立的数据库Schema,数据完全物理隔离,安全性最高,但资源消耗较大。

2. 字段级隔离方案

所有租户共享同一个数据库,通过tenant_id字段区分数据,资源利用率高,但需要严格的访问控制。

3. 混合方案

根据业务特点选择合适的隔离级别,例如核心数据用Schema隔离,日志数据用字段级隔离。

SpringBoot多租户实现

1. 租户标识解析

首先需要确定当前请求对应的租户:

@Component
public class TenantResolver {
    
    public String resolveTenantId() {
        // 从请求头获取租户ID
        HttpServletRequest request = getCurrentRequest();
        String tenantId = request.getHeader("X-Tenant-ID");
        
        if (StringUtils.isEmpty(tenantId)) {
            // 从域名解析租户ID
            String host = request.getServerName();
            tenantId = extractTenantFromHost(host);
        }
        
        if (StringUtils.isEmpty(tenantId)) {
            throw new TenantNotFoundException("Tenant ID not found");
        }
        
        return tenantId;
    }
    
    private String extractTenantFromHost(String host) {
        // 从子域名提取租户ID: customer1.yourapp.com -> customer1
        return host.split("\\.")[0];
    }
}

2. 动态数据源路由

基于租户ID动态选择数据源:

public class TenantRoutingDataSource extends AbstractRoutingDataSource {
    
    @Autowired
    private TenantResolver tenantResolver;
    
    @Override
    protected Object determineCurrentLookupKey() {
        return tenantResolver.resolveTenantId();
    }
    
    @PostConstruct
    public void initializeDataSources() {
        // 根据租户配置动态创建数据源
        Map<String, DataSource> dataSourceMap = new HashMap<>();
        
        List<TenantConfig> tenants = tenantService.getAllTenants();
        for (TenantConfig tenant : tenants) {
            DataSource dataSource = createDataSource(tenant);
            dataSourceMap.put(tenant.getId(), dataSource);
        }
        
        setTargetDataSources(dataSourceMap);
        setDefaultTargetDataSource(dataSourceMap.values().iterator().next());
        afterPropertiesSet();
    }
}

3. MyBatis-Plus租户插件

对于字段级隔离,使用MyBatis-Plus的租户插件:

@Configuration
public class MybatisPlusConfig {
    
    @Bean
    public MybatisPlusInterceptor mybatisPlusInterceptor() {
        MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
        
        // 租户插件
        TenantLineInnerInterceptor tenantInterceptor = new TenantLineInnerInterceptor();
        tenantInterceptor.setTenantLineHandler(new TenantLineHandler() {
            @Override
            public Expression getTenantId() {
                return new StringValue(getCurrentTenantId());
            }
            
            @Override
            public boolean ignoreTable(String tableName) {
                // 指定哪些表不需要租户隔离
                return "sys_tenant".equalsIgnoreCase(tableName) ||
                       "sys_user".equalsIgnoreCase(tableName);
            }
        });
        
        interceptor.addInnerInterceptor(tenantInterceptor);
        return interceptor;
    }
}

4. JPA租户过滤

对于JPA应用,可以使用@Where注解:

@Entity
@Table(name = "user_info")
@Where(clause = "tenant_id = '" + "#{@tenantContext.getCurrentTenantId()}" + "'")
public class UserInfo {
    @Id
    private Long id;
    
    private String username;
    private String email;
    
    @CreatedBy
    private String tenantId; // 租户ID字段
    
    // getters and setters
}

5. 租户上下文管理

创建租户上下文管理器:

@Component
public class TenantContextHolder {
    
    private static final ThreadLocal<String> CONTEXT = new ThreadLocal<>();
    
    public void setCurrentTenant(String tenantId) {
        CONTEXT.set(tenantId);
    }
    
    public String getCurrentTenant() {
        return CONTEXT.get();
    }
    
    public void clear() {
        CONTEXT.remove();
    }
}

@Component
public class TenantFilter implements Filter {
    
    @Autowired
    private TenantContextHolder tenantContextHolder;
    
    @Autowired
    private TenantService tenantService;
    
    @Override
    public void doFilter(ServletRequest request, ServletResponse response, 
                        FilterChain chain) throws IOException, ServletException {
        
        HttpServletRequest httpRequest = (HttpServletRequest) request;
        String tenantId = extractTenantId(httpRequest);
        
        if (tenantService.isValidTenant(tenantId)) {
            tenantContextHolder.setCurrentTenant(tenantId);
        }
        
        try {
            chain.doFilter(request, response);
        } finally {
            tenantContextHolder.clear();
        }
    }
}

高级特性实现

1. 租户动态扩缩容

支持租户的动态接入和移除:

@Service
public class TenantManagementService {
    
    @Autowired
    private DataSource masterDataSource; // 主数据源,管理租户信息
    
    public void createTenant(String tenantId, TenantConfig config) {
        // 1. 在主表中注册租户信息
        registerTenantInfo(tenantId, config);
        
        // 2. 创建租户专属Schema或初始化租户数据表
        initializeTenantSchema(tenantId, config);
        
        // 3. 重新加载数据源路由
        reloadDataSourceRouting(tenantId);
    }
    
    public void removeTenant(String tenantId) {
        // 1. 标记租户为删除状态
        markTenantAsDeleting(tenantId);
        
        // 2. 等待当前租户请求处理完成
        waitForActiveRequests(tenantId);
        
        // 3. 删除租户数据和Schema
        deleteTenantData(tenantId);
        
        // 4. 重新加载数据源路由
        reloadDataSourceRouting(null);
    }
}

2. 跨租户查询

某些场景下需要跨租户查询(如管理员统计):

@Service
public class CrossTenantService {
    
    @Autowired
    private MasterTenantService masterTenantService;
    
    public List<TenantStat> getGlobalStatistics() {
        // 只有超级管理员才能执行跨租户查询
        if (!SecurityUtils.isAdmin()) {
            throw new AccessDeniedException("Access denied");
        }
        
        // 临时切换到主租户上下文
        String originalTenant = TenantContextHolder.getCurrentTenant();
        TenantContextHolder.setCurrentTenant("master");
        
        try {
            return masterTenantService.getGlobalStats();
        } finally {
            // 恢复原始租户上下文
            TenantContextHolder.setCurrentTenant(originalTenant);
        }
    }
}

3. 租户数据备份与恢复

@Component
public class TenantBackupService {
    
    public void backupTenantData(String tenantId) {
        // 1. 获取租户专属数据源
        DataSource tenantDataSource = getTenantDataSource(tenantId);
        
        // 2. 执行备份操作
        String backupFileName = "backup_" + tenantId + "_" + 
                               LocalDate.now().format(DateTimeFormatter.ISO_LOCAL_DATE) + ".sql";
        
        executeBackupScript(tenantDataSource, backupFileName);
        
        // 3. 存储备份文件信息
        saveBackupRecord(tenantId, backupFileName);
    }
    
    public void restoreTenantData(String tenantId, String backupFile) {
        // 验证备份文件安全性
        if (!isValidBackupFile(backupFile)) {
            throw new IllegalArgumentException("Invalid backup file");
        }
        
        DataSource tenantDataSource = getTenantDataSource(tenantId);
        executeRestoreScript(tenantDataSource, backupFile);
    }
}

最佳实践建议

  1. 安全优先:确保数据隔离的绝对性,任何情况下都不能让租户看到其他租户数据
  2. 性能优化:合理选择隔离方案,避免过度隔离导致性能问题
  3. 监控告警:监控租户资源使用情况,及时发现异常
  4. 灰度发布:新租户接入时采用灰度策略,确保系统稳定性

通过这样的多租户架构设计,我们可以用一套代码服务多个企业客户,既保证了数据安全,又提高了资源利用率。


标题:SpringBoot + 多租户数据隔离(Schema/字段级):一套代码服务百家企业客户
作者:jiangyi
地址:http://www.jiangyi.space/articles/2026/01/30/1769664221825.html

    0 评论
avatar