静态变量在分布式单例中无租户隔离,易致跨租户数据污染;须重点扫描public static字段、singleton Bean内可变状态及Serverless模块级变量,并通过双租户日志验证、按场景替换为ThreadLocal/ConcurrentHashMap/显式传参等方案修复。
静态变量在分布式单例环境里是进程级共享的,没有租户边界。一个租户写进去的值,下一个租户线程可能直接读到——这不是并发问题,是设计误用。
重点扫描三类高危位置
别靠猜,直接代码里定位:
- 所有 public static 字段,尤其类型是 String、Map、List、Object 的;特别留意类名或字段名含 “context”“holder”“cache”“config”
- Spring 中声明为 @Component @Scope("singleton") 的 Bean 内部,非 final 的静态字段或可变实例字段(比如没加 volatile 或 synchronized 的状态标记)
- Lambda 函数或 Serverless 初始化阶段定义的模块级变量(Java 的 static、Python 的 module-level dict、Node.js 的 global 属性),尤其在 Init 阶段初始化、Invoke 阶段复用的场景
两步现场验证是否已污染
不用翻历史日志,快速实锤:
- 在关键入口(如 Controller 方法开头)加一行日志:log.info("tid={} | tenant={} | staticVal={}", Thread.currentThread().getId(), TenantContext.getCurrentTenant(), YourClass.STATIC_FIELD);
- 发起两个不同租户的请求(A 和 B),观察 B 请求日志里是否出现了 A 租户写入的 static 值;若出现,即确认污染
- 再顺藤摸瓜:这个 static 值是否被用于拼 SQL、生成缓存 key、做权限判断等敏感路径?只要用了,风险就已落地
按场景替换,不是简单删掉 static
修复要对症,避免换汤不换药:
- 租户上下文传递:改用 InheritableThreadLocal
封装 tenantId,并在 Filter/Interceptor 的 finally 块中调用 remove()(防池化线程残留) - 运行时配置或缓存:禁用 static Map 存租户数据;改用 ConcurrentHashMap + tenantId 作 key,或上 Redis,key 强制带 tenant:{id}: 前缀
- 无状态逻辑:把原 static 工具类方法转为普通实例方法,交由 Spring 管理;或彻底函数化,所有依赖(尤其是 tenantId)必须显式作为参数传入
上线前必须做的兜底检查
静态扫描 + 运行时防护,双保险:
- 用 SonarQube 或自定义 Checkstyle 规则,在 CI 阶段拦截 new static 字段命名含 context/cache/holder 的代码提交
- 在关键 Bean 初始化后,自动触发一次跨租户并发探针测试:模拟多个 tenantId 调用,校验其内部状态是否相互隔离
- 对所有异步执行点(@Async、CompletableFuture、线程池 submit)做代码扫描,确保 tenantId 显式透传或包装在任务载体中
















