ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

多租户数据隔离架构设计:从选型到代码落地的实操指南

多租户数据隔离架构设计:从选型到代码落地的实操指南

最近不管是技术社区还是团队内部的架构评审,多租户、数据隔离这几个词的出镜率都高得离谱。尤其是 dify 社区版 1.10 把多租户作为一个核心卖点之后,我身边不少做 SaaS 的朋友都在研究这事。其实多租户这套东西说穿了并不玄乎,核心就一个问题:一套系统服务 N 家客户,数据如何既隔得开、又不至于把运维成本和性能拖垮。

我这些年经手过不少多租户系统的设计和改造,从最初几家大客户的私有化部署,到现在上百家租户共用一个集群,各种隔离方案都试过。这篇就整理一份可以直接落地的多租户数据隔离参考架构,把方案选型、代码改造、常见翻车点一次讲透。无论你是刚接手公司 SaaS 项目的后端开发,还是正在规划平台化的技术负责人,这篇里的实操经验应该都能帮你少走不少弯路。

1. 先弄清楚:多租户数据隔离到底要隔离什么

1.1 隔离的本质是租户维度的边界

很多人一提数据隔离,第一反应就是“把数据分开存”。但分开存只是手段,多租户隔离的本质是在应用层建立一个贯穿全链路的租户维度边界。什么意思?就是说无论是用户登录、接口请求、后台任务还是报表查询,系统在任何时刻都要知道当前操作属于哪个租户,并且所有数据访问都被强制限制在这个租户的范围内。

这个边界建立不起来,就会出现两类问题:一类是恶意的越权访问,比如一个用户通过改 URL 参数就能查到别的公司的数据;另一类是更隐蔽的逻辑漏洞,比如定时任务里忘记带租户条件,结果某个租户的统计数据里混进了所有租户的数据。第二类往往比第一类更致命,因为它不是被攻击,而是系统自己制造的数据污染。

我见过一个真实的案例:某 SaaS 平台的报表模块在生成月度账单时,查询条件里漏掉了租户 ID,结果所有租户看到的账单数据都是全平台累计的。这种事故一旦发生,客户信任基本就归零了。所以隔离方案设计的首要任务不是选数据库,而是想清楚租户边界如何在代码的每一层传递和被强制执行。

1.2 三种租户识别方式

要在系统里建立一个租户维度边界,第一步是解决“怎么知道当前请求属于哪个租户”的问题。实际项目里常用的有三种识别方式:

第一种是独立域名或子域名方式,比如 tenantA.example.com 和 tenantB.example.com。这种方式最直观,用户访问的 URL 天然带着租户信息,还可以配合 CDN 和独立 SSL 证书做差异化配置。但缺点也很明显:DNS 配置和证书管理在大规模租户场景下会变得很繁琐,而且测试环境要模拟多租户域名也比较麻烦。

第二种是请求 Header 或 Token 携带租户信息。现在绝大多数 SaaS 架构采用 JWT 做认证,租户 ID 作为 Token 里的一个自定义 Claim 一起签发。后端从请求头里解析出租户 ID,放入上下文。这种方式灵活,不依赖 DNS 配置,也方便在 API 网关层统一处理,是目前最主流的做法。

第三种是 URL 路径参数方式,比如 /api/{tenantId}/orders。这种方式老项目改造时比较方便,但风险也最大:如果后端没有严格校验路径参数和当前登录用户所属租户的一致性,就很容易被水平越权。一般不建议新项目直接用这种方式,除非有网关层做租户映射。

我实际做改造时的偏好是第二种为主,域名方式用于大型客户的门户入口,路径参数只在某些公开接口的兼容场景下使用。识别方式确定后,接下来要做的就是把这个租户 ID 从请求入口一路传递到 SQL 执行层。

2. 三种主流隔离模式怎么选

2.1 独立数据库模式:最稳但也最贵

独立数据库模式,也就是每个租户一套独立的数据库实例或独立库。这是隔离强度最高的方案,租户之间在物理层面完全隔离,无论数据备份、恢复、扩缩容还是性能调优,都可以针对单个租户独立操作。

这种模式的优点很直观:安全级别最高,完全不存在租户间串数据的可能;数据库故障的影响面也小,一个租户的库挂了不会拖累其他租户;还能给大客户提供专门的资源保障和定制化部署。

