作为一名有着八年 Java 后端开发经验的技术人员,我参与过多个大型 SaaS 系统的架构设计。在这篇博客中,我将分享如何设计一个支持多租户的 SaaS 系统,重点探讨租户数据隔离(数据库级别 / 表级别)和资源配额控制的实现方案。
一、多租户架构概述
多租户(Multi-Tenant)是指一个软件系统同时服务多个客户(租户),每个租户拥有独立的业务空间,但共享相同的基础设施。SaaS 系统的多租户架构设计需要解决两个核心问题:
- • 数据隔离: 确保租户之间的数据互不干扰,满足安全和合规要求。
- • 资源配额: 控制每个租户使用的系统资源(如存储、API 调用次数),避免资源滥用。
二、数据隔离方案对比与实现
1. 数据隔离方案对比
常见的数据隔离方案有三种,各有优缺点:
隔离级别 | 实现方式 | 优点 | 缺点 | 适用场景 |
---|---|---|---|---|
数据库级别 | 每个租户使用独立数据库 | 隔离性强,安全性高 | 成本高,扩展复杂 | 对数据隔离要求极高的场景 |
表级别 | 所有租户共享数据库,但使用独立表 | 隔离性较好,成本适中 | 表数量过多时管理复杂 | 租户数量中等的场景 |
行级别 | 所有租户共享表,通过租户 ID 区分 | 成本低,易于扩展 | 隔离性弱,需严格权限控制 | 租户数量庞大的场景 |
2. 数据库级别隔离实现
架构设计:

图片
核心代码实现(数据源动态切换) :
scss
/**
* 动态数据源路由
*/
publicclassTenantRoutingDataSourceextendsAbstractRoutingDataSource {
@Override
protected Object determineCurrentLookupKey() {
// 从线程上下文中获取当前租户ID
return TenantContextHolder.getTenantId();
}
}
/**
* 租户上下文持有者(使用ThreadLocal存储租户ID)
*/
publicclassTenantContextHolder {
privatestaticfinal ThreadLocal<String> CONTEXT = newThreadLocal<>();
publicstaticvoidsetTenantId(String tenantId) {
CONTEXT.set(tenantId);
}
publicstatic String getTenantId() {
return CONTEXT.get();
}
publicstaticvoidclear() {
CONTEXT.remove();
}
}
/**
* 数据源配置
*/
@Configuration
publicclassDataSourceConfig {
@Bean
public DataSource dataSource() {
TenantRoutingDataSourceroutingDataSource=newTenantRoutingDataSource();
// 初始化所有租户的数据源
Map<Object, Object> targetDataSources = newHashMap<>();
for (TenantConfig tenant : tenantConfigService.getAllTenants()) {
targetDataSources.put(tenant.getTenantId(),
createDataSource(tenant.getDbUrl(), tenant.getDbUser(), tenant.getDbPassword()));
}
routingDataSource.setDefaultTargetDataSource(defaultDataSource());
routingDataSource.setTargetDataSources(targetDataSources);
return routingDataSource;
}
// 其他配置方法...
}
3. 表级别隔离实现
架构设计:

图片
核心代码实现(表名动态生成) :
ini
/**
* 表名处理器(基于MyBatis拦截器)
*/
@Intercepts({
@Signature(type = StatementHandler.class, method = "prepare", args = {Connection.class, Integer.class})
})
publicclassTableNameInterceptorimplementsInterceptor {
@Override
public Object intercept(Invocation invocation)throws Throwable {
StatementHandlerstatementHandler= (StatementHandler) invocation.getTarget();
BoundSqlboundSql= statementHandler.getBoundSql();
StringoriginalSql= boundSql.getSql();
StringtenantId= TenantContextHolder.getTenantId();
// 替换表名(添加租户前缀)
StringmodifiedSql= replaceTableNames(originalSql, tenantId);
// 通过反射修改SQL
FieldsqlField= boundSql.getClass().getDeclaredField("sql");
sqlField.setAccessible(true);
sqlField.set(boundSql, modifiedSql);
return invocation.proceed();
}
private String replaceTableNames(String sql, String tenantId) {
// 简单实现,实际应使用正则表达式或SQL解析器
return sql.replaceAll("\b(user|order)\b", tenantId + "_$1");
}
}
4. 行级别隔离实现
架构设计:

