租户标识必须在请求最早期捕获并绑定,不可延迟至controller或service层;需在userouting后、useauthentication前通过中间件提取host/路径/head中的tenantid,存入httpcontext.items或asynclocal,全程可控且自动清理。
关键在于不让租户标识“漂浮”在线程里,而是从请求入口就捕获、绑定、清理——全程可控,不依赖隐式继承。
租户标识必须在请求最早期提取
不能等到 Controller 或 Service 层才去取租户 ID。此时线程上下文可能已被复用,或 DbContext 已初始化完毕,隔离已失效。
- ASP.NET Core 中,中间件需放在 UseRouting 之后、UseAuthentication 之前
- Spring Boot 中,Filter 或 WebMvcConfigurer 的拦截器应在 DispatcherServlet 前生效
- Go 的 http.Handler 必须用 context.WithValue 显式注入,绝不可写死在闭包或全局变量中
用安全容器承载租户上下文,而非裸 ThreadLocal
ThreadLocal 在虚拟线程场景下极易污染:载体线程被复用时,未 remove 的值会残留给下一个租户。
- Java 21 推荐用 ScopedValue
替代 ThreadLocal —— 不可变、作用域明确、自动清理 - 若仍用 ThreadLocal,必须配对使用
set()和remove(),且包裹在 try-finally 中 - C# 推荐用 AsyncLocal
,它能随异步上下文流动,比静态字段安全得多
全局捕获器要覆盖所有入口通道
一个系统不止有 HTTP 请求,还有定时任务、消息消费、WebSocket、gRPC 等。每种入口都要独立捕获租户上下文。
- HTTP:从 Host / X-Tenant-ID / 路径前缀(如
/t/{id}/api)提取 - 消息队列(如 Kafka/RabbitMQ):要求生产者在消息头(headers)中携带 tenant_id
- 定时任务:无法自动识别租户,需显式配置每个任务所属租户,启动时手动绑定
- gRPC:通过
Metadata传递,并在 ServerInterceptor 中注入 Context
绑定后必须参与后续所有关键环节
捕获只是起点,上下文要真正驱动隔离行为:
-
数据访问层:JPA 全局过滤器、MyBatis 插件、EF Core Query Filter 都需读取当前租户 ID 并自动追加
WHERE tenant_id = ? -
连接池与监控:数据库连接字符串加入
Application Name=tenant-{id},便于链路追踪和资源限流 - 日志与告警:MDC(Mapped Diagnostic Context)或 log4j2 的 ThreadContext 注入 tenant_id,确保每条日志自带租户标签











