Java 程序员第 43 阶段 19:微服务整合大模型,多租户隔离方案实战

在 SaaS 应用和大型企业平台中,多租户隔离是一项核心技术需求。本章节将详细介绍如何在微服务架构中实现多租户隔离,涵盖数据层隔离、服务层隔离、缓存隔离、消息队列隔离等多个维度。我们将探讨租户识别、租户上下文传播、租户资源配额管理等核心功能的实现方案。

通过本章的学习,读者将掌握以下核心技能:实现基于租户的数据隔离策略;设计租户感知的服务调用链路;配置租户级别的资源配额限制;实现租户数据的归档和清理机制。

19.2.1 租户模型设计

在设计多租户系统时,首先需要确定租户的模型。常见的租户模型包括:独立数据库模型,每个租户拥有独立的数据库实例;共享数据库独立 Schema 模型,多个租户共享数据库实例但使用独立的 Schema;共享 Schema 模型,所有租户共享同一个数据库和 Schema,通过租户 ID 进行数据区分。

┌─────────────────────────────────────────────────────────────────────────────┐
│                           多租户架构模式                                       │
├─────────────────────────────────────────────────────────────────────────────┤
│                                                                              │
│  模式一:独立数据库                                                           │
│  ┌─────────────┐  ┌─────────────┐  ┌─────────────┐                        │
│  │  Tenant A   │  │  Tenant B   │  │  Tenant C   │                        │
│  │  DB: db_a   │  │  DB: db_b   │  │  DB: db_c   │                        │
│  └─────────────┘  └─────────────┘  └─────────────┘                        │
│   优点: 完全隔离  缺点: 成本高                                                │
│                                                                              │
│  模式二:独立 Schema                                                          │
│  ┌─────────────────────────────────────────────┐                            │
│  │           共享数据库实例                     │                            │
│  │  ┌──────────┐ ┌──────────┐ ┌──────────┐   │                            │
│  │  │ Schema A │ │ Schema B │ │ Schema C │   │                            │
│  │  └──────────┘ └──────────┘ └──────────┘   │                            │
│  └─────────────────────────────────────────────┘                            │
│   优点: 隔离性好  缺点: Schema 管理复杂                                        │
│                                                                              │
│  模式三:共享 Schema                                                          │
│  ┌─────────────────────────────────────────────┐                            │
│  │           共享数据库                        │                            │
│  │  ┌────────────────────────────────────┐     │                            │
│  │  │ tenant_id | table_data ...        │     │                            │
│  │  └────────────────────────────────────┘     │                            │
│  └─────────────────────────────────────────────┘                            │
│   优点: 成本低  缺点: 需要额外过滤条件                                        │
│                                                                              │
└─────────────────────────────────────────────────────────────────────────────┘

19.2.2 租户识别机制

租户识别是多租户系统的基础,常见的租户识别方式包括:通过请求头识别,在请求头中携带租户 ID;通过域名识别,根据访问的域名确定租户;通过 Token 识别,从 JWT 令牌中提取租户信息;通过路径识别,从 URL 路径中提取租户标识。

/**
 * 租户上下文Holder
 * 使用 ThreadLocal 存储当前请求的租户信息
 */
public class TenantContext {
    private static final ThreadLocal<String> CURRENT_TENANT = new ThreadLocal<>();
    private static final ThreadLocal<String> CURRENT_TENANT_NAME = new ThreadLocal<>();
    private static final ThreadLocal<Map<String, String>> TENANT_EXTRA = new ThreadLocal<>();
    /**
     * 设置当前租户ID
     */
    public static void setTenantId(String tenantId) {
        CURRENT_TENANT.set(tenantId);
    }
    /**
     * 获取当前租户ID
     */
    public static String getTenantId() {
        return CURRENT_TENANT.get();
    }
    /**
     * 设置当前租户名称
     */
    public static void setTenantName(String tenantName) {
        CURRENT_TENANT_NAME.set(tenantName);
    }
    /**
     * 获取当前租户名称
     */
    public static String getTenantName() {
        return CURRENT_TENANT_NAME.get();
    }
    /**
     * 设置租户扩展信息
     */
    public static void setExtra(Map<String, String> extra) {
        TENANT_EXTRA.set(extra);
    }
    /**
     * 获取租户扩展信息
     */
    public static Map<String, String> getExtra() {
        return TENANT_EXTRA.get();
    }
    /**
     * 清除租户上下文
     * 重要:请求结束时必须调用
     */
    public static void clear() {
        CURRENT_TENANT.remove();
        CURRENT_TENANT_NAME.remove();
        TENANT_EXTRA.remove();
    }
}
/**
 * 租户识别服务
 */
