在 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 三种隔离模式;通过服务层隔离设计,我们实现了租户上下文在微服务调用链中的传播;通过资源配额管理,我们防止了单个租户消耗过多资源;通过缓存隔离策略,我们确保了不同租户的缓存数据不会混淆;通过数据归档服务,我们实现了历史数据的归档和恢复。
下一章节中,我们将通过一个完整的项目实战来整合前面学到的所有知识,并对整个系列进行总结。

702

被折叠的 条评论
为什么被折叠?