但缺点同样突出。首先是成本,数据库实例数量随租户数线性增长,哪怕每个库只占几十 MB,也要为每个实例付出独立的内存和进程开销。其次是运维复杂度,一百个租户就是一百套备份任务、一百套监控告警、一百套升级流程。哪怕用云数据库托管服务,日常管理仍然是个不小的工作量。

所以这个模式的实际适用范围很窄:要么是客单价极高的政企大客户,要么是有强合规要求(比如金融行业必须做到数据物理隔离)的场景。如果你是做通用型 SaaS,全量用这种模式基本等于烧钱。

2.2 共享数据库、独立 Schema:均衡方案

共享数据库、独立 Schema 模式,是指所有租户共用一个数据库实例,但每个租户拥有自己独立的 Schema(在 MySQL 里可以近似理解为独立库,在 PostgreSQL 里则是真正的 Schema)。

这种方案的隔离级别是逻辑层隔离,每个 Schema 内的表结构可以独立演进,应用层通过切换 search_path(PostgreSQL)或者切换数据库名(MySQL)来实现租户路由。由于数据仍然物理上存放在同一个实例中,数据库实例的数量不会随租户数暴涨,硬件成本相对可控。

它的优点在于隔离性和成本之间取得了较好的平衡:一个租户的误操作一般不会直接破坏另一个租户的数据结构;备份也能按 Schema 做精细化处理;租户之间的数据在存储层就有天然边界。缺点是单个数据库实例的资源始终是共享的,一旦某个租户的数据量特别大或者某条 SQL 特别慢,仍然可能拖累同实例上的其他租户。

我的经验是,它特别适合租户数量在几十到几百之间、单租户数据量中等、且存在定制化表结构需求的平台型产品。比如不少企业服务类 SaaS 需要给不同客户开放自定义字段,独立 Schema 就能相对安全地支持这类需求。

2.3 共享表、租户 ID 维度:最常用的取舍

共享表模式是所有租户共用同一组表,通过表里的租户 ID 字段来区分数据归属。这是互联网 SaaS 产品里最普及的方案,几乎所有快速增长的云原生应用初期都会选择它。

它的最大优势是资源利用率高,一张表可以服务所有租户,不需要维护成百上千个 Schema;开发和迭代效率也高,加字段、加索引只需要操作一套表结构;数据聚合分析非常方便,跨租户的统计查询天然就能实现。

但这个模式最考验的是应用层的隔离纪律。你必须在所有 SQL 查询中强制携带租户条件,一条 SQL 漏了 where tenant_id = ?,后果就是数据串号。而且表数据量会随着租户增长迅速膨胀,索引设计、分区策略、慢查询治理都是后续长期要做的事情。

现实中很多团队一开始图省事选择共享表,后来遇到大客户要求更高隔离级别,再想办法迁移。其实这个演进路径是合理的,共享表方案本身没有问题,问题在于有没有从一开始就把租户上下文的传递机制做好。只要上下文机制是健壮的,从共享表迁移到独立 Schema 只是数据搬迁的事。

2.4 选型决策:按客户、成本、合规三个维度权衡

三种模式各有适用场景,我给的建议是不要一步到位,而是按客户维度做混合隔离:

  • 免费试用版/低价值客户:共享表,成本优先
  • 中小付费客户:共享表或独立 Schema,看是否需要定制表结构
  • 大客户/强合规客户:独立 Schema 或独立库,安全优先
  • 需要物理隔离的金融政务客户:独立库

成本侧要注意一个容易被忽略的细节:看似省钱的共享表模式,随着租户和数据量增长,为了扛住压力往往会引入读写分离、分库分表、分布式缓存等复杂基础设施,这些总拥有成本未必比一开始用独立 Schema 低。合规侧则要看客户合同里的条款约定,有些客户会明确要求数据存储与其他租户物理隔离,这时候不管代码层做得有多好,都必须用独立库来兜底。

所以参考架构在选型上的结论是:默认采用共享表 + 强隔离上下文机制,预留独立 Schema 和独立库的升级路径,让隔离级别本身成为可配置项。

3. 把隔离真正落到代码里的关键步骤

3.1 租户上下文的建立与传递

