Java - Redis 篇

1.1 缓存穿透

场景:查询一个根本不存在的数据,缓存和数据库中都没有。

问题:每次请求都会绕过缓存,直接打到数据库;被脚本刷、爬虫遍历或恶意构造 ID 时,QPS 会成倍放大。

方案原理适合
缓存空值把 null 也写入缓存,TTL 设短不存在的数据量有限、会被重复查
布隆过滤器位数组 + 多哈希,判断「可能存在 / 一定不存在」数据量大、只增不删、能接受误判
布谷鸟过滤器桶 + 指纹,支持删除需要删除、能控制写入/删除路径
Redis Set精确存在性判断数据量小、要求精确

缓存空值

  • TTL 要短(例如 30–120 秒),否则新建的数据会在一段时间内不可见;
  • 新建数据成功后,主动删除对应的空值缓存,不要只等 TTL;
  • 攻击者用大量随机 key 刷时,空值缓存会变成内存炸弹,要配合参数校验和限流。

布隆过滤器

位数组 + k 个哈希函数:插入时把 k 个位置置 1;查询时 k 个位置全为 1 才认为「可能存在」。

  • 不会漏判:真正插入过的元素一定查得到;
  • 会误判:没插入过的元素可能因为哈希碰撞被误判为存在;
  • 误判率随容量和哈希数变化:p ≈ (1 - e^(-kn/m))^k,最优 k = ceil(-ln(p)/ln2);
  • 参考:100 万元素、0.1% 误判率,大约需要 14.4 bit/元素,约 1.8 MB。
# 误判率 0.1%,容量 100 万
BF.RESERVE user:filter 0.001 1000000
BF.MADD user:filter user:1001 user:1002
BF.EXISTS user:filter user:1001   # 1=可能存在,0=一定不存在
BF.INFO user:filter

要点:

  • 不能删除:一个 bit 可能被多个元素共享,清 0 会影响别人;需要删除就用布谷鸟或 Set;
  • 容量要预留:扩容会生成子过滤器,查询变慢、误判率上升;
  • Redis 8 起模块已包含在发行包中,但默认 redis.conf 不会自动加载,要用 redis-full.conf 或显式 loadmodule;Redis 7.x 需要 Redis Stack / RedisBloom;
  • 集群里同一过滤器用 hash tag 固定 slot,例如 {user}:filter。

布谷鸟过滤器

每个元素存一个短指纹,并有两个候选桶;桶满了就「踢」一个已有指纹到它的另一个位置。查询只比指纹,所以也会误判,但支持删除。

CF.RESERVE coupon:filter 1000000 BUCKETSIZE 2 MAXITERATIONS 20 EXPANSION 1
CF.ADDNX coupon:filter coupon:123   # 不存在才加入;CF.ADD 允许重复
CF.EXISTS coupon:filter coupon:123
CF.COUNT coupon:filter coupon:123    # 可能存在几次,不是精确计数
CF.DEL coupon:filter coupon:123
CF.INFO coupon:filter

参数默认值:BUCKETSIZE 2、MAXITERATIONS 20、EXPANSION 1;capacity 会向上取整到 2 的幂。过滤器可能在没到 capacity 时就自报 full,然后创建子过滤器(最多 32 个)。

误判率:bucket size 1 时最小约 0.78%;bucket size 3 约 2.35%。桶越大填充率越高,误判率也越高。

⚠️ CF.DEL 官方明确警告:不确定加过就不要删。指纹会碰撞,删一个没加过的元素可能误删别人的指纹,造成 false negative。

适合:优惠券核销、活动报名、已读/已领取去重——加和删都由同一个业务写路径控制。

选型建议

  1. 数据量小、要求精确:直接用 Redis Set。
  2. 只增不删、数据量大、能接受误判:布隆过滤器。
  3. 需要删除、且能控制写入/删除路径:布谷鸟过滤器。
  4. 不想引入概率结构:缓存空值 + 参数校验 + 限流,先解决「随机 key」问题。
  5. 过滤器丢了:从 DB 分页扫描重建,重建期间降级放行。
对比布隆过滤器布谷鸟过滤器
删除不支持支持(有误删坑)
误判率下限可调得很低约 0.78% 起
内存更低略高
适合只增不删需要删除