@Service
@Slf4j
public class TenantIdentificationService {
    @Autowired
    private TenantRepository tenantRepository;
    @Autowired
    private JwtTokenProvider jwtTokenProvider;
    /**
     * 从多种来源识别租户
     */
    public TenantInfo identifyTenant(ServerHttpRequest request) {
        // 1. 优先从 JWT Token 获取
        String token = extractToken(request);
        if (StringUtils.hasText(token)) {
            TenantInfo tokenTenant = getTenantFromToken(token);
            if (tokenTenant != null) {
                log.debug("从 Token 识别租户: {}", tokenTenant.getTenantId());
                return tokenTenant;
            }
        }
        // 2. 从请求头获取
        String tenantId = request.getHeaders().getFirst("X-Tenant-Id");
        if (StringUtils.hasText(tenantId)) {
            TenantInfo headerTenant = getTenantFromId(tenantId);
            if (headerTenant != null) {
                log.debug("从请求头识别租户: {}", tenantId);
                return headerTenant;
            }
        }
        // 3. 从域名获取
        String host = request.getHeaders().getHost().getHostString();
        TenantInfo domainTenant = getTenantFromDomain(host);
        if (domainTenant != null) {
            log.debug("从域名识别租户: {}", host);
            return domainTenant;
        }
        // 4. 默认租户
        log.warn("无法识别租户,使用默认租户");
        return getDefaultTenant();
    }
    private String extractToken(ServerHttpRequest request) {
        String bearerToken = request.getHeaders().getFirst("Authorization");
        if (StringUtils.hasText(bearerToken) && bearerToken.startsWith("Bearer ")) {
            return bearerToken.substring(7);
        }
        return null;
    }
    private TenantInfo getTenantFromToken(String token) {
        try {
            String tenantId = jwtTokenProvider.getTenantId(token);
            if (StringUtils.hasText(tenantId)) {
                return getTenantFromId(tenantId);
            }
        } catch (Exception e) {
            log.debug("从 Token 获取租户失败: {}", e.getMessage());
        }
        return null;
    }
    private TenantInfo getTenantFromId(String tenantId) {
        return tenantRepository.findById(tenantId)
            .map(this::convertToTenantInfo)
            .orElse(null);
    }
    private TenantInfo getTenantFromDomain(String domain) {
        return tenantRepository.findByDomain(domain)
            .map(this::convertToTenantInfo)
            .orElse(null);
    }
    private TenantInfo getDefaultTenant() {
        return tenantRepository.findById("default")
            .map(this::convertToTenantInfo)
            .orElseThrow(() -> new TenantException("系统未配置默认租户"));
    }
    private TenantInfo convertToTenantInfo(Tenant tenant) {
        return TenantInfo.builder()
            .tenantId(tenant.getId())
            .tenantName(tenant.getName())
            .tenantCode(tenant.getCode())
            .databaseConfig(tenant.getDatabaseConfig())
            .status(tenant.getStatus())
            .quota(tenant.getQuota())
            .extra(tenant.getExtraInfo())
            .build();
    }
}

19.3.1 租户数据过滤器

在共享 Schema 模式下,我们需要通过 MyBatis 拦截器或 JPA 注解自动为所有查询添加租户过滤条件。

/**
 * 租户数据过滤器
 * 自动为所有数据操作添加租户条件
 */