代码层的多租户改造,第一步是把租户上下文这个基础设施搭好。租户上下文的核心是一个全局的线程级变量,用来保存当前请求正在处理哪个租户。

我以 Java 技术栈为例,最常见的方式是用 ThreadLocal 包一层:

public class TenantContext { private static final ThreadLocal<String> CURRENT_TENANT = new ThreadLocal<>(); public static void setTenantId(String tenantId) { CURRENT_TENANT.set(tenantId); } public static String getTenantId() { return CURRENT_TENANT.get(); } public static void clear() { CURRENT_TENANT.remove(); } }

这个类本身很简单,但它有几个使用上的关键纪律。第一,必须在请求进入时设置租户 ID,通常在 Filter 或拦截器里从 JWT 或 Header 解析出来。第二,必须在请求结束时清理 ThreadLocal,否则线程池复用的线程会带着上一个租户的上下文处理下一个请求,这是数据串号的头号原因。

@Component public class TenantFilter implements Filter { @Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { HttpServletRequest httpRequest = (HttpServletRequest) request; String tenantId = httpRequest.getHeader("X-Tenant-Id"); if (StringUtils.hasText(tenantId)) { TenantContext.setTenantId(tenantId); } try { chain.doFilter(request, response); } finally { TenantContext.clear(); } } }

如果你用的是 Python/Flask 或 Node.js,思路完全一样,只是换成 contextvars 或者 AsyncLocal。关键是理解一点:租户 ID 不是某个业务代码里随手取的参数,而是贯穿整个调用链的上下文状态。任何异步线程、定时任务、消息消费者内部要使用租户上下文时,都必须显式传入或者从调用链中继承,绝不能在子线程里直接读主线程的 ThreadLocal。

3.2 数据访问层如何自动附加租户条件

上下文有了之后,最难的就是保证所有数据库查询都自动带上租户条件。人工在每条 SQL 里手写 where tenant_id = ? 是不现实的,总会有遗漏。真正的解法是在数据访问层做一个统一的拦截器,自动改写 SQL。

MyBatis 的 Interceptor 是一个典型的实现位置。在拦截器里解析即将执行的 SQL 语句,如果是 SELECT、UPDATE、DELETE,就自动拼接租户条件。需要注意 UPDATE 和 DELETE 必须强制加租户条件,否则就可能出现跨租户误改数据。

实现上有几点经验值得分享:

第一,只对包含目标表名的 SQL 做拼接,系统表、字典表、跨租户的全局配置表要加入白名单。第二,INSERT 语句不需要改写 where 条件,但需要在应用的实体层自动填充 tenant_id 字段,避免开发人员忘记赋值。第三,SQL 改写要考虑到别名、子查询、JOIN 等复杂语法,简单的字符串拼接很容易出错,更稳妥的方案是抽象出通用的 SQL 解析器,或者基于规范化的表名映射做处理。

如果你的团队用的是 JPA/Hibernate,同样有类似的多租户过滤器可以配置。但无论用什么框架,我都建议把“自动加租户条件”当成一个独立的基础设施模块来做单元测试,测试用例覆盖普通查询、聚合查询、连表查询、子查询、批量更新这几类典型场景。

提示:多租户拦截器里的 SQL 改写是所有改造中最危险的部分。上线前一定要用生产环境的真实慢日志做一轮全量 SQL review,确认没有一条 SQL 会被错误改写或漏改写。

3.3 数据源与连接池的处理

隔离模式决定了数据源的路由方式。共享表模式下,所有租户都用同一个数据源,无需动态切换,连接池配置也最简单。独立 Schema 模式下,需要在应用运行时按租户切换到不同的 Schema,也就是动态数据源路由。

Java 生态里最常用的方案是继承 AbstractRoutingDataSource,重写 determineCurrentLookupKey 方法,从 TenantContext 中获取租户 ID,然后根据租户 ID 返回对应的数据源 key。

public class TenantRoutingDataSource extends AbstractRoutingDataSource { @Override protected Object determineCurrentLookupKey() { return TenantContext.getTenantId(); } }

不过这里有个关键问题:连接池是按租户维度建立的,每个租户的数据源都会占用一定数量的数据库连接。如果租户几百个,每个租户的连接池配置 5 个连接,那连接总数就非常可观了。所以实际做动态数据源时,建议把连接池的核心参数调小,或者直接为所有租户使用同一个连接池,通过 schema 名来做路由,而不是按租户建立独立连接池。