面试追问

Q:为什么不用 Redis Set? A:数据量大时每个元素都要存完整字符串,内存比布隆/布谷鸟高一个量级;数据量小、要求精确时 Set 反而更简单。

Q:布隆过滤器放本地还是 Redis? A:本地快、少一次网络,但多实例之间不一致、重建困难;Redis 共享、易重建,但每次查询多一次网络往返。看数据规模和一致性要求。

Q:布隆过滤器为什么不能删除? A:bit 被多个元素共享,清 0 会影响其他元素;需要删除就用计数布隆、布谷鸟或 Set。

Q:布隆过滤器误判了怎么办? A:误判只会把「不存在」判成「可能存在」,继续查缓存/DB 即可,不能把「可能存在」当成业务事实。

Q:布谷鸟过滤器为什么能删除? A:它存的是指纹,删除时删对应指纹;但指纹碰撞会导致误删,所以官方要求「确定加过才删」。


1.2 缓存击穿

场景:热点 key 过期瞬间,大量并发请求同时涌入数据库。

方案原理特点
互斥锁只让一个线程查库回写缓存,其余阻塞等待强一致性,性能较差
逻辑过期不立即删除,先返回旧数据,异步更新缓存高可用,性能好

逻辑过期的实现要点:

  • value 里存 {data, expireAt},Redis 的物理 TTL 设长或永不过期;
  • 读到逻辑过期:先返回旧值,再异步抢锁刷新;
  • 刷新失败要告警并继续用旧值兜底,不能返回空;
  • 价格、库存这类不能返回旧值的业务不要用逻辑过期。

互斥锁的 TTL 怎么定? 覆盖「查库 + 回填」的耗时并留余量;等锁要有超时,超时后降级,不能无限阻塞。


1.3 缓存雪崩

场景:同一时段大量 key 同时过期,或 Redis 宕机,请求全部打到数据库。

解决方案:

  1. TTL 加随机值 — 避免大量 key 同时过期
  2. Redis 集群部署 — 提高可用性和分区容错性
  3. 限流降级策略 — 业务层添加多层防护
  4. 多级缓存 — 引入本地缓存(如 Caffeine)作为前置缓冲

雪崩其实分两种:

类型原因主要应对
大量 key 同时过期批量导入 TTL 相同、整点刷新TTL 加随机抖动、分批预热
Redis 整体不可用节点宕机、网络分区高可用、多级缓存、熔断降级

TTL 抖动一般是在基础 TTL 上加 10%–30% 的随机值;大促或发版后要提前预热,不要等用户请求来触发。


1.4 双写一致性

核心问题:修改数据库的同时,如何保证缓存与数据库的数据一致?

方案一:先删缓存,再操作数据库

正常流程:删除缓存 → 更新数据库 → 查询数据库 → 更新缓存

异常情况:删除缓存 → 查询数据库(旧值)→ 写入缓存(旧值)→ 更新数据库 → 缓存中仍是旧数据!

方案二:先操作数据库,再删除缓存

正常流程:更新数据库 → 删除缓存 → 查询缓存(未命中)→ 查库 → 写入缓存

异常情况:缓存未命中 → 查库(旧值)→ 数据库被更新 → 删除缓存 → 旧值写入缓存

为什么是删缓存,不是更新缓存?

  • 更新缓存要处理并发写覆盖、计算逻辑重复、缓存和 DB 双写不一致;
  • 删除更简单,下一次读自然回填;
  • 工程默认是 Cache-Aside:先更新数据库,事务提交后删缓存,再用 TTL 兜底。

解决方案总结

方案原理适用场景
延时双删删缓存 → 更新库 → 等一会 → 再删缓存兜底策略,轻量级
分布式锁(读写锁)写时加排他锁,读时加共享锁强一致性要求
异步通知(MQ / Canal)更新后异步通知缓存更新/删除允许短暂延迟
  • 共享锁:加锁后其他线程可共享读操作
  • 排他锁:加锁后阻塞其他线程的读写操作

总结:只有读时用共享锁,有写操作时用排他锁。保证了强一致性,但性能较低。

延迟双删的局限:它只是降低旧值回填的概率,不是强一致;延迟时间无法精确覆盖所有读请求,第二次删除失败仍会留下旧值。

