描述

很多后端系统会用内存缓存提升字典查询性能,典型写法如下:

private final Map<String, String> dictListCache = new ConcurrentHashMap<>();

这类实现简单直接,但在生产环境中如果没有容量边界、过期策略和失效机制,很容易演变为“慢性故障”:内存逐步攀升、Full GC频繁、接口抖动,最终触发节点雪崩。本文基于真实场景抽象,复盘问题链路,并给出可执行修复方案。

正文

事故背景与现象

系统中有一个“数据字典查询”接口,QPS高、参数组合多。上线初期通过ConcurrentHashMap做本地缓存后,接口P95由120ms降到15ms,效果明显。约3周后出现以下症状:

  • Pod内存从1.2GB缓慢增长到3.8GB;
  • Full GC从每天1次上升到每小时10次以上;
  • 字典接口偶发超时,依赖它的下游业务出现级联告警;
  • 重启后短暂恢复,随后问题再次出现。

根因分析

1)缓存无上限,Key持续膨胀

业务把“租户ID + 语言 + 字典类型 + 版本”拼成Key,且版本号变化频繁。由于没有容量限制,Map只增不减,形成典型的无界缓存问题。

2)无过期机制,脏数据长期驻留

字典数据会在后台配置平台变更,但本地缓存没有TTL,旧值长期被命中,导致“线上配置已更新、接口仍返回旧值”。

3)回源缺乏并发保护,形成瞬时打爆

当某些热Key被清理或未命中时,请求并发回源数据库,瞬时把DB连接池耗尽,进一步放大超时。

4)错误认知导致治理滞后

团队认为“ConcurrentHashMap线程安全,所以可直接用于生产缓存”。实际上它只保证并发读写正确性,不提供容量控制、TTL、淘汰策略和统计能力。

关键代码问题示例

public String getDict(String key) {
String cached = dictListCache.get(key);
if (cached != null) {
return cached;
}
String value = queryFromDb(key);
dictListCache.put(key, value);
return value;
}

这段代码有三个典型问题:

  • 仅有get/put,无TTL、无淘汰;
  • 缓存击穿时并发回源;
  • 没有监控指标,问题只能靠故障后观察。

修复策略设计

第一阶段:快速止血

  • 对Key做规范化,去除高频变化维度;
  • 增加定时清理任务,先把最长驻留数据回收;
  • 对回源增加并发阈值和超时保护,避免数据库被拖垮。

第二阶段:替换为有治理能力的本地缓存

建议改为Caffeine,配置最大容量与过期策略,并开放命中率、驱逐数、加载耗时指标。

import com.github.benmanes.caffeine.cache.Cache;
import com.github.benmanes.caffeine.cache.Caffeine;
import java.time.Duration;

private final Cache<String, String> dictCache = Caffeine.newBuilder()
.maximumSize(50_000)
.expireAfterWrite(Duration.ofMinutes(30))
.recordStats()
.build();

public String getDict(String key) {
return dictCache.get(key, this::queryFromDb);
}

第三阶段:建立一致性与失效闭环

  • 配置中心发布变更事件,服务订阅后按Key主动失效;
  • 为热点字典加“逻辑过期 + 异步刷新”;
  • 对强一致场景,改为Redis集中缓存并设置版本戳比对。

方案对比

方案 复杂度 性能 一致性 风险
原生ConcurrentHashMap无治理 初期高 内存失控、脏数据
ConcurrentHashMap+手写清理 维护成本高、边界易漏
Caffeine本地缓存 需配合失效通知
Redis集中缓存 中高 中高 网络依赖与运维成本

可执行

可直接用于生产排查与验收的检查清单:

1. 是否存在无界缓存(未设置容量上限)?
2. 是否设置TTL或主动失效机制?
3. 缓存未命中时是否有并发保护(single-flight/互斥加载)?
4. 是否有命中率、驱逐数、加载耗时、当前大小等指标?
5. 是否定义了变更发布后的缓存一致性策略?
6. 是否进行过压测验证:热点Key、冷启动、批量失效场景?

回归验收建议:

  • 压测30分钟以上,观察堆内存是否稳定在可控区间;
  • 验证配置变更后60秒内全链路读到新值;
  • 人工构造缓存击穿场景,确认数据库QPS不过载。

小结

ConcurrentHashMap适合做并发容器,不适合直接当生产级缓存方案。缓存真正的工程问题不在“线程安全”,而在“生命周期治理”。当系统进入高并发和长生命周期运行阶段,容量上限、过期策略、失效机制与监控指标缺一不可。把缓存从“数据结构”升级为“可观测、可控制、可回收”的基础设施,才能避免同类事故反复出现。