⭐⭐⭐ Spring Boot 项目实战 ⭐⭐⭐ Spring Cloud 项目实战
《Dubbo 实现原理与源码解析 —— 精品合集》 《Netty 实现原理与源码解析 —— 精品合集》
《Spring 实现原理与源码解析 —— 精品合集》 《MyBatis 实现原理与源码解析 —— 精品合集》
《Spring MVC 实现原理与源码解析 —— 精品合集》 《数据库实体设计合集》
《Spring Boot 实现原理与源码解析 —— 精品合集》 《Java 面试题 + Java 学习指南》

摘要: 原创出处 juejin.cn/post/7516443180431556643 「天天摸鱼Java」欢迎转载,保留摘要,谢谢!


🙂🙂🙂关注微信公众号:【芋道源码】有福利:

  1. RocketMQ / MyCAT / Sharding-JDBC 所有源码分析文章列表
  2. RocketMQ / MyCAT / Sharding-JDBC 中文注释源码 GitHub 地址
  3. 您对于源码的疑问每条留言都将得到认真回复。甚至不知道如何读源码也可以请教噢。
  4. 新的源码解析文章实时收到通知。每周更新一篇左右。
  5. 认真的源码交流微信群。

作为一名有着八年 Java 后端开发经验的技术人员,我参与过多个大型 SaaS 系统的架构设计。在这篇博客中,我将分享如何设计一个支持多租户的 SaaS 系统,重点探讨租户数据隔离(数据库级别 / 表级别)和资源配额控制的实现方案。

一、多租户架构概述

多租户(Multi-Tenant)是指一个软件系统同时服务多个客户(租户),每个租户拥有独立的业务空间,但共享相同的基础设施。SaaS 系统的多租户架构设计需要解决两个核心问题:

  • 数据隔离: 确保租户之间的数据互不干扰,满足安全和合规要求。
  • 资源配额: 控制每个租户使用的系统资源(如存储、API 调用次数),避免资源滥用。

二、数据隔离方案对比与实现

1. 数据隔离方案对比

常见的数据隔离方案有三种,各有优缺点:

隔离级别 实现方式 优点 缺点 适用场景
数据库级别 每个租户使用独立数据库 隔离性强,安全性高 成本高,扩展复杂 对数据隔离要求极高的场景
表级别 所有租户共享数据库,但使用独立表 隔离性较好,成本适中 表数量过多时管理复杂 租户数量中等的场景
行级别 所有租户共享表,通过租户 ID 区分 成本低,易于扩展 隔离性弱,需严格权限控制 租户数量庞大的场景

2. 数据库级别隔离实现

架构设计:

核心代码实现(数据源动态切换) :

/**
* 动态数据源路由
*/
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. 表级别隔离实现

架构设计:

核心代码实现(表名动态生成) :

/**
* 表名处理器(基于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) :

/**
* 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. 资源配额管理模型

设计一个通用的资源配额模型,支持多种资源类型:

/**
* 资源配额实体
*/
@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. 基于拦截器的配额控制实现

/**
* 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 实现分布式计数器,确保并发场景下的配额精确控制:

/**
* 基于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. 租户识别与认证

/**
* 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 实现基于租户的权限控制:

/**
* 租户权限表达式
*/
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 项目中实现了租户数据的安全隔离和资源的合理分配,支持了从几百到数十万租户的平滑扩展。

文章目录
  1. 1. 一、多租户架构概述
  2. 2. 二、数据隔离方案对比与实现
    1. 2.0.1. 1. 数据隔离方案对比
    2. 2.0.2. 2. 数据库级别隔离实现
      1. 2.0.2.1. 架构设计:
      2. 2.0.2.2. 核心代码实现(数据源动态切换) :
    3. 2.0.3. 3. 表级别隔离实现
      1. 2.0.3.1. 架构设计:
      2. 2.0.3.2. 核心代码实现(表名动态生成) :
    4. 2.0.4. 4. 行级别隔离实现
      1. 2.0.4.1. 架构设计:
      2. 2.0.4.2. 核心代码实现(自动注入租户 ID) :
  • 3. 三、资源配额控制方案
    1. 3.0.1. 1. 资源配额管理模型
    2. 3.0.2. 2. 基于拦截器的配额控制实现
    3. 3.0.3. 3. 分布式环境下的配额控制
  • 4. 四、多租户认证与权限控制
    1. 4.0.1. 1. 租户识别与认证
    2. 4.0.2. 2. 细粒度权限控制
  • 5. 五、方案选择与最佳实践
    1. 5.0.1. 1. 数据隔离方案选择建议
    2. 5.0.2. 2. 资源配额控制最佳实践
  • 6. 六、总结
    1. 6.0.0.1. 数据隔离:
    2. 6.0.0.2. 资源配额控制:
    3. 6.0.0.3. 认证与权限: