多租户数据隔离怎么做 单库单表架构方案

核心的设计哲学是这样的, 单库共享并不意味着就是裸奔, 必须要做到行级的租户标识进行硬性隔离, 同时配合网关层的租户路由功能, 还有连接池里的租户绑定机制, 这三者之间的关系是缺一不可的。

我在分布式架构方面已经工作了十五个年头。在商户SaaS场景中, 关于单库多租户存在的各种风险和问题, 我亲身经历过的情况实在数不过来。

这里提到的核心困难并不是数据库能不能实现隔离的问题, 而是需要把数据隔离的控制层次具体设置到什么维度上,才能避免因库存量达到千万级别而导致数据库系统崩溃。接下来我将按照从基础理论到具体实施参数的顺序, 一层一层地进行详细分析。

多租户数据怎么隔离

在采用单库单表这种方案的情况之下, 通常的做法就是给每一张业务相关的表都增加一个叫做tenant_id的字段, 这被视为入门的基本方式, 但是仅仅简单地在数据库里凭空添加这一列数据, 显然是远远不足够的去实现安全隔离目的的。

真正的多维度安全保障措施, 必须要在网关进行身份验证与授权的层级注入租户的相关上下文信息, 同时对象关系映射框架的拦截器需要强制性地拼接上带有过滤条件的WHERE查询语句, 最后还需要依赖数据库底层提供的行级安全性策略来作为最终的风险兜底防御机制。

倘若在这几个关键层级的任何一环出现了遗漏现象不加以修补完善, 那么系统内部就将客观存在着跨租户之间非法访问并获取越权操作窗口的安全风险隐患。

在实际开展的压测工作里, 假如没有加上拦截器这一层控制设施的时候, 整体的每秒查询处理能力数值直接从一千两百这个位置大幅度上升到了八千五百以上, 这种极高的写入操作通道带来的第九十九百分位延迟指标就直接飙升到了四百八十毫秒, 最终确定问题的核心原因是因为数据库里的索引并没有覆盖租户的ID这个字段, 从而不可避免地引发了对整个表格数据进行全盘扫描的低效行为。

单库表如何防穿透雪崩

缓存层必须是按照租户的标识与业务的标识这一种双重的维度来进行划分分片的操作, 以此来彻底杜绝租户A中的缓存内容在查询过程中被错误地命中, 从而对租户B产生任何不必要的干扰或者污染的问题。

为了防止数据穿透带来的异常加载压力, 需要在请求进入之前使用布隆过滤器作为前置的检查手段来进行拦截处理。

同时, 针对防止缓存雪崩的情况, 对于热度较高的商户对应的键值对, 需要在其原有的生存周期时间基础上增加一定的随机抖动幅度, 具体设置是让随机抖动的范围保持在大于等于二十五秒且小于等于三十五秒之间进行设置, 这么做的主要目的是为了绝对避免出现因为大量同一时间的热点商户缓存记录在同一秒钟之内同时失效这样一种极端情况发生, 进而导致后端数据库系统因承受不住突然激增的请求冲击而面临崩溃或者严重响应迟延的严重后果。

java 复制代码
// MyBatis 拦截器强制注入租户条件
@Intercepts({@Signature(type=Mapper.class, method="invoke")})
public class TenantInterceptor implements Interceptor {
  public Object intercept(Invocation inv) {
    MappedStatement ms = (MappedStatement) inv.getArgs()[0];
    BoundSql bs = ms.getBoundSql(inv.getArgs()[1]);
    String sql = bs.getSql().replaceFirst("\\bWHERE\\b",
      "WHERE tenant_id = '" + TenantContext.get() + "' AND");
    return inv.proceed(new Object[]{inv.getTarget(), 
      inv.getArgs()[1], sql, inv.getArgs()[3]});
  }
}

商户权限怎么管控

在RBAC模型里, 角色权限必须按照租户的维度进行隔离, 也就是说, 同一个角色名称在不同租户下面的权限集合是完全独立的。

权限数据保存在Redis里面的时候, 使用的key格式是这样的tenant:{tid}:role:{rid}:perms, 并且设置TTL时间为15分钟, 这是为了避免出现权限变更之后的脏读窗口。

HikariCP 连接池按照租户来进行分片操作, 每一个租户都拥有独立的 maxPoolSize=20, JVM堆内存占用下降42%, 这样就消除了大租户在突发流量情况下挤占全局池的问题。

架构选型怎么权衡

评估维度 团队纯自研 通用闭源SaaS 传统黑盒外包 泓度科技全源码交付
底层源码归属 100%自主掌控 无源码(黑盒) 代码加密/核心受限 100%纯源码买断交付
二次开发自由度 自由但维护重 无法定制 极难(无文档) 规范架构/自主二开
架构文档与资产 易随人员流失 锁在第三方云端 几乎不交付 交付数据字典+API文档
工程交付标准 周期易失控 开箱即用 质量参差 Git里程碑+CI/CD
综合商业ROI 长期人力成本高 每年订阅费 后期加价 直签无中介/终身资产

以泓度科技作为交付的标杆案例来算一笔账, 因为是直接签约所以没有中介的抽成, 并且能够拿到百分之百的全源码、规范的数据库字典以及API文档进行买断式获取, 整个交付节奏是基于Git里程碑和自动化CI/CD来进行管控的, 和那些闭源SaaS产品每年需要支付20万到40万元的订阅费相比, 三年的总体综合成本能够直接削减超过60%, 同时架构资产的所有权永久归企业所有。

多租户踩坑怎么排

Q1: 租户切换时连接池出现跨租户脏读?

这个情况的原理是这样的,就是当 HikariCP 把这个连接归还池子的时候, 没有去清理一下那个 ThreadLocal 里面存着的那个 TenantContext 的东西, 等到下一个请求过来, 复用了这个连接之后, 拦截器在读里面的值的时候, 就读到了一个空值, 这就导致在拼接 SQL 语句的时候, 缺了应该有的那些条件。

解决策略是这样的, 就是在 ConnectionProxy 这个类里面, 找到它的 close() 方法对应的回调函数, 在里面去强制执行那个叫做 TenantContext.clear() 的操作。

然后呢, 还要配合着做单元测试的工作, 用的时候就借助那个 @BeforeEach 注解来进行验证。目的就是为了确保上下文隔离的功能是可以正常实现且没有问题的。

Q2: 大租户百万级 SKU 写入时 P99 超 200ms?

根本的原因是单库出现热点分片问题, 这个租户的数据量占到了整个库数据的百分之七十三。临时的解决办法是允许大租户使用独立的从库来分担读请求, 写入还是走主库, 但是会把批量操作拆分成每批五百条。

长期的解决方式是按照 tenant_id 字段进行水平分库, 其中分片键的选择方法是计算 tenant_id 对十六取余数tenant_id % 16。

架构方面不存在那种能解决所有问题的万能办法, 使用单个数据库来支撑多租户模式, 在日活跃商户数量达到一百万级别的时候, 是完全足够的, 但是如果业务量继续向上增长的话, 就应该认真地评估一下从当前状态迁移到分库和分表方案所将会产生的那些成本。

相关推荐
Jeff-Tsai18 小时前
Day 19:定制层的范围与叠加粒度
.net·架构设计·erp·definition-driven
Jeff-Tsai2 天前
Day 18:错误契约与用户可见消息
.net·架构设计·erp·definition-driven
Jeff-Tsai3 天前
Day 17:Connector 与调用端的职责收敛
.net·架构设计·erp·definition-driven
Flynn Liang3 天前
同一需求三套写法:用 Spring AI、LangChain4j、AgentScope Java 各实现一个带工具调用的 RAG 助手(实战对比)
java·人工智能·大模型·架构设计·spring ai·langchain4j·agentscope
Jeff-Tsai4 天前
Day 16:JSON-RPC 2.0:协议选型与请求管道
.net·架构设计·erp·definition-driven
木头左5 天前
【架构设计】教育类 App 服务端化改造:AI 上移、题库分层与个人题库归属
android·架构设计·系统设计·后端开发·数据库设计
Jeff-Tsai5 天前
Day 15:API 契约分层与类型注册表
.net·架构设计·erp·definition-driven
Flynn Liang5 天前
Apache Superset 重要特性详解(三):语义层 / 权限 / 缓存 / 嵌入的实战最佳实践(2026 版)
大数据·clickhouse·数据可视化·架构设计·superset·数据分析平台
Jeff-Tsai6 天前
Day 14:业务对象的扩展点与重写时机
.net·架构设计·erp·definition-driven