MySQL里有2000万数据,Redis只给20万空间,如何保证缓存里全是热点?
兄弟们,面试被问过这道题吗?“MySQL 2000万数据,Redis只有20万的空间,怎么保证Redis存的都是热点数据?”
我面架构岗的时候被问过,当时回答得磕磕绊绊,后来在生产环境真遇到这个问题——缓存命中率从95%跌到60%,内存里全是冷数据,热数据反而被挤出去了。今天就把这个问题的完整解决方案复盘出来,涵盖淘汰策略、冷热分离、热度统计、分层存储四个层面。
一、问题本质:有限空间下的价值选择
这个问题可以拆成三个子问题:
-
如何识别热点:怎么判断哪些数据是“热”的?
-
如何淘汰冷门:空间满了,怎么把冷数据踢出去?
-
如何防止污染:怎样避免一次性查询把冷数据大量灌入?

二、第一道屏障:Redis内存淘汰策略
2.1 配置方案
# redis.conf maxmemory 256mb # 对应存20万条数据的内存大小 maxmemory-policy allkeys-lru
2.2 策略选择
| 策略 | 说明 | 适用场景 |
|---|---|---|
allkeys-lru |
所有key中淘汰最近最少使用的 | 推荐:需要统一管理所有缓存数据 |
volatile-lru |
只淘汰带过期时间的key | 只淘汰部分数据,不适合全量管理 |
allkeys-lfu |
淘汰访问频率最低的(Redis4.0+) | 对访问频率统计要求更高时 |
为什么我选
allkeys-lru?allkeys-lru是Redis提供的标准LRU近似算法,通过抽样比较淘汰最久未访问的Key。如果热点数据有明显的频率分布差异,allkeys-lfu精度更高,但简单场景下LRU足够。
2.3 工作流程

配置好淘汰策略后,当缓存达到20万容量上限时,Redis会自动将冷门key驱逐出去,腾出空间给新热点。
三、核心防线:查询时动态判断热度
3.1 查询流程

