“2000万数据,只让Redis存20万,怎么保证存的都是最热的?”
一位粉丝在美团三面时被问懵。
一、问题本质:缓存系统的“生存游戏”
2000万数据中,只有20万是高频访问的“顶流”,剩下1980万都是“冷数据”。这像一场生存游戏——如何让Redis精准淘汰“冷数据”,长期保留“热数据”?
以下数据暴露核心矛盾:
- Redis内存成本太高
- 80%请求集中在20%数据(二八法则)
- 热点数据动态变化(如突发新闻、秒杀商品)
二、三级缓存治理体系
1. 第一层:智能淘汰策略(守门员)
Redis配置黄金法则:
# redis.conf关键配置
maxmemory 20gb # 按20万数据*1KB计算
maxmemory-policy allkeys-lfu # 使用LFU算法(Least Frequently Used)
淘汰策略对比:
策略 | 特点 | 适用场景 |
allkeys-lfu | 淘汰访问频率最低 | 稳定热点(如商品详情) |
volatile-ttl | 淘汰剩余时间最短 | 限时活动(如秒杀) |
allkeys-random | 随机淘汰 | 无规律访问 |
2. 第二层:实时热点探测(雷达系统)
Flink实时统计代码:
DataStream windowData = data
.keyBy("itemId")
.window(SlidingProcessingTimeWindows.of(Time.minutes(5), Time.minutes(1)))
.aggregate(new CountAgg(), new WindowResultFunction());
windowData.keyBy("windowEnd")
.process(new TopNHotItems(200000)) // 取Top20万
.addSink(new ZkConfigUpdater()); // 上报配置中心
3. 第三层:多级缓存架构(防御矩阵)
本地+Redis二级缓存实现:
// Caffeine本地缓存(第一级)
LoadingCache localCache = Caffeine.newBuilder()
.maximumSize(1000)
.expireAfterWrite(30, TimeUnit.SECONDS)
.build(key -> {
// Redis查询(第二级)
Object val = redis.get(key);
if(val == null) {
val = mysql.get(key);
redis.setex(key, 3600, val); // 回填Redis
}
return val;
});
三、四大核心优化技巧
1. 热点标记与保护
// 热点标记
ConcurrentHashMap hotKeyCounter = new ConcurrentHashMap<>();
public Object getData(String key) {
hotKeyCounter.compute(key, (k,v) ->
v == null ? new AtomicLong(1) : v.incrementAndGet());
if(hotKeyCounter.get(key).get() > 1000) { // 判定为热点
redis.persist(key); // 取消过期时间
zkClient.registerHotKey(key); // 上报监控
}
return localCache.get(key);
}
2. 冷热数据分离存储
MySQL表优化:
ALTER TABLE products
ADD COLUMN hot_score INT DEFAULT 0 COMMENT '热度值',
ADD INDEX idx_hot_score (hot_score);
Redis存储优化:
# 使用Hash结构压缩存储
HMSET product:1234
data "{...json...}"
hot 1
expire 1735689600
3. 智能预热机制
# 定时预热脚本(每日凌晨执行)
def preheat_cache():
# 获取昨日Top20万热点
hot_items = mysql.query("""
SELECT item_id
FROM access_log
WHERE date = CURDATE() - INTERVAL 1 DAY
GROUP BY item_id
ORDER BY COUNT(*) DESC
LIMIT 200000
""")
# 批量写入Redis
pipeline = redis.pipeline()
for item in hot_items:
data = mysql.get(item.id)
pipeline.setex(item.id, 86400, data) # 缓存24小时
pipeline.execute()
4. 动态策略调整
四、压测数据对比
方案 | 缓存命中率 | 平均延迟 | MySQL负载 |
无缓存 | 0% | 95ms | 100% |
基础LRU | 65% | 18ms | 35% |
智能方案 | 98.5% | 2.1ms | 1.5% |
五、面试加分项
1.缓存雪崩防护:
// 随机过期时间避免集体失效
public void setCache(String key, Object value) {
int expire = 3600 + new Random().nextInt(600); // 3600~4200秒随机
redis.setex(key, expire, value);
}
2.热点Key分片:
def get_cache_key(item_id):
shard = item_id % 10 # 分10个片
return f"item_{shard}_{item_id}"
3.多级降级策略:
六、实战建议
1.每日运维:
- 凌晨低峰期执行缓存分析脚本
- 使用redis-cli --hotkeys主动探测热点
2.监控预警:
- 对缓存击穿率设置分级报警(>5%触发警告)
- Redis内存使用超过80%时自动扩容
3.业务隔离:
- 不同业务线使用独立缓存实例(如商品、订单分离)
思考:若某热点商品因流量过大导致Redis分片崩溃,如何在不影响用户体验的前提下实现快速自愈?