欢迎光临
我们一直在努力

【Redis】缓存策略与三大经典问题

【Redis】系列第2期:VibeLoop 的流量护盾 — 缓存策略与三大经典问题

贯穿案例「VibeLoop」:为虚拟的互动平台,仅用于技术演示,并非真实存在的产品。 上期速递:【Redis】Redis 数据结构与 Spring Boot 集成 本文是 VibeLoop 系列的 第2期,全文共9 个章节。覆盖缓存更新策略、三大经典问题(穿透/击穿/雪崩)、淘汰策略选型、双写一致性、Spring Cache 踩坑、8 道面试题、必背速查表。


目录

  • 开篇场景:Feed 流崩溃之夜
  • 理论基础:为什么缓存像图书馆借阅台
  • 1. 缓存更新策略四种模式对比
  • 2. 缓存穿透三件套
  • 3. 缓存击穿:热点 key 的生死时速
  • 4. 缓存雪崩:多米诺骨牌效应
  • 5. 八种淘汰策略选型矩阵
  • 6. 双写一致性:最难解的并发题
  • 7. Spring Cache 注解踩坑实录
  • 8. 面试八连问 + 详解
  • 9. 必背速查表

开篇场景:Feed 流崩溃之夜

20xx年双十一当晚,VibeLoop 技术群炸了。

运维老张在群里甩了一张 Grafana 截图:MySQL CPU 飙到 98%,慢查询堆积如山,首页 Feed 流接口 P99 延迟从平日的 80ms 飙升到 12 秒。用户疯狂下拉刷新,每次下拉都是一次 SELECT * FROM posts ORDER BY update_time DESC LIMIT 20,QPS 10000 的路由全砸在数据库上。

值班的小王慌了,紧急加了 Redis 缓存。Feed 流接口查 Redis 命中直接返回,miss 才查 DB。10 分钟后,CPU 降到了 30%,世界清净了。