更常用的现状做法:

做法原理代价
Cache-Aside + 事务提交后删缓存 + TTL最常用;删除失败由 TTL 兜底允许短暂旧值
binlog / CDC 异步失效Canal、Debezium 订阅数据库变更后删缓存有延迟,需维护 CDC 链路
事务性 outbox + MQ本地事务和消息一起提交,消费端删缓存实现复杂,要保证幂等

强一致要求(金额、库存)通常不要靠缓存保证:读走数据库,或者用分布式锁/读写锁;缓存只做加速。


1.5 持久化(RDB / AOF)

RDB(Redis Database Backup)

利用页表映射关系 + Copy-on-Write 技术:

  • 主进程读操作 → 直接访问共享内存
  • 主进程写操作 → 拷贝一份数据,在新副本上执行

AOF(Append Only File)

核心思路:把每一条写命令追加记录到日志文件中,重启时重新执行这些命令来恢复数据。

AOF 重写机制:用最少的命令达到相同的数据恢复效果,大幅压缩文件体积。

appendfsync 三档:

值行为可靠性性能
always每条命令都 fsync最高最差
everysec每秒 fsync 一次通常最多丢约 1 秒默认折中
no交给操作系统可能丢更多最好

Redis 7+ 用 Multi-Part AOF:不再是一个大文件,而是 manifest + base AOF + incr AOF;重写时先生成新的 base 再切换 manifest,避免旧版重写的阻塞窗口。

RDB vs AOF 对比

维度RDBAOF
文件大小小大
恢复速度快慢
数据安全性可能丢失最后一次快照后的数据更高
默认默认配置通常有 RDB save 规则默认关闭(以实际 conf 为准)

1.6 删除策略与淘汰策略

删除策略(Key 过期后如何删除)

惰性删除:访问 key 时检查,过期则删除。对 CPU 友好,但对内存不友好。

定期删除:每隔一段时间随机抽取 key 检查,过期则删除。有 Slow / Fast 两种模式。

Redis 实际使用 惰性删除 + 定期删除 的组合策略。

数据淘汰策略(内存满了怎么办)

策略范围说明
noeviction—默认。不淘汰,内存满后不允许写入
volatile-ttl有 TTL 的 keyTTL 越小越先淘汰
allkeys-random全体 key随机淘汰
volatile-random有 TTL 的 key随机淘汰
allkeys-lru全体 keyLRU 算法淘汰
volatile-lru有 TTL 的 keyLRU 算法淘汰
allkeys-lfu全体 keyLFU 算法淘汰
volatile-lfu有 TTL 的 keyLFU 算法淘汰
volatile-lrm有 TTL 的 keyRedis 8.6+,最久未修改
allkeys-lrm全体 keyRedis 8.6+,最久未修改

LRU vs LFU:

算法全称淘汰依据
LRULeast Recently Used当前时间 - 最后访问时间,差值越大越优先
LFULeast Frequently Used访问频率越小越优先

1.7 Redis 分布式锁

场景:服务集群部署下,普通的 JVM 锁无法跨节点互斥。

SETNX 原理

核心命令:SETNX(SET if Not eXists)。利用 Redis 单线程特性,只有一个客户端能成功设置 key。

⚠️ 不设置失效时间可能导致死锁(线程宕机后锁无法释放)。

Redisson 实现

public void redisLock() throws InterruptedException {
    // 1. 获取锁对象
    RLock lock = redissonClient.getLock("heimalock");

    // 2. 尝试获取锁
    // tryLock(waitTime, timeUnit)
    // - waitTime(10s): 最大等待时间,超时未获取则放弃并返回 false
    // - 省略 leaseTime 时,Redisson 默认启用「看门狗」机制:
    //   锁默认有效期 30s,看门狗会自动续期
    boolean isLock = lock.tryLock(10, TimeUnit.SECONDS);

    // 3. 执行业务
    if (isLock) {
        try {
            System.out.println("执行业务");
        } finally {
            // 4. 释放锁(必须放在 finally 中!)
            lock.unlock();
        }
    } else {
        System.out.println("获取锁失败,请稍后重试");
    }
}

Redisson 封装的关键能力:看门狗机制、Lua 脚本保证原子性、UUID 验证锁持有者、可重入、集群兼容。

