网站首页 > 教程文章 正文
作为一名有着八年 Java 后端开发经验的技术人员,我参与过多个大型 SaaS 系统的架构设计。在这篇博客中,我将分享如何设计一个支持多租户的 SaaS 系统,重点探讨租户数据隔离(数据库级别 / 表级别)和资源配额控制的实现方案。
一、多租户架构概述
多租户(Multi-Tenant)是指一个软件系统同时服务多个客户(租户),每个租户拥有独立的业务空间,但共享相同的基础设施。SaaS 系统的多租户架构设计需要解决两个核心问题:
- 数据隔离 :确保租户之间的数据互不干扰,满足安全和合规要求。
- 资源配额 :控制每个租户使用的系统资源(如存储、API 调用次数),避免资源滥用。
二、数据隔离方案对比与实现
1. 数据隔离方案对比
常见的数据隔离方案有三种,各有优缺点: #技术分享
| 隔离级别 | 实现方式 | 优点 | 缺点 | 适用场景 | | ---
| 数据库级别 | 每个租户使用独立数据库 | 隔离性强,安全性高 | 成本高,扩展复杂 | 对数据隔离要求极高的场景 | | 表级别 | 所有租户共享数据库,但使用独立表 | 隔离性较好,成本适中 | 表数量过多时管理复杂 | 租户数量中等的场景 | | 行级别 | 所有租户共享表,通过租户 ID 区分 | 成本低,易于扩展 | 隔离性弱,需严格权限控制 | 租户数量庞大的场景 |
2. 数据库级别隔离实现
架构设计 :
+-------------------+
| 租户 A 数据库 | | 租户 B 数据库 | | 租户 N 数据库 | +-------------------+
| 用户表 | | 用户表 | | 用户表 | | 订单表 | | 订单表 | | 订单表 | +-------------------+ +-------------------+ +-------------------+
核心代码实现(数据源动态切换) :
public class TenantRoutingDataSource extends AbstractRoutingDataSource {
@Override
protected Object determineCurrentLookupKey() {
return TenantContextHolder.getTenantId();
}
}
public class TenantContextHolder { private static final ThreadLocal<String> CONTEXT = new ThreadLocal<>(); public static void setTenantId(String tenantId) { CONTEXT.set(tenantId); } public static String getTenantId() { return CONTEXT.get(); } public static void clear() { CONTEXT.remove(); } }
@Configuration public class DataSourceConfig { @Bean public DataSource dataSource() { TenantRoutingDataSource routingDataSource = new TenantRoutingDataSource(); Map<Object, Object> targetDataSources = new HashMap<>(); 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. 表级别隔离实现
架构设计 :
plaintext
+-------------------+
| 共享数据库 |
+-------------------+
| 租户 A_订单表 | | 租户 B_用户表 | | 租户 B_订单表 | +-------------------+
核心代码实现(表名动态生成) :
@Intercepts({
@Signature(type = StatementHandler.class, method = "prepare", args = {Connection.class, Integer.class})
})
public class TableNameInterceptor implements Interceptor {
@Override
public Object intercept(Invocation invocation) throws Throwable {
StatementHandler statementHandler = (StatementHandler) invocation.getTarget();
BoundSql boundSql = statementHandler.getBoundSql();
String originalSql = boundSql.getSql();
String tenantId = TenantContextHolder.getTenantId();
String modifiedSql = replaceTableNames(originalSql, tenantId);
Field sqlField = boundSql.getClass().getDeclaredField("sql");
sqlField.setAccessible(true);
sqlField.set(boundSql, modifiedSql);
return invocation.proceed();
}
private String replaceTableNames(String sql, String tenantId) {
return sql.replaceAll("\b(user|order)\b", tenantId + "_$1");
}
}
4. 行级别隔离实现
架构设计 :
plaintext
+-------------------+
| 共享数据库 |
+-------------------+
| +
| +
| +
| 订单表 | | +
| +
| + amount | +-------------------+
核心代码实现(自动注入租户 ID) :
@Intercepts({
@Signature(type = Executor.class, method = "update", args = {MappedStatement.class, Object.class})
})
public class TenantIdInterceptor implements Interceptor {
@Override
public Object intercept(Invocation invocation) throws Throwable {
Object parameter = invocation.getArgs()[1];
String tenantId = TenantContextHolder.getTenantId();
if (parameter instanceof BaseEntity) {
((BaseEntity) parameter).setTenantId(tenantId);
}
return invocation.proceed();
}
}
public class TenantAwareJpaRepository<T, ID> extends SimpleJpaRepository<T, ID> { private final EntityManager entityManager; private final Class<T> domainClass; public TenantAwareJpaRepository(JpaEntityInformation<T, ?> entityInformation, EntityManager entityManager) { super(entityInformation, entityManager); this.entityManager = entityManager; this.domainClass = entityInformation.getJavaType(); } @Override public List<T> findAll() { CriteriaBuilder cb = entityManager.getCriteriaBuilder(); CriteriaQuery<T> query = cb.createQuery(domainClass); Root<T> root = query.from(domainClass); query.where(cb.equal(root.get("tenantId"), TenantContextHolder.getTenantId())); return entityManager.createQuery(query).getResultList(); } }
三、资源配额控制方案
1. 资源配额管理模型
设计一个通用的资源配额模型,支持多种资源类型:
@Entity
@Table(name = "tenant_quota")
public class TenantQuota {
@Id
private String tenantId;
private Long storageQuota;
private Long storageUsed;
private Long apiCallQuota;
private Long apiCallsUsed;
private Integer concurrentUserQuota;
private LocalDateTime lastUpdateTime;
public boolean canUseStorage(long size) {
return (storageUsed + size) <= storageQuota;
}
public boolean useStorage(long size) {
if (!canUseStorage(size)) {
return false;
}
this.storageUsed += size;
return true;
}
}
2. 基于拦截器的配额控制实现
public class QuotaInterceptor implements HandlerInterceptor {
@Autowired
private TenantQuotaService quotaService;
@Override
public boolean preHandle(HttpServletRequest request,
HttpServletResponse response,
Object handler) throws Exception {
String tenantId = getTenantIdFromRequest(request);
TenantQuota quota = quotaService.getQuota(tenantId);
if (quota.getApiCallsUsed() >= quota.getApiCallQuota()) {
response.setStatus(HttpStatus.TOO_MANY_REQUESTS.value());
response.getWriter().write("API调用超出配额");
return false;
}
quotaService.recordApiCall(tenantId);
return true;
}
}
3. 分布式环境下的配额控制
使用 Redis 实现分布式计数器,确保并发场景下的配额精确控制:
@Service
public class RedisQuotaServiceImpl implements QuotaService {
@Autowired
private RedisTemplate<String, Long> redisTemplate;
private static final String QUOTA_KEY_PREFIX = "tenant:quota:";
private static final String USAGE_KEY_PREFIX = "tenant:usage:";
@Override
public boolean checkAndConsume(String tenantId, String resourceType, long amount) {
String quotaKey = QUOTA_KEY_PREFIX + tenantId + ":" + resourceType;
String usageKey = USAGE_KEY_PREFIX + tenantId + ":" + resourceType;
Long quota = redisTemplate.opsForValue().get(quotaKey);
if (quota == null || quota <= 0) {
return false;
}
String script =
"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";
Long result = redisTemplate.execute(
new DefaultRedisScript<>(script, Long.class),
Arrays.asList(quotaKey, usageKey),
amount, quota);
return result != null && result > 0;
}
}
四、多租户认证与权限控制
1. 租户识别与认证
public class JwtAuthenticationFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain) throws ServletException, IOException {
String token = extractToken(request);
if (token != null) {
try {
Claims claims = Jwts.parser()
.setSigningKey(secretKey)
.parseClaimsJws(token)
.getBody();
String tenantId = claims.get("tenantId", String.class);
TenantContextHolder.setTenantId(tenantId);
} catch (Exception e) {
response.setStatus(HttpStatus.UNAUTHORIZED.value());
return;
}
}
filterChain.doFilter(request, response);
}
}
2. 细粒度权限控制
使用 Spring Security 实现基于租户的权限控制:
public class TenantSecurityExpressionRoot extends SecurityExpressionRoot
implements MethodSecurityExpressionOperations {
private Object filterObject;
private Object returnObject;
public TenantSecurityExpressionRoot(Authentication authentication) {
super(authentication);
}
public boolean isTenantUser(String tenantId) {
String currentTenantId = TenantContextHolder.getTenantId();
return currentTenantId != null && currentTenantId.equals(tenantId);
}
@Override
public void setFilterObject(Object filterObject) {
this.filterObject = filterObject;
}
@Override
public Object getFilterObject() {
return filterObject;
}
@Override
public void setReturnObject(Object returnObject) {
this.returnObject = returnObject;
}
@Override
public Object getReturnObject() {
return returnObject;
}
@Override
public Object getThis() {
return this;
}
}
五、方案选择与最佳实践
1. 数据隔离方案选择建议
| 因素 | 数据库级别 | 表级别 | 行级别 | | ---
| 隔离性 | 最高 | 中等 | 最低 | | 成本 | 最高 | 中等 | 最低 | | 扩展性 | 低(新增租户需创建库) | 中等(新增租户需创建表) | 高(共享表结构) | | 维护复杂度 | 高 | 中等 | 低 | | 适用租户数量 | 少(<1000) | 中(1000-10 万) | 多(>10 万) |
2. 资源配额控制最佳实践
- 分层控制 :同时实现应用层和基础设施层的配额控制。
- 预付费机制 :支持按使用量计费(Pay-as-you-go)和预付费模式。
- 弹性扩展 :当租户资源使用接近配额时,提供升级提示。
- 监控与告警 :实时监控资源使用情况,设置异常使用告警。
六、总结
设计一个高效、安全的多租户 SaaS 系统需要综合考虑数据隔离和资源配额控制:
- 数据隔离 :
- 数据库级别:适合对隔离性要求极高的场景。
- 表级别:平衡隔离性和成本的折中方案。
- 行级别:适合租户数量庞大的场景。
- 资源配额控制 :
- 设计通用的配额模型,支持多种资源类型。
- 使用 Redis 实现分布式环境下的精确控制。
- 通过拦截器和 AOP 实现透明的配额检查。
- 认证与权限 :
- 从请求中提取租户 ID,建立上下文。
- 基于租户 ID 实现细粒度的权限控制。
在实际项目中,建议根据租户规模、数据敏感性和预算选择合适的数据隔离方案,并通过弹性的资源配额控制机制确保系统稳定运行。通过上述方案,我们成功在多个 SaaS 项目中实现了租户数据的安全隔离和资源的合理分配,支持了从几百到数十万租户的平滑扩展。
猜你喜欢
- 2025-09-13 SDN、NFV、云网融合傻傻分不清?理清它们的来龙去脉与应用逻辑
- 2025-09-13 Java多租户SaaS系统实现方案_多租户 springboot
- 2025-09-13 【推荐】一款开源免费、功能强大的多租户 Saas 快速开发平台
- 2025-09-13 EFCore多租户实现-共享数据库模式
- 2025-09-13 这款 .NET 9 + React 后台权限管理系统太强了!支持多租户、按钮权限(简单易用且文档齐全)
- 2025-09-13 “凭什么要比业主多收钱?”租户抱怨遭区别对待,小区管理方喊委屈……
- 2025-09-13 设计一个多租户 SaaS 系统,如何实现租户数据隔离与资源配额控制?
- 2025-09-13 多租户登录页设计全攻略:如何实现灵活切换与数据隔离?
- 2025-09-13 springboot多租户实现(1):基础架构
- 2025-09-13 一文掌握 Spring Boot 数据库热插拔与多租户支持
- 最近发表
-
- K8s 部署频繁出错?一套流程教你快速定位故障,工作效率翻倍
- 防火墙服务无法启用,显示灰色的解决办法
- 网络问题-电脑无法上网处理思路以及办法 (总集)
- Win10学院:Windows Denfender无法启动怎么办?
- Windows账户登录问题解决方案_登录windows账户什么意思
- IIS无法启动提示计算机上"."的服务W3SVC,一分钟搞定,抓紧收藏
- 已申请到免费顶级域名如何管理_顶级域名免费注册
- 网站被劫持了老是跳转怎么办_网站被劫持到其它网站如何解决
- 这些“常用药”被注销!涉及维生素、去痛片、眼药水等!快看看你家有吗?
- 《皕宋楼藏书志》清 藏书家陆心源与其门人李宗莲合编的藏书目录
- 标签列表
-
- location.href (44)
- document.ready (36)
- git checkout -b (34)
- 跃点数 (35)
- 阿里云镜像地址 (33)
- qt qmessagebox (36)
- mybatis plus page (35)
- vue @scroll (38)
- 堆栈区别 (33)
- 什么是容器 (33)
- sha1 md5 (33)
- navicat导出数据 (34)
- 阿里云acp考试 (33)
- 阿里云 nacos (34)
- redhat官网下载镜像 (36)
- srs服务器 (33)
- pico开发者 (33)
- https的端口号 (34)
- vscode更改主题 (35)
- 阿里云资源池 (34)
- os.path.join (33)
- redis aof rdb 区别 (33)
- 302跳转 (33)
- http method (35)
- js array splice (33)