@Aspect
@Component
@Slf4j
public class TenantDataFilter {
    @Autowired
    private TenantContextService tenantContextService;
    /**
     * 查询增强 - 自动添加租户过滤
     */
    @Around("execution(* com.example.repository.*Repository.*(..))")
    public Object filterQuery(ProceedingJoinPoint joinPoint) throws Throwable {
        String methodName = joinPoint.getSignature().getName();
        Object[] args = joinPoint.getArgs();
        // 获取当前租户
        String tenantId = tenantContextService.getCurrentTenantId();
        if (StringUtils.isEmpty(tenantId)) {
            log.warn("当前请求无租户信息,跳过租户过滤");
            return joinPoint.proceed();
        }
        // 检查是否为租户相关的数据操作
        if (isTenantRelatedOperation(methodName)) {
            // 处理查询参数,添加租户条件
            Object[] modifiedArgs = addTenantCondition(args, tenantId);
            return joinPoint.proceed(modifiedArgs);
        }
        return joinPoint.proceed();
    }
    private boolean isTenantRelatedOperation(String methodName) {
        // 需要租户过滤的方法
        Set<String> tenantMethods = Set.of(
            "find", "select", "query", "list", "get", "search",
            "count", "exists", "Page"
        );
        return tenantMethods.stream()
            .anyMatch(methodName::startsWith);
    }
    private Object[] addTenantCondition(Object[] args, String tenantId) {
        if (args == null || args.length == 0) {
            return args;
        }
        // 克隆参数数组
        Object[] modifiedArgs = args.clone();
        // 遍历参数,添加租户条件
        for (int i = 0; i < args.length; i++) {
            Object arg = args[i];
            if (arg instanceof Example) {
                modifiedArgs[i] = addTenantToExample((Example) arg, tenantId);
            } else if (arg instanceof Specification) {
                modifiedArgs[i] = addTenantToSpecification((Specification<?>) arg, tenantId);
            } else if (arg instanceof QueryCriteria) {
                modifiedArgs[i] = ((QueryCriteria) arg).setTenantId(tenantId);
            }
        }
        return modifiedArgs;
    }
    private Example addTenantToExample(Example example, String tenantId) {
        Example.Builder builder = new Example.Builder(example);
        builder.and("tenantId =", tenantId);
        return builder.build();
    }
}
/**
 * 租户字段注解
 * 用于标记需要租户隔离的字段
 */
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface TenantEntity {
}

19.3.2 租户数据库路由

在独立数据库或独立 Schema 模式下,我们需要实现动态数据源路由,根据当前租户自动切换数据源。

/**
 * 租户数据源上下文
 */
public class TenantDataSourceContext {
    private static final ThreadLocal<String> CURRENT_DATA_SOURCE = new ThreadLocal<>();
    public static void setDataSource(String dataSourceKey) {
        CURRENT_DATA_SOURCE.set(dataSourceKey);
    }
    public static String getDataSource() {
        return CURRENT_DATA_SOURCE.get();
    }
    public static void clear() {
        CURRENT_DATA_SOURCE.remove();
    }
}
/**
 * 租户动态数据源
 */
public class TenantRoutingDataSource extends AbstractRoutingDataSource {
    @Override
    protected Object determineCurrentLookupKey() {
        String tenantDataSource = TenantDataSourceContext.getDataSource();
        if (StringUtils.hasText(tenantDataSource)) {
            log.debug("使用租户数据源: {}", tenantDataSource);
            return tenantDataSource;
        }
        log.debug("使用默认数据源");
        return "master";
    }
}
/**
 * 租户数据源管理器
 */