我在实践中更推荐后一种方式:数据库连接还是用统一的连接池,只是在执行 SQL 前动态切换当前连接的 schema。PostgreSQL 里的实现是执行 SET search_path TO tenant_xxx,MySQL 则是 USE tenant_xxx。这样连接数不会随租户数膨胀,路由开销也非常小。只有独立数据库模式才真正需要按租户建独立数据源和独立连接池。

4. 实际动手:把一个单体应用改造为多租户架构

4.1 存量表结构如何改造

很多场景不是从零搭建新系统,而是把已经跑了好几年的单体系统改造成多租户。这里最大的难点是存量数据怎么打上租户标记。

第一步是梳理所有业务表,找出哪些是需要按租户隔离的,哪些是全局共享的。我一般会定一个简单的规则:凡是与业务数据直接相关的表(订单、用户、商品、账单、消息等)都必须有 tenant_id;凡是系统配置类、枚举类、平台管理类的表可以不加,但在访问时要明确标注为全局表。

第二步给业务表加 tenant_id 字段。这个字段建议用字符串类型(UUID)而不是自增数字,因为多个租户的数据在同一张表里,数字主键已经存在,再用数字做租户标识很容易混淆。tenant_id 加上后,要建立 (tenant_id, 业务主键) 的联合唯一约束,防止不同租户的数据在业务索引层面互相干扰。

第三步是存量数据的回填。如果原来的系统是单人户的,所有存量数据都属于同一个内部租户,需要把这批数据统一打上这个内部租户的标识。如果原来的系统是多套独立部署的,则需要把每个部署的数据迁移到新系统中,按原部署标记租户 ID。这一步建议通过离线脚本完成,并且在迁移完跑一遍全表校验:每条业务数据都有且仅有一个合法的 tenant_id。

4.2 在入口层注册租户上下文中间件

表结构改完之后,接下来在应用层接入租户上下文。以 Spring Boot 项目为例,做法是把自己实现的 TenantFilter 注册到过滤链里。需要注意一个问题:Filter 的执行顺序,租户过滤器的顺序要放在认证过滤器之后、业务 Controller 之前。因为要先通过认证解析出用户身份,再根据用户所属租户设置上下文。

顺序错了会出现一个很隐蔽的问题:匿名接口(比如用户未登录时访问的登录接口)也需要租户上下文,如果认证过滤器先执行,未登录请求直接就被拒掉或者跳过,租户上下文就无法设置。我的建议是租户过滤器放在最外层,先按请求头里解析到的租户信息设置上下文,后续所有组件(包括认证)就都能访问到。

对于移动端或第三方开放平台接入的场景,Header 可能不是 X-Tenant-Id,而是藏在签名参数里。这种情况建议在 API 网关层做一次统一转换:所有进来的请求,网关负责识别租户身份并追加统一的租户 Header,后端服务无感知,这样的架构最干净。后端只认规范化的 Header 名,不用关心租户信息是从域名、Token 还是签名参数里解析出来的。

4.3 用拦截器统一处理 SQL 级隔离

结构改造和上下文之后,核心是 SQL 隔离层。我给出一个 MyBatis 拦截器的最小实现思路,方便你照着自己的框架改造:

@Intercepts({ @Signature(type = StatementHandler.class, method = "prepare", args = {Connection.class, Integer.class}) }) public class TenantLineInterceptor 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 = TenantContext.getTenantId(); if (StringUtils.hasText(tenantId) && isTenantTable(originalSql)) { String newSql = SqlUtils.appendTenantCondition(originalSql, tenantId); Field sqlField = boundSql.getClass().getDeclaredField("sql"); sqlField.setAccessible(true); sqlField.set(boundSql, newSql); } return invocation.proceed(); } }

这个实现虽然可以用,但我必须诚实地说:真正上线时,字符串级别的 SQL 改写会有各种边界情况。比如 SQL 里已经存在同名的 tenant_id 条件、子查询里带着同名字段、函数内部出现字符串包含 where 关键字等等。所以更可靠的方案是基于 Druid 或 JSqlParser 这类成熟的 SQL 解析库,把 SQL 解析成语法树,在语法树层面注入条件再重新生成 SQL。