但是第二天一早,更大的问题来了:

  • 缓存穿透:有恶意脚本用随机 userId 疯狂请求不存在用户的主页,Redis 全部 miss,请求一路打到 DB,CPU 又飙了
  • 缓存击穿:运营置顶了一个"双十一战报"帖子,访问量暴增。缓存刚好过期的那一秒,几百个线程同时打到 DB 拉了同一份数据
  • 缓存雪崩:小王给所有帖子缓存设了统一的 1 小时 TTL。08:00 整,大量缓存同时失效,DB 瞬间被打穿
  • 这就是缓存引入后必须面对的三大经典问题。缓存降低了 DB 压力,但把一致性、可用性、穿透防护的复杂度全部转移到了中间层。会用 Redis 不稀奇,能把缓存用对、用稳、用不出事,才是面试官真正想考察的。


    理论基础:为什么缓存像图书馆借阅台

    在进入具体方案之前,先建立一个核心类比:

    缓存 = 图书馆借阅台。热门书放在前台随手取(缓存命中),冷门书要去书架深处找(查数据库)。借阅台空间有限,所以要不断淘汰冷门书(淘汰策略)。如果有人捏造一本不存在的书名来查询,借阅台查不到就去书架翻,翻完也没有——这就是缓存穿透。如果某本书突然爆火但刚好被人还回来还没上架,所有人冲到书架去抢——这就是缓存击穿。如果图书馆断电,借阅台所有书被清空,全馆读者同时涌向书架——这就是缓存雪崩。

    带着这个类比,我们逐个击破。 在这里插入图片描述

    图1:穿透(蓝色)/ 击穿(橙色)/ 雪崩(红色)在请求链路中的位置与应对方案全景


    1. 缓存更新策略四种模式对比

    缓存的本质是"用空间换时间",但谁来负责更新缓存?有四种经典模式。

    1.1 Cache Aside(旁路缓存)— VibeLoop 实际采用

    读:先查缓存 → 命中返回 / miss 查 DB → 写缓存 → 返回
    写:先更新 DB → 再删除缓存(而非更新缓存)

    为什么写时删缓存而不是更新缓存? 因为更新缓存可能产生"写-写"并发导致脏数据:线程 A 更新 DB → 线程 B 更新 DB → 线程 B 更新缓存 → 线程 A 更新缓存(B 的更新被 A 覆盖)。而删除缓存则没有这个问题——缓存被删后,下次读请求会自然从 DB 加载最新值。

    VibeLoop 帖子编辑的简化代码:

    @Transactional
    public void updatePost(Long postId, PostDTO dto) {
    postMapper.updateById(postId, dto); // 1. 更新 DB
    stringRedisTemplate.delete("post:" + postId); // 2. 删除缓存
    }

    适用场景:读多写少,对一致性要求不极端严格。VibeLoop 首页 Feed、帖子详情都用这个模式。

    1.2 Read/Write Through(读写穿透)

    缓存层直接代理 DB,应用只和缓存打交道:

    读:应用 → 缓存 → (miss时缓存自己查DB并回填) → 返回
    写:应用 → 缓存 → (缓存同步更新DB) → 返回

    Redisson 的 RLocalCachedMap 支持类似语义,但 Spring Cache 默认不实现 Write Through。

    适用场景:追求代码简洁、缓存与 DB 强绑定的场景。

    1.3 Write Behind(异步回写)

    写操作只写缓存,异步批量刷入 DB。例如每秒合并 100 次计数器更新为一条 SQL:

    应用 → 只写缓存 → 返回
    ↓ (异步批量)
    DB

    风险极高:缓存宕机 = 数据丢失。VibeLoop 仅用于非关键数据的计数(如帖子浏览数),核心业务不用。

    1.4 四种模式对比

    模式写延迟一致性复杂度VibeLoop 场景
    Cache Aside 低 最终一致 低 帖子详情、用户资料
    Read Through 低 强一致 中 配置数据
    Write Through 低 强一致 中 很少使用
    Write Behind 极低 弱一致 高 浏览计数(非关键)

    2. 缓存穿透三件套

    问题本质:查询一个数据库中根本不存在的数据,缓存永远 miss,每次请求都穿透到 DB。

    VibeLoop 实战场景:爬虫脚本遍历 userId=1 到 1000000,大量随机 ID 根本不存在。Redis 里没有,MySQL 里也没有,但每次都要扫一遍索引。

    2.1 第一件套:布隆过滤器(Bloom Filter)

    原理:用极小的内存判断"一个 key 是否一定不存在"。

    • 添加元素时,用 k 个哈希函数映射到长度为 m 的 bit 数组,将对应位全部置为 1
    • 查询时,同样计算 k 个哈希,如果任意一位为 0 → 100% 确定不存在;如果全为 1 → 可能存在(有误判率)

    在这里插入图片描述

    图2:添加元素(左)vs 查询元素(右)的完整流程,含误判场景演示与 Redisson 参数解读

    Redisson 实战:

    @Configuration
    public class BloomConfig {
    @Bean
    public RBloomFilter<String> userBloomFilter(RedissonClient redisson) {
    RBloomFilter<String> bloom = redisson.getBloomFilter("user_bloom");
    // 预计100万用户,误判率1%
    bloom.tryInit(1_000_000L, 0.01);
    return bloom;
    }
    }

    // 注册时添加到布隆过滤器
    @Transactional
    public void register(User user) {
    userMapper.insert(user);
    userBloomFilter.add("user:" + user.getId());
    }

    // 查询前先过布隆
    public User getUser(Long userId) {
    String key = "user:" + userId;
    if (!userBloomFilter.contains(key)) {
    return null; // 一定不存在,直接返回,不查 DB
    }
    // 可能存在,正常走缓存→DB 流程
    User cached = (User) redisTemplate.opsForValue().get(key);
    if (cached != null) return cached;
    User dbUser = userMapper.selectById(userId);
    if (dbUser != null) {
    redisTemplate.opsForValue().set(key, dbUser, 30, TimeUnit.MINUTES);
    }
    return dbUser;
    }

    核心参数推导:

    误判率 p ≈ (1 – e^(-k·n/m))^k

    n = 预期元素数量(100万)
    m = bit 数组长度
    k = 哈希函数数量

    当 p = 0.01 时,Redisson 自动算出:
    m ≈ 9,585,058 bits ≈ 1.14 MB
    k ≈ 7 个哈希函数

    注意:布隆过滤器不能删除元素(删除需要计数型布隆,Redisson 不内置支持)。数据迁移或用户注销时,需要重建布隆过滤器。

    2.2 第二件套:缓存空值

    即使布隆过滤器判断"可能存在",查到 DB 后也可能真的不存在。此时缓存一个空值,防止短时间内的重复穿透:

    if (dbUser == null) {
    // 缓存空值,TTL 设短(1~5 分钟)
    redisTemplate.opsForValue().set(key, null, 2, TimeUnit.MINUTES);
    return null;
    }

    为什么 TTL 要短? 因为"不存在"是暂时的——用户可能下一秒就注册了。TTL 过长会导致新注册用户长时间查不到。

    2.3 第三件套:参数前置校验

    在 Controller 层就拦截明显非法的请求:

    @GetMapping("/user/{userId}")
    public Result<User> getUser(@PathVariable Long userId) {
    if (userId == null || userId <= 0) {
    return Result.fail("非法用户ID");
    }
    // 继续后续逻辑
    }

    简单、零成本,但很多团队忽略。

    2.4 三件套组合拳

    请求 → 参数校验(非法拦截) → 布隆过滤器(一定不存在拦截)
    → 缓存查询(命中返回) → DB查询(无数据缓存空值)

    三件套各司其职,穿透率可以从 100% 降到 1% 以下。


    3. 缓存击穿:热点 key 的生死时速

    问题本质:一个热点 key(访问量极高)刚好过期,瞬间大量并发请求全部打到 DB。

    穿透 vs 击穿的核心区别:穿透查的是不存在的数据,击穿查的是存在但缓存刚好过期的热点数据。

    3.1 互斥锁方案(VibeLoop 首选)

    关键思想:缓存 miss 时,不让所有线程都去查 DB,只有抢到分布式锁的线程才能查 DB 并回写缓存,没抢到的自旋等待。

    public Post getPost(Long postId) {
    String cacheKey = "post:" + postId;
    String lockKey = "lock:post:" + postId;

    Post post = (Post) redisTemplate.opsForValue().get(cacheKey);
    if (post != null) return post;

    // 抢锁,超时 10 秒
    boolean locked = redisTemplate.opsForValue()
    .setIfAbsent(lockKey, "1", 10, TimeUnit.SECONDS);
    try {
    if (locked) {
    // 抢到锁,查 DB 并回写
    post = postMapper.selectById(postId);
    if (post != null) {
    redisTemplate.opsForValue()
    .set(cacheKey, post, 30 + ThreadLocalRandom.current().nextInt(10),
    TimeUnit.MINUTES);
    }
    return post;
    } else {
    // 没抢到锁,自旋重试读缓存
    TimeUnit.MILLISECONDS.sleep(50);
    return getPost(postId); // 递归重试
    }
    } finally {
    if (locked) {
    redisTemplate.delete(lockKey);
    }
    }
    }

    关键细节:

  • setIfAbsent 即 SET NX EX,加锁与设过期时间原子完成,防止死锁
  • 缓存 TTL 加了随机值 ThreadLocalRandom.current().nextInt(10),避免大量 key 同时过期(顺便防雪崩)
  • 锁的 value 应该用 UUID 标识持有者,避免 A 的锁被 B 误删。上例简化了,生产环境必须加上
  • 3.2 逻辑过期方案

    与互斥锁思路相反:缓存永不过期,但在 value 中携带一个逻辑过期时间戳。读取时检查时间戳,如果逻辑已过期,返回旧值(保证可用),同时异步开一个新线程去刷新缓存。

    @Data
    public class PostWithExpire {
    private Post data;
    private Long expireAt; // 逻辑过期时间戳(ms)
    }

    public Post getPostV2(Long postId) {
    String key = "post:" + postId;
    PostWithExpire wrapper = (PostWithExpire) redisTemplate.opsForValue().get(key);
    if (wrapper == null) {
    // 缓存没有(第一次),必须同步加载
    return loadAndCache(postId);
    }

    if (wrapper.getExpireAt() > System.currentTimeMillis()) {
    return wrapper.getData(); // 未过期,直接返回
    }

    // 逻辑已过期,先返回旧值,异步刷新
    executor.execute(() -> {
    String lockKey = "lock:refresh:post:" + postId;
    if (redisTemplate.opsForValue().setIfAbsent(lockKey, "1", 5, TimeUnit.SECONDS)) {
    try {
    loadAndCache(postId);
    } finally {
    redisTemplate.delete(lockKey);
    }
    }
    });

    return wrapper.getData(); // 返回旧值,不阻塞用户
    }

    3.3 两种方案对比

    维度互斥锁逻辑过期
    数据一致性 强,返回最新数据 弱,可能返回旧数据
    性能 有等待,部分请求阻塞 无等待,全部非阻塞返回
    实现复杂度 低 中(需维护过期时间戳)
    适用场景 对一致性要求高的数据 对可用性要求高的数据

    VibeLoop 选择:帖子内容用互斥锁(用户不能看到旧版本),帖子点赞数用逻辑过期(数字差几个无所谓)。


    4. 缓存雪崩:多米诺骨牌效应

    问题本质:大量缓存 key 同时过期,或 Redis 集群宕机,导致所有请求直接打到 DB。

    4.1 随机 TTL 打散过期时间

    最简单有效的方案:给过期时间加随机抖动量。

    // 基础 TTL 30 分钟,加上 0~10 分钟的随机值
    int ttl = 30 * 60 + ThreadLocalRandom.current().nextInt(10 * 60);
    redisTemplate.opsForValue().set(key, value, ttl, TimeUnit.SECONDS);

    这行代码的价值远超你的想象:把一条陡峭的过期线打散成一条平滑曲线,DB 压力均匀分布。

    4.2 多级缓存降级

    请求 → 本地缓存 (Caffeine) → Redis → DB

    当 Redis 不可用时,本地缓存还能撑一阵:

    @Configuration
    public class MultiLevelCacheConfig {
    @Bean
    public Cache<String, Post> localCache() {
    return Caffeine.newBuilder()
    .maximumSize(10000)
    .expireAfterWrite(5, TimeUnit.MINUTES)
    .build();
    }
    }

    public Post getPostMultiLevel(Long postId) {
    String key = "post:" + postId;

    // L1: 本地缓存
    Post local = localCache.getIfPresent(key);
    if (local != null) return local;

    try {
    // L2: Redis
    Post redisPost = (Post) redisTemplate.opsForValue().get(key);
    if (redisPost != null) {
    localCache.put(key, redisPost);
    return redisPost;
    }
    } catch (Exception e) {
    log.warn("Redis 不可用,降级到 DB", e);
    }

    // L3: DB
    Post dbPost = postMapper.selectById(postId);
    if (dbPost != null) {
    localCache.put(key, dbPost);
    }
    return dbPost;
    }

    4.3 限流熔断

    在网关层或服务层加限流,给 DB 留出喘息空间:

    // Sentinel / Resilience4j 限流
    @SentinelResource(value = "getPost", fallback = "getPostFallback")
    public Post getPost(Long postId) {
    // …正常逻辑
    }

    public Post getPostFallback(Long postId, Throwable e) {
    // 返回降级数据:默认帖子或提示"服务繁忙"
    return Post.builder().title("系统繁忙,请稍后重试").build();
    }

    4.4 Redis 高可用

    这是根本性方案,延后到第4期详细展开:主从复制 + 哨兵 + Cluster。


    5. 八种淘汰策略选型矩阵

    类比:冰箱食材管理 — FIFO 是"先买先扔",LRU 是"最久没吃的清理掉",LFU 是"吃最少的断舍离"。

    Redis 4.0 起提供 8 种淘汰策略:

    策略行为适用场景
    noeviction 不淘汰,写满报错 绝对不能丢数据的场景(很少用)
    volatile-lru 在设了 TTL 的 key 中淘汰最近最少使用 VibeLoop 帖子缓存(默认推荐)
    allkeys-lru 在所有 key 中淘汰最近最少使用 纯缓存场景,数据可从 DB 恢复
    volatile-lfu 在设了 TTL 的 key 中淘汰最不频繁使用 热点数据保护
    allkeys-lfu 在所有 key 中淘汰最不频繁使用 有明显冷热数据差异
    volatile-random 在设了 TTL 的 key 中随机淘汰 所有 key 价值均等
    allkeys-random 在所有 key 中随机淘汰 同上
    volatile-ttl 淘汰最近要过期的 key 自然淘汰,减少业务影响

    VibeLoop 选型决策

    帖子缓存 → volatile-lru(有过期时间,按访问热度淘汰)
    用户 Session → volatile-ttl(自然过期最合理)
    计数器(点赞/浏览)→ noeviction(不能丢,单独实例)

    LRU vs LFU 源码层面区别

    Redis 的 LRU 不是精确 LRU(需要维护链表,开销大),而是近似 LRU:随机采样 N 个 key,淘汰其中 idle 时间最长的那个。maxmemory-samples 控制采样数,默认 5。

    LFU 在 24 位中拆分为两部分:高 16 位存 last-decrement-time,低 8 位存 logarithmic-counter。计数器不是简单 +1,而是对数增长:

    counter = 1 / (1 – p) // p = 1/(counter * lfu_log_factor + 1)

    这使得 100 次访问和 10000 次访问的计数器差距远小于 100 倍,防止少数热点 key 永远不被淘汰。


    6. 双写一致性:最难解的并发题

    6.1 先删缓存还是先更新 DB?

    这是面试中的经典陷阱题。推演一下:

    方案 A:先删缓存,再更新 DB

    T1: A 删缓存
    T2: B 读缓存 miss → 查 DB 得到旧值 → 写缓存(旧值)
    T3: A 更新 DB(新值)
    结果:DB 是新值,缓存是旧值 → 不一致

    方案 B:先更新 DB,再删缓存

    T1: 缓存刚好过期
    T2: A 读缓存 miss → 查 DB 得到旧值
    T3: B 更新 DB 为新值 → 删缓存
    T4: A 写缓存(旧值)
    结果:DB 是新值,缓存是旧值 → 不一致

    方案 B 的不一致窗口比方案 A 窄得多(需要在"缓存刚好过期"的极端巧合下才触发),所以先更新 DB 再删缓存是更好的基础方案。

    6.2 延迟双删

    在方案 A 的基础上加一次延迟删除来兜底:

    1. 删除缓存
    2. 更新 DB
    3. 休眠 N 毫秒(比如 500ms)
    4. 再次删除缓存

    public void updatePostWithDoubleDelete(Long postId, PostDTO dto) {
    stringRedisTemplate.delete("post:" + postId); // 1. 删缓存
    postMapper.updateById(postId, dto); // 2. 更新 DB
    try {
    TimeUnit.MILLISECONDS.sleep(500); // 3. 等待
    } catch (InterruptedException e) {
    Thread.currentThread().interrupt();
    }
    stringRedisTemplate.delete("post:" + postId); // 4. 再删缓存
    }

    延迟多久? 大于"读请求查 DB + 写缓存"的时间。这个值需要压测确定,通常 200ms~1s。

    6.3 终极方案:Canal + Binlog

    延迟双删依赖 sleep,不优雅且有风险。大厂方案:订阅 MySQL Binlog,异步删除/更新缓存。

    MySQL 写入 → Binlog → Canal 解析 → 推送变更事件 → 服务消费 → 删除缓存

    @CanalEventListener
    public class PostCacheListener {
    @ListenPoint(destination = "vibeloop", schema = "vibeloop", table = "posts")
    public void onPostUpdate(CanalEntry.Entry entry) {
    // 从 binlog 解析出更新的 postId
    Long postId = parsePostId(entry);
    stringRedisTemplate.delete("post:" + postId);
    }
    }

    优势:即使业务代码忘记删缓存,Canal 也能兜底;适合最终一致性场景。

    在这里插入图片描述

    图3:延迟双删 + Canal Binlog 完整时序,底部含四种方案演进对比

    6.4 一致性方案金字塔

    最强一致性 ── 分布式事务(Seata AT/TCC)
    ↑
    Canal + Binlog + MQ 重试 ← VibeLoop 推荐
    ↑
    延迟双删(99% 场景可用)
    ↑
    先更新 DB 再删缓存(基础方案)


    7. Spring Cache 注解踩坑实录

    7.1 @Cacheable 的 key 生成陷阱

    // ❌ 错误:默认 key 是所有参数拼接,遇到复杂对象可能抛异常
    @Cacheable(value = "post")
    public Post getPost(Long postId) { ... }

    // ✅ 正确:显式指定 SpEL key
    @Cacheable(value = "post", key = "#postId")
    public Post getPost(Long postId) { ... }

    7.2 @CachePut vs @Cacheable 的区别

    @CachePut(value = "post", key = "#result.id") // 每次都执行方法,强制更新缓存
    public Post savePost(Post post) { ... }

    @Cacheable(value = "post", key = "#postId") // 有缓存就跳过方法,不执行
    public Post getPost(Long postId) { ... }

    很多人以为 @CachePut 和 @Cacheable 一样,结果缓存一直不生效。记牢:@CachePut = 执行方法 + 更新缓存;@Cacheable = 检查缓存 + 决定是否执行方法。

    7.3 @CacheEvict 的 beforeInvocation

    // 默认:方法执行后删缓存。方法抛异常时不删
    @CacheEvict(value = "post", key = "#postId")
    public void deletePost(Long postId) { ... }

    // beforeInvocation=true:方法执行前删缓存。即使方法抛异常也删
    @CacheEvict(value = "post", key = "#postId", beforeInvocation = true)
    public void riskyDeletePost(Long postId) { ... }

    默认行为更安全(不会在操作失败时误删),但如果你需要最快释放缓存空间,可以开 beforeInvocation。

    7.4 条件缓存

    // 结果不为 null 时才缓存
    @Cacheable(value = "post", key = "#postId", unless = "#result == null")

    // 参数满足条件时才缓存
    @Cacheable(value = "post", key = "#postId", condition = "#postId > 1000")

    7.5 多级嵌套的缓存穿透问题

    @Cacheable(value = "post", key = "#postId", cacheManager = "redisCacheManager")
    @Cacheable(value = "post", key = "#postId", cacheManager = "caffeineCacheManager")
    public Post getPost(Long postId) { ... }

    Spring Cache 默认按注解顺序执行:第一个 @Cacheable 命中就返回,不会走到第二个!多级缓存必须自己实现,不能用嵌套注解。

    7.6 序列化坑

    默认 JDK 序列化要求类实现 Serializable,忘记实现会导致运行时 NotSerializableException。生产环境务必换成 Jackson/Protobuf:

    @Bean
    public RedisCacheManager cacheManager(RedisConnectionFactory factory) {
    RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig()
    .serializeValuesWith(
    RedisSerializationContext.SerializationPair
    .fromSerializer(new GenericJackson2JsonRedisSerializer())
    );
    return RedisCacheManager.builder(factory).cacheDefaults(config).build();
    }


    8. 面试八连问 + 详解

    Q1:缓存穿透和缓存击穿有什么区别?

    穿透查的是不存在的数据,击穿查的是存在但刚好过期的热点数据。穿透用布隆过滤器 + 空值缓存,击穿用互斥锁 + 逻辑过期。

    Q2:布隆过滤器的误判是怎么回事?能彻底消除吗?

    布隆过滤器只会有假阳性(说不存在的一定不存在,说可能存在的可能不存在),不会有假阴性。误判率取决于 m/n 比值和 k。不能彻底消除,但可以降到任意低(代价是更多内存)。

    Q3:延迟双删的延迟时间怎么定?

    大于"读请求查 DB + 写缓存"的时间。通过压测确定,通常在 200ms~1s。如果追求强一致,应该上 Canal + Binlog。

    Q4:Redis 的 LRU 是真的 LRU 吗?

    不是。Redis 用的是近似 LRU,随机采样 N 个 key(默认 5 个),淘汰其中 idle 时间最长的。精确 LRU 需要维护双向链表,内存开销大。近似 LRU 在采样数足够大时效果接近精确 LRU。

    Q5:先更新 DB 再删缓存,万一删缓存失败了怎么办?

    引入重试机制。可以把"删除缓存"这个操作发到消息队列(RocketMQ/Kafka),消费失败时自动重试。或者用 Canal 监听 Binlog 做异步补偿删除。

    Q6:为什么 Redis 单线程处理命令,还能扛住高并发?

    Redis 6.0 之前命令执行是单线程的(网络 IO 在 6.0 后引入多线程),但:

  • 纯内存操作,10 万 QPS 的单条命令只需微秒级
  • epoll IO 多路复用,一个线程管理数万个连接
  • 无锁竞争,没有线程切换开销
  • 真正的瓶颈通常是网络带宽,不是 CPU。

    Q7:Redis 挂了怎么办?数据全丢?

    分两层:高可用和持久化。高可用靠主从复制 + 哨兵自动故障转移(第 4 期详细讲)。持久化靠 RDB 快照 + AOF 日志,即使宕机也能恢复到最近的状态。

    Q8:缓存的 TTL 设多长合适?

    没有标准答案。考虑因素:

    • 数据变更频率:频繁变更的数据 TTL 要短
    • 业务容忍度:能接受多长时间的旧数据
    • 内存容量:TTL 越长,内存占用越大

    VibeLoop 实践:帖子详情 30min,用户资料 1h,配置数据 24h,热点数据不设 TTL(用逻辑过期)。


    9. 必背速查表

    三大问题速查

    问题场景根因解法关键词
    穿透 查不存在的数据 缓存永远 miss 布隆 + 空值缓存 + 参数校验 Bloom Filter
    击穿 热点 key 过期 并发回源 DB 互斥锁 / 逻辑过期 SET NX EX
    雪崩 大量 key 同时过期 缓存大面积失效 随机 TTL + 多级缓存 + 限流 抖动过期

    淘汰策略速查

    策略淘汰范围淘汰依据VibeLoop 用途
    volatile-lru 有过期时间的 key 最近最少使用 帖子缓存(推荐)
    allkeys-lru 所有 key 最近最少使用 纯缓存场景
    volatile-lfu 有过期时间的 key 最不频繁使用 热点保护
    volatile-ttl 有过期时间的 key 最近要过期 Session
    noeviction 无 不淘汰 计数器专用实例

    一致性方案速查

    方案一致性复杂度可靠性推荐度
    先更新 DB 再删缓存 弱 低 低(删失败则不一致) ★★
    延迟双删 中 低 中 ★★★
    Canal + Binlog 最终一致 高 高 ★★★★
    MQ 重试删除 最终一致 中 高 ★★★★★

    配置速查

    # Redis 淘汰策略
    maxmemory-policy volatile-lru
    maxmemory-samples 10

    # Spring Cache key 规范
    @Cacheable(value = "post", key = "#postId", unless = "#result == null")

    # 随机 TTL 防雪崩
    int ttl = 30 * 60 + ThreadLocalRandom.current().nextInt(10 * 60);


    下期预告 — 第3期「分布式锁从青铜到王者」:从 setnx 的三个致命漏洞出发,逐步演进到 Lua 原子释放 → WatchDog 自动续期 → Redlock 多数派原理,含 Redisson 源码走读。VibeLoop 帖子编辑锁、定时任务 Leader 选举、用户注册唯一性校验三大实战场景。

    赞(0)
    未经允许不得转载:171主机测评 » 【Redis】缓存策略与三大经典问题
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址