@Service
@Slf4j
public class TenantDataSourceManager {
    private Map<String, DataSource> tenantDataSources = new ConcurrentHashMap<>();
    @Autowired
    private DataSourceProperties dataSourceProperties;
    @Autowired
    private TenantRepository tenantRepository;
    /**
     * 初始化租户数据源
     */
    public void initializeTenantDataSource(String tenantId) {
        if (tenantDataSources.containsKey(tenantId)) {
            log.debug("租户数据源已存在: {}", tenantId);
            return;
        }
        Tenant tenant = tenantRepository.findById(tenantId)
            .orElseThrow(() -> new TenantException("租户不存在: " + tenantId));
        // 根据租户配置创建数据源
        DataSource dataSource = createDataSourceForTenant(tenant);
        tenantDataSources.put(tenantId, dataSource);
        log.info("初始化租户数据源完成: {}", tenantId);
    }
    /**
     * 切换到租户数据源
     */
    public void switchToTenant(String tenantId) {
        // 确保数据源已初始化
        if (!tenantDataSources.containsKey(tenantId)) {
            initializeTenantDataSource(tenantId);
        }
        TenantDataSourceContext.setDataSource(tenantId);
        TenantContext.setTenantId(tenantId);
        // 切换 Redis 租户
        RedisClientSwitcher.switchToTenant(tenantId);
    }
    /**
     * 清除租户上下文
     */
    public void clearTenantContext() {
        TenantDataSourceContext.clear();
        TenantContext.clear();
        RedisClientSwitcher.clear();
    }
    /**
     * 创建租户专用数据源
     */
    private DataSource createDataSourceForTenant(Tenant tenant) {
        DatabaseConfig config = tenant.getDatabaseConfig();
        HikariConfig hikariConfig = new HikariConfig();
        hikariConfig.setJdbcUrl(config.getJdbcUrl());
        hikariConfig.setUsername(config.getUsername());
        hikariConfig.setPassword(config.getPassword());
        hikariConfig.setDriverClassName("com.mysql.cj.jdbc.Driver");
        hikariConfig.setMaximumPoolSize(config.getMaxPoolSize());
        hikariConfig.setMinimumIdle(config.getMinPoolSize());
        hikariConfig.setConnectionTimeout(config.getConnectionTimeout());
        return new HikariDataSource(hikariConfig);
    }
    /**
     * 获取租户数据源
     */
    public DataSource getTenantDataSource(String tenantId) {
        return tenantDataSources.get(tenantId);
    }
    /**
     * 销毁租户数据源
     */
    public void destroyTenantDataSource(String tenantId) {
        DataSource dataSource = tenantDataSources.remove(tenantId);
        if (dataSource instanceof HikariDataSource) {
            ((HikariDataSource) dataSource).close();
        }
        log.info("销毁租户数据源: {}", tenantId);
    }
}

19.4.1 租户上下文传播

在微服务调用链中,租户上下文需要在服务之间传播,确保每个服务都能识别当前请求属于哪个租户。

/**
 * 租户上下文传播过滤器
 * 在网关层和每个服务中都需要配置
 */
@Component
@Slf4j
public class TenantPropagationFilter implements GlobalFilter, Ordered {
    private static final String TENANT_HEADER = "X-Tenant-Id";
    private static final String TENANT_NAME_HEADER = "X-Tenant-Name";
    @Autowired
    private TenantContextService tenantContextService;
    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        try {
            // 1. 从请求头获取租户信息
            String tenantId = exchange.getRequest().getHeaders().getFirst(TENANT_HEADER);
            String tenantName = exchange.getRequest().getHeaders().getFirst(TENANT_NAME_HEADER);
            // 2. 如果请求头没有,尝试从 Token 获取
            if (StringUtils.isEmpty(tenantId)) {
                TenantInfo tenantInfo = tenantContextService.identifyTenant(exchange.getRequest());
                if (tenantInfo != null) {
                    tenantId = tenantInfo.getTenantId();
                    tenantName = tenantInfo.getTenantName();
                }
            }
            // 3. 设置租户上下文
            if (StringUtils.hasText(tenantId)) {
                TenantContext.setTenantId(tenantId);
                if (StringUtils.hasText(tenantName)) {
                    TenantContext.setTenantName(tenantName);
                }
                // 4. 将租户信息添加到响应头,供下游服务使用
                ServerHttpResponse response = exchange.getResponse();
                response.getHeaders().add(TENANT_HEADER, tenantId);
                if (StringUtils.hasText(tenantName)) {
                    response.getHeaders().add(TENANT_NAME_HEADER, tenantName);
                }
                log.debug("设置租户上下文: {}", tenantId);
            }
            // 5. 继续过滤器链
            return chain.filter(exchange);
        } finally {
            // 6. 清理租户上下文
            TenantContext.clear();
        }
    }
    @Override
    public int getOrder() {
        return -150; // 在认证过滤器之前执行
    }
}
/**
 * Feign 客户端租户传播拦截器
 */