看门狗只解决「业务没跑完锁先过期」,不解决主从切换丢锁:Redis 复制是异步的,主节点还没把锁写入同步给从节点就挂了,从节点升主后不知道这把锁存在。

如果锁失效的后果很严重(资金、库存),不要只依赖「锁还没过期」:加 fencing token,每次拿锁得到一个单调递增的编号,写数据库时带上编号,存储层拒绝旧编号的写入。Redlock 和五种锁方案的完整讨论见 《分布式锁选型》。


1.8 Redis 集群

主从复制

定位:数据备份 + 读写分离的基础方案。

  • 全量同步:首次连接,Master 生成 RDB 发送给 Slave
  • 增量同步:基于 repl_backlog,断线重连后只同步差异命令

哨兵机制

定位:主从复制的进阶补丁,解决高可用问题。哨兵持续监控节点状态。

脑裂问题:网络分区导致多个 Master 同时存在,数据冲突。

分片集群

定位:解决海量存储 + 高并发写问题。分布式分片 + 自动扩展。

核心机制 — 哈希槽(Hash Slot):16384 个槽位分配给不同主节点,根据 key 计算槽 → 路由到对应节点。去中心化的多主多从架构。

两个路由细节:

  • MOVED:槽已经永久迁到新节点,客户端应更新本地路由;
  • ASK:槽正在迁移,这次请求去目标节点查,但不更新路由。

跨 slot 的多 key 操作、Lua、事务会受限;需要把相关 key 放同一 slot 时用 hash tag,例如 {user:1001}:name 只对花括号里的内容计算槽。


1.9 单线程为什么快 / I/O 模型

Redis 为什么快

  • 纯内存操作,执行速度非常快(根本原因)
  • 命令执行单线程,避免不必要的上下文切换和竞争条件,单条命令天然原子
  • I/O 多路复用模型,非阻塞 IO

I/O 多路复用及网络模型

Redis 基于 Reactor 模式,使用 I/O 多路复用监听多个 Socket,主线程串行执行命令,避免了多线程的锁竞争问题。

准确说法是「命令执行单线程」,不是「整个 Redis 单线程」:Redis 6+ 的 I/O threads 可以并行读写 socket/解析协议,后台线程还会处理 AOF fsync、lazy free 等。

pipeline 为什么快? 把多条命令合并成一次网络往返,减少 RTT 和 syscall;pipeline 不是事务,也不保证原子性。


1.10 Redis 8.x 现状(速记)

版本关键变化
8.0(2025-05)Bloom/Cuckoo/JSON/Search/TimeSeries 并入发行包;AGPLv3 许可选项;Vector Set beta
8.2(2025-08)kvobj 统一存储、短字符串内存优化、I/O 吞吐提升
8.4(2025-11)FT.HYBRID 混合检索;Atomic Slot Migration
8.6(2026-02)HOTKEYS 热 key 检测;volatile-lrm / allkeys-lrm;Streams XADD 幂等
8.8(2026-05)Array 数据类型;INCREX 窗口限流
8.10(2026-07)compact hash / HIMPORT;BACKUP

注意:Redis 8 的发行包已经包含 Bloom/Cuckoo 等模块,但默认 redis.conf 不会自动加载,需要用 redis-full.conf 或显式 loadmodule;Redis 7.x 仍然依赖 Redis Stack / RedisBloom。8.12 目前是 RC(2026-10-29),生产基线仍是 8.10。


1.11 大 key / 热 key(速记)

问题危害发现治理
大 key阻塞主线程、网络拥塞、迁移慢redis-cli --bigkeys、MEMORY USAGE key、SLOWLOG拆分 key、UNLINK、hash field 过期、避免 KEYS *
热 key单节点/单 slot 过载redis-cli --hotkeys(需 LFU)、HOTKEYS(8.6+)、CLUSTER SLOT-STATS(8.2+)本地缓存、key 拆分、读写分离、迁移热 slot、限流
MEMORY USAGE user:1001
SLOWLOG GET 10
HOTKEYS START METRICS 2 CPU NET COUNT 10 DURATION 60000 SAMPLE 100
HOTKEYS GET

监控重点:命中率、evicted_keys、used_memory / used_memory_rss、碎片率、慢查询、复制延迟。