图片
核心代码实现(自动注入租户 ID) :
scss
/**
* MyBatis拦截器:自动注入租户ID
*/
@Intercepts({
@Signature(type = Executor.class, method = "update", args = {MappedStatement.class, Object.class})
})
publicclassTenantIdInterceptorimplementsInterceptor {
@Override
public Object intercept(Invocation invocation)throws Throwable {
Objectparameter= invocation.getArgs()[1];
StringtenantId= TenantContextHolder.getTenantId();
// 如果参数是实体类,自动注入tenantId
if (parameter instanceof BaseEntity) {
((BaseEntity) parameter).setTenantId(tenantId);
}
return invocation.proceed();
}
}
/**
* JPA规范:自动添加租户ID条件
*/
publicclassTenantAwareJpaRepository<T, ID> extendsSimpleJpaRepository<T, ID> {
privatefinal EntityManager entityManager;
privatefinal Class<T> domainClass;
publicTenantAwareJpaRepository(JpaEntityInformation<T, ?> entityInformation,
EntityManager entityManager) {
super(entityInformation, entityManager);
this.entityManager = entityManager;
this.domainClass = entityInformation.getJavaType();
}
@Override
public List<T> findAll() {
CriteriaBuildercb= entityManager.getCriteriaBuilder();
CriteriaQuery<T> query = cb.createQuery(domainClass);
Root<T> root = query.from(domainClass);
// 添加租户ID条件
query.where(cb.equal(root.get("tenantId"), TenantContextHolder.getTenantId()));
return entityManager.createQuery(query).getResultList();
}
}
三、资源配额控制方案
1. 资源配额管理模型
设计一个通用的资源配额模型,支持多种资源类型:
kotlin
/**
* 资源配额实体
*/
@Entity
@Table(name = "tenant_quota")
publicclassTenantQuota {
@Id
private String tenantId;
// 存储配额(MB)
private Long storageQuota;
// 已使用存储(MB)
private Long storageUsed;
// API调用次数配额
private Long apiCallQuota;
// 已使用API调用次数
private Long apiCallsUsed;
// 并发用户数配额
private Integer concurrentUserQuota;
// 上次更新时间
private LocalDateTime lastUpdateTime;
// 资源使用记录方法
publicbooleancanUseStorage(long size) {
return (storageUsed + size) <= storageQuota;
}
publicbooleanuseStorage(long size) {
if (!canUseStorage(size)) {
returnfalse;
}
this.storageUsed += size;
returntrue;
}
// 其他资源使用方法...
}
2. 基于拦截器的配额控制实现
scss
/**
* API调用配额拦截器
*/
publicclassQuotaInterceptorimplementsHandlerInterceptor {
@Autowired
private TenantQuotaService quotaService;
@Override
publicbooleanpreHandle(HttpServletRequest request,
HttpServletResponse response,
Object handler)throws Exception {
StringtenantId= getTenantIdFromRequest(request);
TenantQuotaquota= quotaService.getQuota(tenantId);
// 检查API调用配额
if (quota.getApiCallsUsed() >= quota.getApiCallQuota()) {
response.setStatus(HttpStatus.TOO_MANY_REQUESTS.value());
response.getWriter().write("API调用超出配额");
returnfalse;
}
// 记录API调用
quotaService.recordApiCall(tenantId);
returntrue;
}
}
3. 分布式环境下的配额控制
使用 Redis 实现分布式计数器,确保并发场景下的配额精确控制:
ini
/**
* 基于Redis的分布式配额服务
*/
@Service
publicclassRedisQuotaServiceImplimplementsQuotaService {
@Autowired
private RedisTemplate<String, Long> redisTemplate;
privatestaticfinalStringQUOTA_KEY_PREFIX="tenant:quota:";
privatestaticfinalStringUSAGE_KEY_PREFIX="tenant:usage:";
@Override
publicbooleancheckAndConsume(String tenantId, String resourceType, long amount) {
StringquotaKey= QUOTA_KEY_PREFIX + tenantId + ":" + resourceType;
StringusageKey= USAGE_KEY_PREFIX + tenantId + ":" + resourceType;
// 获取配额
Longquota= redisTemplate.opsForValue().get(quotaKey);
if (quota == null || quota <= 0) {
returnfalse;
}
// 使用Lua脚本原子性检查并消费资源
Stringscript=
"local usage = redis.call('GET', KEYS[2]) or 0 " +
"if usage + ARGV[1] > tonumber(ARGV[2]) then " +
" return 0 " +
"else " +
" return redis.call('INCRBY', KEYS[2], ARGV[1]) " +
"end";
Longresult= redisTemplate.execute(
newDefaultRedisScript<>(script, Long.class),
Arrays.asList(quotaKey, usageKey),
amount, quota);
return result != null && result > 0;
}
}
四、多租户认证与权限控制
1. 租户识别与认证
scss
/**
* JWT过滤器:从Token中提取租户ID
*/
publicclassJwtAuthenticationFilterextendsOncePerRequestFilter {
@Override
protectedvoiddoFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain)throws ServletException, IOException {
Stringtoken= extractToken(request);
if (token != null) {
try {
Claimsclaims= Jwts.parser()
.setSigningKey(secretKey)
.parseClaimsJws(token)
.getBody();
// 提取租户ID并设置到上下文中
StringtenantId= claims.get("tenantId", String.class);
TenantContextHolder.setTenantId(tenantId);
} catch (Exception e) {
response.setStatus(HttpStatus.UNAUTHORIZED.value());
return;
}
}
filterChain.doFilter(request, response);
}
}
2. 细粒度权限控制
使用 Spring Security 实现基于租户的权限控制:
typescript
/**
* 租户权限表达式
*/
publicclassTenantSecurityExpressionRootextendsSecurityExpressionRoot
implementsMethodSecurityExpressionOperations {
private Object filterObject;
private Object returnObject;
publicTenantSecurityExpressionRoot(Authentication authentication) {
super(authentication);
}
/**
* 判断当前用户是否属于指定租户
*/
publicbooleanisTenantUser(String tenantId) {
StringcurrentTenantId= TenantContextHolder.getTenantId();
return currentTenantId != null && currentTenantId.equals(tenantId);
}
// 其他权限方法...
@Override
publicvoidsetFilterObject(Object filterObject) {
this.filterObject = filterObject;
}
@Override
public Object getFilterObject() {
return filterObject;
}
@Override
publicvoidsetReturnObject(Object returnObject) {
this.returnObject = returnObject;
}
@Override
public Object getReturnObject() {
return returnObject;
}
@Override
public Object getThis() {
returnthis;
}
}
五、方案选择与最佳实践
1. 数据隔离方案选择建议
因素 | 数据库级别 | 表级别 | 行级别 |
---|---|---|---|
隔离性 | 最高 | 中等 | 最低 |
成本 | 最高 | 中等 | 最低 |
扩展性 | 低(新增租户需创建库) | 中等(新增租户需创建表) | 高(共享表结构) |
维护复杂度 | 高 | 中等 | 低 |
适用租户数量 | 少(<1000) | 中(1000-10 万) | 多(>10 万) |
2. 资源配额控制最佳实践
- • 分层控制: 同时实现应用层和基础设施层的配额控制。
- • 预付费机制: 支持按使用量计费(Pay-as-you-go)和预付费模式。
- • 弹性扩展: 当租户资源使用接近配额时,提供升级提示。
- • 监控与告警: 实时监控资源使用情况,设置异常使用告警。
六、总结
设计一个高效、安全的多租户 SaaS 系统需要综合考虑数据隔离和资源配额控制:
数据隔离:
- • 数据库级别:适合对隔离性要求极高的场景。
- • 表级别:平衡隔离性和成本的折中方案。
- • 行级别:适合租户数量庞大的场景。
资源配额控制:
- • 设计通用的配额模型,支持多种资源类型。
- • 使用 Redis 实现分布式环境下的精确控制。
- • 通过拦截器和 AOP 实现透明的配额检查。
认证与权限:
- • 从请求中提取租户 ID,建立上下文。
- • 基于租户 ID 实现细粒度的权限控制。
在实际项目中,建议根据租户规模、数据敏感性和预算选择合适的数据隔离方案,并通过弹性的资源配额控制机制确保系统稳定运行。
通过上述方案,我们成功在多个 SaaS 项目中实现了租户数据的安全隔离和资源的合理分配,支持了从几百到数十万租户的平滑扩展。