@Configuration
public class FeignTenantInterceptor implements RequestInterceptor {
    @Override
    public void apply(RequestTemplate template) {
        String tenantId = TenantContext.getTenantId();
        if (StringUtils.hasText(tenantId)) {
            template.header("X-Tenant-Id", tenantId);
            log.debug("Feign 请求添加租户头: {}", tenantId);
        }
    }
}
/**
 * RestTemplate 租户传播拦截器
 */
@Component
@Slf4j
public class RestTemplateTenantInterceptor implements ClientHttpRequestInterceptor {
    @Override
    public ClientHttpResponse intercept(HttpRequest request, byte[] body,
                                        ClientHttpRequestExecution execution) throws IOException {
        String tenantId = TenantContext.getTenantId();
        if (StringUtils.hasText(tenantId)) {
            request.getHeaders().set("X-Tenant-Id", tenantId);
            log.debug("RestTemplate 请求添加租户头: {}", tenantId);
        }
        return execution.execute(request, body);
    }
}

19.4.2 租户隔离服务

对于需要完全隔离的租户,可以提供独立的微服务实例,实现服务级别的隔离。

/**
 * 租户隔离服务配置
 */
@Configuration
public class TenantIsolationConfig {
    /**
     * 为特定租户创建独立的服务 Bean
     */
    @Bean
    @Scope("prototype")
    public TenantAwareService tenantAwareService(@Qualifier("currentTenantId") String tenantId) {
        return new TenantAwareService(tenantId);
    }
    /**
     * 租户感知的 BeanPostProcessor
     */
    @Bean
    public TenantBeanPostProcessor tenantBeanPostProcessor() {
        return new TenantBeanPostProcessor();
    }
}
/**
 * 租户隔离的 AI 服务
 */
@Service
@TenantIsolation(enabled = true)
@Slf4j
public class IsolatedAiService {
    private final String tenantId;
    private final LlmClient llmClient;
    private final TenantQuotaService quotaService;
    public IsolatedAiService(String tenantId) {
        this.tenantId = tenantId;
        this.llmClient = new LlmClient(); // 租户独立的客户端
        this.quotaService = new TenantQuotaService(tenantId);
    }
    /**
     * 使用租户独立的 AI 模型
     */
    public String chat(String prompt) {
        // 检查租户配额
        if (!quotaService.hasQuota()) {
            throw new QuotaExceededException("租户 " + tenantId + " 配额已用尽");
        }
        // 使用租户专属配置
        LlmConfig config = LlmConfigLoader.loadForTenant(tenantId);
        // 调用 AI 服务
        String response = llmClient.chat(config, prompt);
        // 扣减配额
        quotaService.consume(1);
        return response;
    }
}

19.5.1 租户配额服务

每个租户应该有资源使用配额限制,防止单个租户消耗过多资源影响其他租户。

/**
 * 租户配额服务
 */