另外,为了将来从共享表平滑升级到独立 Schema 模式,我建议把租户条件注入的逻辑做成可配置的。配置文件里定义一个 tenant.mode 参数,值为 shared 时自动加 where tenant_id = ?,值为 schema 时自动切换 search_path,值为 db 时自动路由数据源。业务代码完全不感知隔离级别的变化,这是参考架构里很关键的一层抽象。

5. 最容易翻车的坑和排查手册

5.1 定时任务和异步线程丢失租户上下文

多租户系统里最隐蔽的数据串号问题,几乎都是从一个被遗忘的异步线程开始的。主线程里 TenantContext 设置得好好的,但一旦代码里出现了 @Async、消息消费者、定时任务或 CompletableFuture,子线程就拿不到父线程的 ThreadLocal,自然也就丢了租户上下文。

一个行业里常见的失误发生在定时任务场景。比如每天凌晨跑批量结算任务,代码里遍历所有租户循环处理:

for (String tenantId : tenantList) { TenantContext.setTenantId(tenantId); settlementService.settle(); // 如果这里内部又开了异步线程,上下文就丢了 }

看起来蛮正常的。但如果在 settle 方法里用了并行流或者线程池去批量处理订单明细,子线程里完全没有租户信息,SQL 拦截器一看 TenantContext 为空,可能就直接跳过了租户条件注入。结果就是所有租户的订单都算到一起。

解决方案有两个,缺一不可。第一,线程池的任务提交时,显式把租户 ID 作为任务参数传入,任务内部主动设置上下文。第二,兜底策略:如果 SQL 拦截器检测到需要隔离的表没有被注入租户条件,直接抛出异常,而不是静默执行。也就是说,宁可系统报错,也不能让数据在无租户边界的情况下被访问。

注意:这是多租户系统里一条铁律——没有租户上下文的访问必须被视为致命错误,而不是正常请求。把漏租户的 SQL 默认拦截成异常,能救你无数次。

5.2 缓存和消息队列里的租户隔离

数据隔离不能只看数据库。Redis 缓存和消息队列里的 key、topic 同样需要租户维度。

常见的坑是缓存 key 没有带租户前缀。比如用户信息缓存的 key 设计成 user_123,租户 A 和租户 B 的用户 ID 都是 123,那就互相覆盖了。设计缓存 key 的规范应该是 tenantId:bizType:bizId,比如 t001:user:123。做法虽然简单,但需要形成编码规范,否则后面排查起来非常费劲。

消息队列那边的问题更隐蔽。消费者处理消息时需要知道这条消息属于哪个租户,而消息体里不一定包含租户字段。我建议在消息生产时,把租户 ID 写入消息头,消费者在监听方法里解析消息头并设置上下文。还有一种场景是延迟队列或重试队列,要注意从队列重新投递的消息也要重新设置租户上下文,不能依赖上一个消费者遗留的状态。

搜热词的时候看到 dify 社区版 1.10 的多租户更新,本质上也是在做同样的处理。像这种 AI 平台类的产品,多租户除了业务数据,还要考虑知识库索引、向量数据库集合、模型调用配额的多租户维度,逻辑和这里说的一致,只是多了几个需要隔离的存储层。

5.3 报表导出和后台管理漏过滤

报表导出是另一个高发事故区。很多系统里业务页面都用了 MyBatis 拦截器自动加租户条件,但报表模块因为性能原因经常直接写原生 SQL 或者用独立的数据查询服务,绕过了常规的数据访问层。跑批导出的时候如果没有在 SQL 里手动加租户条件,导出来的就是全量的数据。

我曾经排查过一个客户投诉案例:业务人员导出本租户的销售报表,结果 Excel 里出现了几万行其他公司的数据。排查了半天,发现那个导出接口为了性能用 JdbcTemplate 直接执行了一条动态拼接的 SQL,而 JdbcTemplate 不在 MyBatis 的拦截范围内。所以这里必须补充两条规范:第一,所有绕过主数据访问层框架的数据库操作,必须由开发者在代码 review 阶段明确确认租户条件;第二,报表查询走独立的网关入口,由报表服务统一拼接租户条件,避免业务端各自为政。

后台管理系统也不能放松。管理员可能拥有跨租户的查询权限,负责运营的人需要看到一个租户一个租户的数据。这种场景下的建议是建立“数据权限”体系:查询逻辑同时校验用户的角色权限和租户范围,管理员可以切换租户身份,但切换动作要留审计日志。这里不用强制所有后台查询带单租户条件,但必须保证每个查询都在明确的租户范围内执行。

5.4 快速定位异常的两条排查路径

当数据串号、租户上下文丢失这类问题发生时,高效的排查路径比直接翻代码重要得多。

第一条路径是看日志。必须在租户过滤器中添加 MDC 变量,把租户 ID 打入日志上下文。这样所有业务日志都会自动带上 [tenantId=t001] 这样的前缀,排查时 grep 单个租户的日志即可快速定位。没有租户日志前缀的日志系统,多租户排查基本靠猜。

第二条路径是开 SQL 审计。在测试环境或灰度环境开启 SQL 日志输出,检查每条 SQL 是否都有租户条件。自动化一点的做法是写一个集成测试,模拟多租户并发访问,断言每个租户查询到的数据集只包含自己的数据。这个集成测试的意义不只是验证当前代码,更是防止未来新同事接手时不小心破坏隔离逻辑。

下面是几张常见问题速查表,我贴出来供你参考:

问题现象可能原因解决思路
租户 A 能看到租户 B 的数据拦截器未覆盖某些查询路径检查所有 DAO 是否走统一访问层,为绕过访问层的查询补租户条件
某条 SQL 无租户条件但未报错拦截器把该表识别为全局表检查表白名单配置,确认该表确实需要全局共享
异步任务处理的数据归属错乱子线程丢失租户上下文任务提交时显式传入租户 ID,子线程内重新设置上下文
缓存数据错乱缓存 key 缺少租户维度统一改成 tenantId:业务类型:业务ID 格式,清理历史脏数据
定时报表数据偶发偏差报表 SQL 拼接了租户条件但使用 JOIN 时产生笛卡尔积检查 JOIN 条件中是否漏掉租户关联,必要时用 ROW_NUMBER 去重

6. 关于多租户数据隔离,说点实在的

整套参考架构讲到这里,我把核心脉络再串一下:多租户数据隔离不是选一个数据库方案就完事,而是识别方式、隔离模式、上下文传递、SQL 注入、异步链路、缓存消息、运维排查这一整套体系的组合。刚开始改造时不要追求一步到位做到独立库,而是先把共享表和强隔离上下文机制做扎实,再叠加动态切换能力,这样成本可控、演进路径也清晰。

在我个人的实操经验里,有几个点值得你再三确认。

第一个是租户 ID 无处不在的原则。我见过太多系统在刚开始做多租户时只给主表加了 tenant_id,但关联表、日志表、导入记录表、操作审计表全都没有。一旦业务流程覆盖到这些表,数据归属就乱了。所以在设计阶段画一遍全平台的数据模型,把所有可能存业务数据的表都列出来,逐个标记隔离策略,这一步一定不要省。

第二个是尽量早地建立“无租户上下文即异常”的兜底机制。做多租户最怕的不是报错,而是静默地查出串号数据。宁可让一条 SQL 因为缺少租户条件抛异常导致整个接口报错,也不能让它在无边界状态下执行。这个策略在上线早期会暴露不少漏网之鱼,但排查完这些之后,系统会进入一个非常稳定的状态。

第三个是隔离模式不要锁死在一种方案上。客户需求是变化的,今天很多客户接受共享表,明天可能有几个大客户提出数据物理隔离的合规要求。所以从一开始就把 TenantContext 抽象好、把数据源路由做成可配置,将来切换模式时就是改配置加迁移脚本的事,而不是推倒重写。这个扩展性价值会在平台发展到一个阶段后体现得淋漓尽致。

一点小技巧:如果团队里多人并行开发多租户功能,建议约定一个简单的代码规范——任何新增的 DAO 方法、任何新增的业务表,都必须写一个“分租户查询”的单元测试用例,验证同一方法是能正确区分不同租户的数据。这个习惯坚持下来,多租户相关的回归问题会少一大半。

多租户数据隔离说难不难,说简单也不简单,核心就是纪律和细节。希望这份参考架构能帮你把思路理清楚,少踩几个我踩过的坑。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进