一次真实业务中,我是如何做缓存方案取舍的
一、背景
在一个电商系统中,我们有一块活动 + 商品明细信息的查询场景:
-
活动明细规模: ~1 万
-
商品明细规模: ~50 万
-
查询路径在 下单 / 校验 / 计算价格 等核心链路上
-
对 RT 和稳定性要求较高
系统启动后会有一次全量初始化(冷启动) ,
后续在 活动或商品信息发生变更时,通过 MQ 进行同步更新(热更新) 。
这类场景看起来很常见,但在缓存结构的选择上,其实很容易踩坑。
二、真正的问题是什么?
一开始我们面对的并不是「用不用缓存」,而是这几个更具体的问题:
-
读非常多,写相对少,但写不是完全没有
-
数据量不算小,内存占用需要可控
-
热更新期间,不能影响读请求
-
代码需要 长期可维护,不是 Demo
所以问题就变成了:
在这种「高并发读 + 低频写 + 有热更新」的业务场景下,
缓存到底该选什么数据结构?
三、候选方案对比
一开始我们主要在下面两个方向上纠结:
方案一:ConcurrentHashMap<Long, Object>
这是最“安全”的选择。
优点:
-
JDK 原生
-
线程安全
-
读写模型成熟
-
所有人都熟,维护成本低
缺点:
- Long 装箱,对象数量多
- 内存占用偏高
- 在大数据量下(几十万级),GC 压力不可忽视
方案二:FastUtil 的 Long2ObjectMap
这是一个偏性能 & 内存优化的方案。
优点:
-
使用 long 原始类型作为 key,无装箱
-
内存占用更低
-
遍历、查询性能更好
缺点:
- 非线程安全
- 引入三方依赖
- 需要自行保证并发安全
四、为什么一开始会倾向 Long2ObjectMap?
从纯技术角度看:
-
商品明细 50 万级
-
key 是纯 long
-
读多写少
Long2ObjectMap 在内存 & 性能上是更“优雅”的方案。
如果这是一个:
-
只在启动时初始化
-
运行期完全不变的缓存
那我会毫不犹豫选它。
五、真正改变决策的点:热更新 + 并发读
但问题在于:
我们的数据不是只初始化一次。
-
活动 / 商品变更
-
MQ 推送更新
-
运行期存在写操作
这时就必须正视一个现实问题:
Long2ObjectMap 本身不是线程安全的
为了保证读写安全,常见的补救方案无非是:
-
synchronized
-
ReadWriteLock
-
Copy-on-write
但一旦走到这一步,问题就来了:
-
复杂度明显上升
-
锁粒度不好控制
-
稍不注意就会影响读性能
而我们的核心诉求恰恰是:读要绝对稳。
六、最终选择:ConcurrentHashMap(但有边界)
最终我们选择了 ConcurrentHashMap,但不是“无脑用”。
关键原因只有一句话:
在存在运行期写操作的前提下,稳定性和可维护性,比极致性能更重要
我们明确了几条边界:
-
初始化阶段:全量 put
-
运行期:通过 MQ 做增量更新
-
不做复杂的锁封装
-
不做过度抽象
换句话说:
我们接受一点内存损耗,换取系统行为的可预期性
七、事后复盘
回头看这个决策,我觉得有 2 个点非常关键:
-
数据结构的选择,必须结合“数据生命周期”
- 只读?可变?
- 初始化一次?还是持续更新?
-
不要为了“看起来高级”而牺牲可维护性
-
三方库不是不能用
-
但要想清楚:谁来维护?出问题谁兜底?
-
如果未来这个缓存真的变成:
-
运行期完全不更新
-
或更新频率极低
那么 退回 Long2ObjectMap 是完全可能的。
八、最后一点个人感受
技术选型里,
没有“最好”,只有“在当下约束下最合适” 。
很多时候,
决定你是不是“高级工程师”的,
不是你用了什么技术,
而是你为什么这么选。