@Service
@Slf4j
public class TenantQuotaService {
    private static final String QUOTA_KEY_PREFIX = "tenant:quota:";
    @Autowired
    private RedisTemplate<String, Object> redisTemplate;
    @Autowired
    private TenantRepository tenantRepository;
    /**
     * 检查租户是否有可用配额
     */
    public boolean hasQuota(String tenantId, String resourceType) {
        String quotaKey = buildQuotaKey(tenantId, resourceType);
        Long remaining = redisTemplate.opsForValue().decrement(quotaKey);
        if (remaining == null || remaining < 0) {
            // 配额已用尽,恢复计数
            redisTemplate.opsForValue().increment(quotaKey);
            return false;
        }
        return true;
    }
    /**
     * 消耗配额
     */
    public void consume(String tenantId, String resourceType, int amount) {
        String quotaKey = buildQuotaKey(tenantId, resourceType);
        redisTemplate.opsForValue().decrement(quotaKey, amount);
        log.debug("租户 {} 消耗 {} 配额: {}", tenantId, resourceType, amount);
    }
    /**
     * 获取配额信息
     */
    public QuotaInfo getQuotaInfo(String tenantId) {
        Tenant tenant = tenantRepository.findById(tenantId)
            .orElseThrow(() -> new TenantException("租户不存在: " + tenantId));
        TenantQuota quota = tenant.getQuota();
        return QuotaInfo.builder()
            .tenantId(tenantId)
            .aiQuota(quota.getAiQuota())
            .aiUsed(getUsedQuota(tenantId, "ai"))
            .storageQuota(quota.getStorageQuota())
            .storageUsed(getUsedQuota(tenantId, "storage"))
            .apiQuota(quota.getApiQuota())
            .apiUsed(getUsedQuota(tenantId, "api"))
            .build();
    }
    /**
     * 重置配额
     */
    public void resetQuota(String tenantId, String resourceType) {
        String quotaKey = buildQuotaKey(tenantId, resourceType);
        Tenant tenant = tenantRepository.findById(tenantId)
            .orElseThrow(() -> new TenantException("租户不存在"));
        int defaultQuota = getDefaultQuota(tenant, resourceType);
        redisTemplate.opsForValue().set(quotaKey, defaultQuota);
        // 设置过期时间
        redisTemplate.expire(quotaKey, getQuotaPeriod(resourceType), TimeUnit.SECONDS);
        log.info("重置租户 {} 的 {} 配额为: {}", tenantId, resourceType, defaultQuota);
    }
    /**
     * 初始化租户配额
     */
    public void initializeQuota(String tenantId) {
        Tenant tenant = tenantRepository.findById(tenantId)
            .orElseThrow(() -> new TenantException("租户不存在"));
        TenantQuota quota = tenant.getQuota();
        // AI 配额
        redisTemplate.opsForValue().set(
            buildQuotaKey(tenantId, "ai"),
            quota.getAiQuota()
        );
        // 存储配额
        redisTemplate.opsForValue().set(
            buildQuotaKey(tenantId, "storage"),
            quota.getStorageQuota()
        );
        // API 配额
        redisTemplate.opsForValue().set(
            buildQuotaKey(tenantId, "api"),
            quota.getApiQuota()
        );
        log.info("初始化租户 {} 配额完成", tenantId);
    }
    private String buildQuotaKey(String tenantId, String resourceType) {
        return QUOTA_KEY_PREFIX + tenantId + ":" + resourceType;
    }
    private int getUsedQuota(String tenantId, String resourceType) {
        String quotaKey = buildQuotaKey(tenantId, resourceType);
        Object value = redisTemplate.opsForValue().get(quotaKey);
        if (value == null) {
            return 0;
        }
        return ((Number) value).intValue();
    }
    private int getDefaultQuota(Tenant tenant, String resourceType) {
        TenantQuota quota = tenant.getQuota();
        switch (resourceType) {
            case "ai": return quota.getAiQuota();
            case "storage": return quota.getStorageQuota();
            case "api": return quota.getApiQuota();
            default: return 0;
        }
    }
    private long getQuotaPeriod(String resourceType) {
        switch (resourceType) {
            case "ai":
            case "api":
                return 24 * 60 * 60; // 日配额
            case "storage":
                return 30 * 24 * 60 * 60; // 月配额
            default:
                return 24 * 60 * 60;
        }
    }
}
/**
 * 配额检查拦截器
 */
