欢迎来到窥视未来的博客

https://github.com/lwx57280 https://gitee.com/li_VillageHead

MySQL里有2000万数据,Redis只给20万空间,如何保证缓存里全是热点?

兄弟们,面试被问过这道题吗?“MySQL 2000万数据,Redis只有20万的空间,怎么保证Redis存的都是热点数据?”

我面架构岗的时候被问过,当时回答得磕磕绊绊,后来在生产环境真遇到这个问题——缓存命中率从95%跌到60%,内存里全是冷数据,热数据反而被挤出去了。今天就把这个问题的完整解决方案复盘出来,涵盖淘汰策略、冷热分离、热度统计、分层存储四个层面。

一、问题本质:有限空间下的价值选择

这个问题可以拆成三个子问题:

  1. 如何识别热点怎么判断哪些数据是“热”的?

  2. 如何淘汰冷门空间满了,怎么把冷数据踢出去?

  3. 如何防止污染怎样避免一次性查询把冷数据大量灌入?

mermaid-1785665836623

二、第一道屏障: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 工作流程

mermaid-1785665955736

 配置好淘汰策略后,当缓存达到20万容量上限时,Redis会自动将冷门key驱逐出去,腾出空间给新热点。

三、核心防线:查询时动态判断热度

3.1 查询流程

mermaid-1785666040537

 

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 完整架构图

image

 

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太多 缩短过期时间或采样统计

 

八、避坑总结

  1. 淘汰策略必须配maxmemory-policy allkeys-lru 是底线配置,不能省略

  2. 冷查询不进缓存通过热度阈值控制,避免冷数据写入污染缓存

  3. 定时任务主动清理LRU是被动淘汰,主动清理效率更高

  4. 批量查询要隔离批量任务直连MySQL,不走Redis,避免冷数据灌入

  5. 监控命中率是王道命中率直接反映热点策略是否有效

兄弟们,你们在生产环境中是怎么保证Redis存的全是热数据的?有没有遇到过冷数据污染导致命中率暴跌的情况?评论区聊聊。

posted on 2026-08-02 18:32  k8s-Mango  阅读(10)  评论(0)    收藏  举报

导航