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

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

贯穿案例「VibeLoop」 :为虚拟 的互动平台,仅用于技术演示,并非真实存在 的产品。

上期速递 :【Redis】Redis 数据结构与 Spring Boot 集成

本文是 VibeLoop 系列的 第2期,全文共9 个章节。覆盖缓存更新策略、三大经典问题(穿透/击穿/雪崩)、淘汰策略选型、双写一致性、Spring Cache 踩坑、8 道面试题、必背速查表。


目录

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

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

  1. 缓存穿透:有恶意脚本用随机 userId 疯狂请求不存在用户的主页,Redis 全部 miss,请求一路打到 DB,CPU 又飙了
  2. 缓存击穿:运营置顶了一个"双十一战报"帖子,访问量暴增。缓存刚好过期的那一秒,几百个线程同时打到 DB 拉了同一份数据
  3. 缓存雪崩:小王给所有帖子缓存设了统一的 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 帖子编辑的简化代码:

java 复制代码
@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 实战

java 复制代码
@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 后也可能真的不存在。此时缓存一个空值,防止短时间内的重复穿透:

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

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

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

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

java 复制代码
@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 并回写缓存,没抢到的自旋等待。

java 复制代码
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);
        }
    }
}

关键细节

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

3.2 逻辑过期方案

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

java 复制代码
@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 打散过期时间

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

java 复制代码
// 基础 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 不可用时,本地缓存还能撑一阵:

java 复制代码
@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 留出喘息空间:

java 复制代码
// 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. 再次删除缓存
java 复制代码
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 解析 → 推送变更事件 → 服务消费 → 删除缓存
java 复制代码
@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 生成陷阱

java 复制代码
// ❌ 错误:默认 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 的区别

java 复制代码
@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

java 复制代码
// 默认:方法执行后删缓存。方法抛异常时不删
@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 条件缓存

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

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

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

java 复制代码
@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:

java 复制代码
@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 后引入多线程),但:

  1. 纯内存操作,10 万 QPS 的单条命令只需微秒级
  2. epoll IO 多路复用,一个线程管理数万个连接
  3. 无锁竞争,没有线程切换开销

真正的瓶颈通常是网络带宽,不是 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 重试删除 最终一致 ★★★★★

配置速查

properties 复制代码
# 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 选举、用户注册唯一性校验三大实战场景。

相关推荐
zzz_23681 小时前
【Redis】Redis 数据结构与 Spring Boot 集成
数据结构·spring boot·redis
菠萝猫yena1 小时前
【数据库软件】beekeeper-studio安装方式(Mac)
数据库
Dovis(誓平步青云)1 小时前
《指标中转站:Pushgateway 如何把监控覆盖到这些原本看不见的角落》
数据库·生成对抗网络·oracle·内网穿透·飞牛nas
yurenpai(27届找实习中)1 小时前
Elasticsearch 核心总结 + 面试题实战(黑马点评项目)
redis·es
YJlio1 小时前
OpenClaw v2026.5.26-beta.1 / beta.2 预发布解读:Gateway 加速、transcript 路径统一、多通道修复、语音增强与安装更新链路加固
人工智能·windows·python·ui·缓存·gateway·outlook
IT龟苓膏10 小时前
Redis 数据类型底层原理:SDS、quicklist、intset、skiplist、Bitmap、HyperLogLog 一篇讲清
数据库·redis·skiplist
流星白龙11 小时前
【MySQL高阶】19.变更缓冲区,自适应哈希索引,日志缓冲区
数据库·windows·mysql
晴天¥11 小时前
Oracle中的监听配置与管理(动态、静态监听配置对比以及listener.ora和tnsnames.ora)
数据库·oracle
瀚高PG实验室12 小时前
python连接HGDB超时
数据库·瀚高数据库·highgo