@Aspect
@Component
@Slf4j
public class QuotaCheckInterceptor {
    @Autowired
    private TenantQuotaService quotaService;
    @Around("@annotation(QuotaRequired)")
    public Object checkQuota(ProceedingJoinPoint joinPoint, QuotaRequired quotaRequired) throws Throwable {
        String tenantId = TenantContext.getTenantId();
        if (tenantId == null) {
            throw new TenantException("无法确定租户");
        }
        String resourceType = quotaRequired.resourceType();
        if (!quotaService.hasQuota(tenantId, resourceType)) {
            throw new QuotaExceededException(
                String.format("租户 %s 的 %s 配额已用尽", tenantId, resourceType)
            );
        }
        try {
            return joinPoint.proceed();
        } finally {
            quotaService.consume(tenantId, resourceType, 1);
        }
    }
}
/**
 * 配额需求注解
 */
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface QuotaRequired {
    String resourceType() default "api";
}

19.6.1 租户缓存键设计

在缓存设计中,需要确保不同租户的缓存数据不会混淆。常见的做法是在缓存键中添加租户标识。

/**
 * 租户缓存键生成器
 */
@Component
public class TenantCacheKeyGenerator {
    private static final String KEY_PREFIX = "tenant:";
    /**
     * 生成租户感知的缓存键
     */
    public String generate(String tenantId, String category, String key) {
        return String.format("%s%s:%s:%s", KEY_PREFIX, tenantId, category, key);
    }
    /**
     * 生成用户相关的缓存键
     */
    public String generateUserKey(String tenantId, String userId, String key) {
        return String.format("%s%s:user:%s:%s", KEY_PREFIX, tenantId, userId, key);
    }
    /**
     * 生成通用缓存键(自动包含当前租户)
     */
    public String generateCurrent(String category, String key) {
        String tenantId = TenantContext.getTenantId();
        if (tenantId == null) {
            throw new TenantException("当前请求无租户上下文");
        }
        return generate(tenantId, category, key);
    }
}
/**
 * 租户缓存管理器
 */
@Service
@Slf4j
public class TenantCacheManager {
    @Autowired
    private RedisTemplate<String, Object> redisTemplate;
    @Autowired
    private TenantCacheKeyGenerator keyGenerator;
    /**
     * 缓存查询结果
     */
    public <T> void cache(String tenantId, String category, String key, T value, long ttl) {
        String cacheKey = keyGenerator.generate(tenantId, category, key);
        redisTemplate.opsForValue().set(cacheKey, value, ttl, TimeUnit.SECONDS);
        log.debug("缓存数据: {}", cacheKey);
    }
    /**
     * 获取缓存
     */
    @SuppressWarnings("unchecked")
    public <T> T get(String tenantId, String category, String key) {
        String cacheKey = keyGenerator.generate(tenantId, category, key);
        return (T) redisTemplate.opsForValue().get(cacheKey);
    }
    /**
     * 清除租户缓存
     */
    public void clearTenantCache(String tenantId) {
        String pattern = keyGenerator.generate(tenantId, "*", "*");
        Set<String> keys = redisTemplate.keys(pattern);
        if (keys != null && !keys.isEmpty()) {
            redisTemplate.delete(keys);
            log.info("清除租户 {} 的缓存,共 {} 条", tenantId, keys.size());
        }
    }
    /**
     * 清除特定分类的缓存
     */
    public void clearCategoryCache(String tenantId, String category) {
        String pattern = keyGenerator.generate(tenantId, category, "*");
        Set<String> keys = redisTemplate.keys(pattern);
        if (keys != null && !keys.isEmpty()) {
            redisTemplate.delete(keys);
            log.info("清除租户 {} 分类 {} 的缓存,共 {} 条", tenantId, category, keys.size());
        }
    }
}

19.7.1 数据生命周期管理

对于长期不活跃的租户,可以将其数据归档以释放存储空间,同时保持数据可查询能力。

/**
 * 租户数据归档服务
 */