3.2 关键代码实现
@Service
public class HotCacheService {
@Autowired
private RedisTemplate<String, Object> redisTemplate;
@Autowired
private JdbcTemplate jdbcTemplate;
// 热度阈值:访问次数 >= 3 才允许进入缓存
private static final int HOT_THRESHOLD = 3;
// 热度统计窗口:5分钟
private static final int FREQ_WINDOW_SECONDS = 300;
public Object getData(String key) {
// 1. 先查缓存
Object cached = redisTemplate.opsForValue().get(key);
if (cached != null) {
// 命中:增加访问计数
incrementFreq(key);
return cached;
}
// 2. 缓存未命中:查数据库
Object data = queryFromMySQL(key);
if (data == null) {
return null;
}
// 3. 判断是否为热点
Long freq = getFreq(key);
if (freq >= HOT_THRESHOLD) {
// 热点:写入缓存,TTL 1小时
redisTemplate.opsForValue().set(key, data, 1, TimeUnit.HOURS);
}
// 非热点:不写缓存,避免冷数据污染
return data;
}
// 热度计数器(使用 Redis Hash 存储)
private void incrementFreq(String key) {
String freqKey = "freq:" + key;
redisTemplate.opsForValue().increment(freqKey);
redisTemplate.expire(freqKey, FREQ_WINDOW_SECONDS, TimeUnit.SECONDS);
}
private Long getFreq(String key) {
String freqKey = "freq:" + key;
Object val = redisTemplate.opsForValue().get(freqKey);
return val == null ? 0 : Long.parseLong(val.toString());
}
}
3.3 为什么“冷查询不进缓存”?
如果没有这道检查,大量冷数据会被写入缓存,挤出热点数据。设置准入阈值后,只有被访问次数≥N的key才能进入缓存,避免冷数据污染。
四、主动清理:定时任务批量冷热迁移
4.1 定时任务设计
@Component
public class ColdDataCleanupTask {
@Scheduled(cron = "0 0 2 * * ?") // 每天凌晨2点执行
public void cleanupColdData() {
// 1. 扫描Redis中所有业务key(使用SCAN,避免阻塞)
Set<String> keys = scanKeys("business:*");
for (String key : keys) {
// 2. 获取该key的访问频次
Long freq = getFreq(key);
if (freq == null || freq < 5) {
// 3. 低频访问:删除缓存
redisTemplate.delete(key);
redisTemplate.delete("freq:" + key);
}
}
// 4. 检测到MySQL中新的热点,批量预热
preloadHotData();
}
private void preloadHotData() {
// 查询MySQL中最近访问频次高的数据
List<String> hotKeys = jdbcTemplate.queryForList(
"SELECT key FROM access_log WHERE create_time > NOW() - INTERVAL 1 HOUR " +
"GROUP BY key HAVING COUNT(*) > 50 ORDER BY COUNT(*) DESC LIMIT 1000",
String.class
);
// 批量预热
for (String key : hotKeys) {
if (!redisTemplate.hasKey(key)) {
Object data = queryFromMySQL(key);
if (data != null) {
redisTemplate.opsForValue().set(key, data, 1, TimeUnit.HOURS);
}
}
}
}
}
4.2 四层防护机制总结
| 层级 | 机制 | 作用 |
|---|---|---|
| 第一层 | Redis LRU淘汰策略 | 内存超过20万容量时自动淘汰冷Key |
| 第二层 | 查询时动态判断热度 | 冷查询不进缓存,防止污染 |
| 第三层 | 定时任务清理+预热 | 主动扫描Redis冷Key删除,批量预热MySQL热点 |
| 第四层 | 热度计数器 | 精准统计访问频率,辅助判断热点 |
五、如何防止批量查询灌入冷数据?
5.1 批量查询的问题
如果业务有大量批处理任务,一次查询百万条数据,会把大量冷数据写入Redis。解决方案:批量查询强制走MySQL,不入缓存,额外维护本地统计计数,定期批量合并。
// 批量查询场景
public List<Object> batchQuery(List<String> keys) {
// 方案1:批量请求直接走MySQL,不经过缓存
return queryBatchFromMySQL(keys);
}
// 或者:加入“批量标记”,写入缓存时设置较短的TTL或标记为低优先级
public void batchCacheWithLowPriority(String key, Object data) {
// 设置低优先级标记
redisTemplate.opsForValue().set(key, data, 10, TimeUnit.MINUTES);
// 后续在淘汰时优先清理带有低优先级标记的数据
}
5.2 保护措施
-
批量查询直连MySQL:不经过缓存,避免冷数据污染
-
短TTL兜底:即使被写入,也很快过期
-
业务隔离:批处理服务使用单独的Redis实例或DB,不影响在线业务
六、分层存储架构
6.1 完整架构图

6.2 分层策略
| 层级 | 存储介质 | 容量 | 存储内容 | 访问速度 |
|---|---|---|---|---|
| L1 | 本地缓存(Caffeine) | 1万条 | 最高频热点 | 极快(纳秒级) |
| L2 | Redis | 20万条 | 次高频热点 | 快(微秒级) |
| L3 | MySQL | 2000万条 | 全量数据 | 慢(毫秒级) |
通过分层存储,将数据按热度分级存放,保证最热的数据在最快的位置。
七、监控与动态调优
7.1 关键监控指标
# 监控缓存命中率 redis-cli INFO stats | grep keyspace_hits redis-cli INFO stats | grep keyspace_misses # 命中率 = hits / (hits + misses)
7.2 动态调优
| 指标 | 问题 | 调整策略 |
|---|---|---|
| 命中率 < 80% | 热点识别不准或容量不足 | 降低热点阈值(如从5降到3),或增加容量 |
| 淘汰量过大 | 数据写入量远超淘汰能力 | 增加容量或调高淘汰效率 |
| 热度计数器内存占用高 | 统计key太多 | 缩短过期时间或采样统计 |
八、避坑总结
-
淘汰策略必须配:
maxmemory-policy allkeys-lru是底线配置,不能省略 -
冷查询不进缓存:通过热度阈值控制,避免冷数据写入污染缓存
-
定时任务主动清理:LRU是被动淘汰,主动清理效率更高
-
批量查询要隔离:批量任务直连MySQL,不走Redis,避免冷数据灌入
-
监控命中率是王道:命中率直接反映热点策略是否有效
兄弟们,你们在生产环境中是怎么保证Redis存的全是热数据的?有没有遇到过冷数据污染导致命中率暴跌的情况?评论区聊聊。
浙公网安备 33010602011771号