租户上下文泄漏本质是生命周期未绑定租户会话,需显式管理threadlocal、异步任务、自定义tenantscope及主动清理离线租户;租户级缓存须以tenant_id为一级key,配合推送失效与运行时诊断兜底。
这个问题本质是租户上下文泄漏,不是单纯“清理不及时”,而是上下文生命周期未与租户会话真实绑定。核心在于:线程、缓存、连接池等资源在租户切换或租户停用后未主动释放,导致内存中残留过期租户的配置、元数据、数据库连接甚至业务对象。
租户上下文必须显式绑定生命周期
不能依赖“请求结束即自动清理”的假设。尤其在异步任务、定时调度、后台线程中,租户上下文极易脱离 HTTP 生命周期。必须强制要求所有租户感知组件(如数据源路由、日志MDC、权限检查器)支持“租户上下文开启/关闭”语义:
- 使用 ThreadLocal 存储租户 ID 时,必须在请求入口(如网关过滤器)设置,并在响应完成或异常捕获块中 显式 remove(),避免线程复用导致污染
- 异步任务(如 @Async、Quartz、消息消费)启动前,需手动传递并绑定当前租户上下文;任务结束时同步清除
- Spring 的 RequestScope 不适用于离线场景,应改用自定义 TenantScope,配合 AOP 在租户操作前后自动启停
离线租户状态需驱动上下文失效
“离线租户”不是静默状态,而是一个明确的系统事件(如租户停用、试用期到期、欠费冻结)。不能等内存自然老化,要主动触发清理:
- 当租户状态变更为 DISABLED 或 EXPIRED 时,立即调用 TenantContextEvictor.evict(tenantId),清空该租户在本地缓存、连接池、动态数据源注册表中的全部条目
- 为每个租户维护一个轻量级 租户活跃句柄(TenantHandle),包含最后活跃时间、关联线程数、缓存键集合。定期扫描过期句柄并执行回收
- 在统一元数据库中为租户增加 last_active_at 和 eviction_flag 字段,清理逻辑可基于此做幂等判断
避免静态缓存成为内存黑洞
常见误区是把租户配置、Schema 映射、权限规则全塞进 static Map 或 Caffeine 的全局缓存。一旦租户下线,这些缓存不会自动失效:
- 所有租户级缓存必须带 tenant_id 作为一级 key,禁止跨租户共享 value 实例(例如不要缓存 Connection,而应缓存 ConnectionFactory 配置)
- 使用 Caffeine.newBuilder().maximumSize(1000).expireAfterWrite(30, TimeUnit.MINUTES) 并配合 tenant_id + key 组合构造缓存 key
- 对高频读取但低频变更的租户配置(如日志级别、功能开关),启用 主动推送失效:租户状态变更时,向所有节点广播
EVICT_TENANT_CONFIG:tenant_123消息
运行时诊断与兜底机制
即使设计周全,仍需可观测性和熔断能力:
- 暴露 /actuator/tenant-contexts 端点,实时返回当前 JVM 中存活的租户上下文数量、最大租户 ID、最久未更新时间
- 设置 JVM 内存阈值告警(如老年代使用率 >85%),触发自动扫描并 dump 所有 tenant 相关对象的 GC Roots
- 为关键组件(如动态数据源管理器)添加 forceEvictStaleTenants() 管理接口,支持运维紧急干预