@Service
@Slf4j
public class TenantArchiveService {
    @Autowired
    private TenantRepository tenantRepository;
    @Autowired
    private TenantDataSourceManager dataSourceManager;
    @Autowired
    private ArchiveRecordRepository archiveRecordRepository;
    /**
     * 归档租户数据
     */
    public void archiveTenant(String tenantId) {
        Tenant tenant = tenantRepository.findById(tenantId)
            .orElseThrow(() -> new TenantException("租户不存在"));
        // 1. 创建归档记录
        ArchiveRecord record = createArchiveRecord(tenant);
        // 2. 导出数据到归档存储
        String archivePath = exportDataToArchive(tenantId);
        // 3. 清空生产表数据
        truncateTenantTables(tenantId);
        // 4. 释放租户资源
        releaseTenantResources(tenantId);
        // 5. 更新租户状态
        tenant.setStatus(TenantStatus.ARCHIVED);
        tenant.setArchivedAt(LocalDateTime.now());
        tenantRepository.save(tenant);
        // 6. 更新归档记录
        record.setStatus(ArchiveStatus.COMPLETED);
        record.setArchivePath(archivePath);
        archiveRecordRepository.save(record);
        log.info("租户 {} 数据归档完成,归档路径: {}", tenantId, archivePath);
    }
    /**
     * 恢复归档数据
     */
    public void restoreTenant(String tenantId) {
        ArchiveRecord record = archiveRecordRepository.findByTenantId(tenantId)
            .orElseThrow(() -> new ArchiveException("未找到归档记录"));
        // 1. 重新初始化租户资源
        dataSourceManager.initializeTenantDataSource(tenantId);
        // 2. 从归档恢复数据
        restoreDataFromArchive(tenantId, record.getArchivePath());
        // 3. 清理归档文件
        deleteArchiveFile(record.getArchivePath());
        // 4. 更新租户状态
        Tenant tenant = tenantRepository.findById(tenantId).get();
        tenant.setStatus(TenantStatus.ACTIVE);
        tenant.setArchivedAt(null);
        tenantRepository.save(tenant);
        // 5. 更新归档记录
        record.setStatus(ArchiveStatus.RESTORED);
        archiveRecordRepository.save(record);
        log.info("租户 {} 数据恢复完成", tenantId);
    }
    private ArchiveRecord createArchiveRecord(Tenant tenant) {
        ArchiveRecord record = ArchiveRecord.builder()
            .tenantId(tenant.getId())
            .tenantName(tenant.getName())
            .archiveTime(LocalDateTime.now())
            .dataSize(calculateTenantDataSize(tenant.getId()))
            .status(ArchiveStatus.IN_PROGRESS)
            .build();
        return archiveRecordRepository.save(record);
    }
    private String exportDataToArchive(String tenantId) {
        // 使用 mysqldump 或 pg_dump 导出数据
        String timestamp = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyyMMddHHmmss"));
        String archivePath = String.format("/archive/tenant_%s_%s.sql", tenantId, timestamp);
        // 执行导出命令
        String command = String.format("mysqldump -h %s -u %s -p %s tenant_%s > %s",
            dbHost, dbUser, dbPassword, tenantId, archivePath);
        try {
            ProcessUtil.exec(command);
        } catch (Exception e) {
            throw new ArchiveException("数据导出失败: " + e.getMessage());
        }
        return archivePath;
    }
}

本章节我们详细介绍了微服务架构中的多租户隔离方案。通过租户识别机制,我们实现了从请求中自动识别租户;通过数据层隔离方案,我们支持了独立数据库、独立 Schema 和共享 Schema 三种隔离模式;通过服务层隔离设计,我们实现了租户上下文在微服务调用链中的传播;通过资源配额管理,我们防止了单个租户消耗过多资源;通过缓存隔离策略,我们确保了不同租户的缓存数据不会混淆;通过数据归档服务,我们实现了历史数据的归档和恢复。

下一章节中,我们将通过一个完整的项目实战来整合前面学到的所有知识,并对整个系列进行总结。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

洛水石

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值