堆外缓存让GC垃圾回收不扫描缓存数据 我们之前做的C端商品服务要缓存的数据量很大全部放到Caffeine这种堆内缓存里并不合适。因为缓存对象都在JVM堆里GC时需要扫描这些对象。缓存越大GC压力越大停顿时间也会越来越长。所以我们最终用了OHC做堆外缓存。OHC把数据放到JVM堆外不参与GC扫描。缓存再大也不会因为缓存对象太多而拖慢GC。对于这种G级别的商品缓存这一点收益非常明显。当然堆外缓存也不是没有代价。OHC存储的是字节数组Java对象放进去之前需要先序列化取出来还要反序列化。我们使用的是Kryo序列化速度快占用空间也比较小。由于Kryo不是线程安全的我们通过一个ThreadLocal为每个线程维护独立实例整个项目一直沿用了这种方式。商品缓存也没有做成一个大Cache而是按数据维度拆开。BASE、PRICE、SKU、BRAND、CATEGORY、IMAGE、ATTRIBUTE、DESCRIPTION每个维度都有独立的Loader负责数据加载、序列化以及缓存未命中的处理。更重要的是每个维度都可以单独配置容量和TTL。访问频率高的基础数据容量配大一些TTL也长一点访问频率低、占内存比较大的描述信息则控制容量和过期时间。这样才能把有限的内存尽可能留给热点数据提高整体命中率。另外还有一个细节我觉得OHC设计得不错。它的getWithLoader()天然带有singleflight能力。同一个Key在同一台机器上只会有一个线程真正去加载数据其他线程直接等待结果。也就是说即使热点商品缓存刚好过期也不会出现同一台机器几十个线程一起打下游的情况单机内部的缓存击穿问题基本就解决了。不过我们并没有把OHC作为长期方案。它在整个系统里的定位就是大促期间额外增加的一层本地缓存。大促开始前提前预热正式开始后大部分热点商品已经在各台机器的OHC里。等活动结束再直接旁路OHC所有请求恢复走Redis。这样做最大的好处就是不用长期维护两级缓存的一致性平时系统还是保持最简单的缓存架构只在真正需要的时候才把这层能力打开。线上压测和生产数据都比较符合预期。OHC命中率基本稳定在92%95%绝大部分请求都在本机直接返回只有少量请求继续访问Redis最终真正落到数据库的请求已经不到1%。