DDD 多租户设计到底该不该用 AsyncLocal?90% 项目一开始就选错了


引言:一个被反复问错的问题

在 DDD 项目里,只要一谈到多租户,几乎必然会出现这个问题:

“多租户用 AsyncLocal 行不行?”

甚至很多项目在一开始,就已经默认答案是 “行,而且很方便”

但如果你认真从 DDD 的视角 来审视这个问题,就会发现:

这不是一个技术技巧选择问题,而是一个架构边界问题。

而遗憾的是,90% 的项目在第一步就选错了方向。


多租户在 DDD 中到底是什么?

先把一个最关键的概念说清楚:

在 DDD 中,多租户不是运行时上下文,而是业务事实(Business Fact)。

也就是说:

  • 租户 ≠ 当前线程信息
  • 租户 ≠ 技术上下文
  • 租户 = 业务规则的一部分

它直接影响:

  • 聚合的合法性
  • 实体的可访问范围
  • 领域不变量
  • 授权与边界上下文

一个订单属于哪个租户,本质上和它的金额、状态同一个层级。


AsyncLocal 为什么会“看起来很香”?

我们先承认现实:
AsyncLocal 在工程上确实很诱人。

它解决了三个让人头疼的问题:

  • 不想层层传参
  • 不想污染方法签名
  • 不想每个构造函数都带 TenantId

于是很多项目会这样做:

TenantContext.Current = tenantId;

然后在任意地方直接读取:

var tenantId = TenantContext.Current;

零侵入、零参数、全局可用。

从“写代码爽”的角度看,它几乎是满分。


但这正是问题的开始

1️⃣ AsyncLocal 隐式地破坏了依赖边界

DDD 的一个核心原则是:

依赖必须显式,而不是隐式。

而 AsyncLocal 带来的恰恰是:

  • 看不见的依赖
  • 无法从构造函数判断对象是否依赖租户
  • 无法从方法签名判断业务前置条件

你看到的方法是:

PlaceOrder(order);

但真实的前提是:

“当前 AsyncLocal 中必须已经存在合法的 TenantId”

这在 DDD 中是不可接受的。


2️⃣ 它把业务事实伪装成了技术上下文

AsyncLocal 本质是:

  • 线程 / 异步流上下文存储
  • 技术设施级能力

但多租户是:

  • 业务边界
  • 授权约束
  • 数据隔离规则

当你用 AsyncLocal 承载租户时,相当于在说:

“这个业务规则是可选的,只是运行时环境的一部分。”

这是一个语义层级的严重错误


3️⃣ 它让领域模型“看起来纯净,实际上脆弱”

很多人用 AsyncLocal 的真实动机是:

“我不想让 Domain / Application 层看到 TenantId。”

于是你得到一个:

  • 表面很干净
  • 实际高度依赖运行环境
  • 离开 Web 请求就会出问题的领域模型

一旦出现下面场景:

  • 后台任务
  • 消息消费
  • 批处理
  • 单元测试
  • 并发执行

你就会发现:

领域模型根本不知道自己运行在什么租户下。


显式依赖:DDD 里“难但正确”的选择

在 DDD 中,多租户的正确姿势只有一个关键词:

显式

显式意味着什么?

  • TenantId 是构造参数 / 方法参数
  • 聚合创建时就绑定租户
  • Repository 查询明确以 TenantId 作为条件
  • Application 层显式控制租户边界

例如:

public sealed class Order
{
    public TenantId TenantId { get; }

    public Order(TenantId tenantId, ...)
    {
        TenantId = tenantId;
    }
}

这带来的好处是:

  • 业务前置条件一目了然
  • 单元测试不依赖运行环境
  • 架构边界清晰
  • 错误更早暴露

是的,它更啰嗦, 但它是 DDD 语义上正确的设计


那 AsyncLocal 就一无是处吗?

不是。

AsyncLocal 适合的场景只有一个:

技术上下文的传递,而不是业务建模。

例如:

  • TraceId
  • CorrelationId
  • 审计信息
  • 日志上下文

如果你把 AsyncLocal 用在这里,它是加分项。

但一旦你用它承载:

  • TenantId
  • UserId
  • 业务身份
  • 授权边界

那它就变成了架构妥协


为什么 90% 的项目一开始就选错了?

因为他们在用下面的逻辑做决策:

  • “这样写最省事”
  • “大家都这么用”
  • “框架也是这么干的”
  • “后面再重构吧”

但 DDD 的现实是:

多租户是地基问题,不是重构级别的问题。

一旦方向错了,后面只会不断打补丁。


结论:这不是技术选择,而是架构立场

如果你真的在做 DDD:

  • 多租户必须是显式依赖
  • 租户必须是业务事实
  • 领域模型必须知道自己属于谁

AsyncLocal 不是“错”, 但它不该出现在多租户建模中。


微信订阅号二维码