Redis相关必问面试题之三种业务场景的问答、完全吃透理解!
目录
如何在有限的空间内表示一个可能非常大的集合,并快速判断某个元素是否“可能”存在?
Q1:什么是缓存穿透?怎么解决?
缓存穿透是指查询一个一定不存在的数据,由于存储层查不到数据因此不写入缓存,这将导致这个不存在的数据每次请求都要到 DB 去查询,可能导致 DB 挂掉。
这种情况大概率是遭到了攻击。
解决方案的话,我们通常都会用布隆过滤器来解决它。
Q2:你能介绍一下“布隆过滤器”吗?
布隆过滤器主要是用于检索一个元素是否在一个集合中。我们当时使用的是Redisson实现的布隆过滤器。它的底层原理是,先初始化一个比较大的数组(BitMap),里面存放的是二进制0或1。
一开始都是0,当一个key来了之后,经过3次hash计算,模数组长度找到数据的下标,然后把数组中原来的0改为1。这样,三个数组的位置就能标明一个key的存在。查找的过程也是一样的。
当然,布隆过滤器有可能会产生一定的误判,我们一般可以设置这个误判率,大概不会超过5%。其实这个误判是必然存在的,要不就得增加数组的长度。5%以内的误判率一般的项目也能接受,不至于高并发下压倒数据库。
布隆过滤器拓展
如何在有限的空间内表示一个可能非常大的集合,并快速判断某个元素是否“可能”存在?
布隆过滤器的设计初衷就是为了解决这个问题。它在空间效率和查询准确性之间做出了权衡。它牺牲了100%的准确性(存在误判,但不会漏判),换取了极高的空间效率。
为什么使用较小的空间?
现实中我们不可能为每一个可能的 Long
值(资源id值)分配一个比特位。布隆过滤器使用的“较小空间”指的是一个固定大小的位数组(Bit Array),这个大小通常远小于要表示的集合中元素的数量。
为什么需要多次哈希?
这是布隆过滤器的精髓所在,也是为了在有限空间内降低误判率的关键。
假设我们有一个大小为 m
的位数组,以及要表示的 n
个元素。如果我们只用一个哈希函数 h(key)
,将 h(key) % m
对应的位设置为 1。
- 当插入一个元素
key1
时,计算h(key1) % m
,将对应位设置为 1。 - 当查询一个元素
key2
时,计算h(key2) % m
。如果对应位是 1,我们就认为key2
可能存在。
问题来了: 即使 key2
不存在,如果存在另一个元素 key3
,它的哈希值 h(key3)
经过取模后与 h(key2)
相同(即发生哈希冲突),那么 key2
的查询也会返回“可能存在”,这就发生了误判。
为了降低误判率,布隆过滤器使用了多个(k 个)独立的哈希函数。
当插入一个元素 key
时,会计算 k
个哈希值:h1(key), h2(key), ..., hk(key)
。然后将 h1(key) % m, h2(key) % m, ..., hk(key) % m
对应的 k
个位全部设置为 1。
当查询一个元素 key
时,同样计算 k
个哈希值。只有当这 k
个哈希值经过取模后对应的 k
个位全部都为 1 时,我们才认为 key
可能存在。
为什么这样能降低误判率?
误判发生的条件是:一个不存在的元素 key
,它的 k
个哈希值经过取模后对应的 k
个位全部都被其他存在的元素设置为了 1。
如果只用一个哈希函数,误判的概率取决于哈希冲突的概率。 如果使用多个哈希函数,误判的概率取决于这 k
个位置同时被其他元素设置的概率。这个概率要比单个位置被设置的概率低得多。
情景示意图
位数组 (m 个比特位)
[ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, ... ]
插入元素 "apple":
哈希函数1: h1("apple") % m = 2
哈希函数2: h2("apple") % m = 5
哈希函数3: h3("apple") % m = 8
位数组变化:
[ 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, ... ]
查询元素 "banana":
哈希函数1: h1("banana") % m = 1
哈希函数2: h2("banana") % m = 4
哈希函数3: h3("banana") % m = 7
检查位数组:
位置 1: 0
位置 4: 0
位置 7: 0
结论: "banana" 不存在 (因为至少有一个位置是 0)
查询元素 "appla" (一个不存在的相似词):
哈希函数1: h1("appla") % m = 2 (可能是哈希冲突)
哈希函数2: h2("appla") % m = 5 (可能是哈希冲突)
哈希函数3: h3("appla") % m = 9
检查位数组:
位置 2: 1
位置 5: 1
位置 9: 0
结论: "appla" 不存在 (因为位置 9 是 0)
查询元素 "orange" (一个不存在的词,但哈希值巧合):
假设 h1("orange") % m = 2, h2("orange") % m = 5, h3("orange") % m = 8
检查位数组:
位置 2: 1
位置 5: 1
位置 8: 1
结论: "orange" 可能存在 (发生了误判)
Q3:什么是缓存击穿?怎么解决?
缓存击穿:当设置了过期时间的 key 在缓存失效的瞬间,遭遇大量并发请求,导致请求直接穿透到数据库,可能引发 DB 过载宕机。
解决方案
方案一:互斥锁(强一致)
- 请求发现缓存失效 → 尝试用
SETNX
获取分布式锁(SET lock_key 1 EX 10 NX
) - 获锁线程:查询 DB → 回填缓存 → 释放锁
- 未获锁线程:睡眠重试或直接返回旧值(若允许短暂不一致)
优点:保证数据强一致
缺点:性能较低、需处理锁超时(防死锁)
方案二:逻辑过期(高可用)
- 数据存入缓存时:
{ "value": "真实数据", "expire_ts": 1672500000 // 逻辑过期时间戳 }
- 请求读缓存时:
- 若
expire_ts > now
→ 直接返回数据 - 若
expire_ts <= now
→ 立即返回旧数据,并尝试抢锁- 抢锁成功者:启动异步线程更新缓存
- 其他请求:不阻塞,直接返回旧数据
优点:高并发下仍可用
缺点:存在短暂数据不一致(异步更新完成前返回脏数据)
- 若
可能的追问/回答
Q:逻辑过期方案中,如果异步更新失败怎么办?
A:可加入重试机制 + 告警监控,同时设置二次过期时间(如更新失败后5秒再过期),避免脏数据永久残留。Q:互斥锁方案中大量线程等待如何处理?
A:引入锁等待超时机制(如线程最多等待100ms),超时后降级返回兜底数据或错误码,避免雪崩。Q:两种方案如何选择?
A:金融/交易类用方案一(强一致),资讯/商品详情用方案二(高并发优先)。可结合业务容忍度选择。
Q4:什么是缓存雪崩?怎么解决?
指大量缓存 key 因设置了相同或相近的过期时间,在某一时刻同时失效,导致所有请求穿透到数据库,造成 DB 瞬时压力过大甚至宕机。
它与缓存击穿的区别在于,雪崩影响的是大量不同的 key,而击穿是针对某个热点 key。
解决方案
- 设置随机过期时间:在原始过期时间基础上增加一个较小的随机值(如 1-5 分钟),使 key 的失效时间点分散开。
- 构建多级缓存体系:结合本地缓存(如 Caffeine)和分布式缓存(如 Redis)。本地缓存可以拦截部分请求,降低对 Redis 的压力。但需注意多级缓存一致性问题,通常通过设置 TTL 或消息通知来解决。
- 使用 Redis 集群:虽然不能直接解决单个 key 的击穿,但集群通过分片和主从复制提高了整体的可用性和容量,能更好地应对部分节点的失效,但不能完全避免所有 key 同时失效的情况。
- 添加降级和限流策略:
- 降级:当 DB 压力过大时,牺牲非核心功能,如返回兜底数据、旧数据,或关闭部分功能,保障核心服务可用。
- 限流:在缓存层前或服务入口,限制穿透到 DB 的请求数量,防止 DB 被压垮。
缓存穿透、缓存击穿和缓存雪崩的横向对比理解
问题 | 核心概念(一句话) | 触发条件(一句话) | 影响范围(一句话) | 核心解决方案(一句话) |
---|---|---|---|---|
缓存穿透 | 查询不存在数据,导致请求直达DB。 | 大量请求查询不存在的Key | 针对不存在的Key的请求 | 布隆过滤器(拦截不存在的Key)或缓存空结果(避免重复查DB) |
缓存击穿 | 热点Key过期瞬间,大量请求直达DB。 | 单个热点Key过期 + 高并发 | 针对单个热点Key的请求 | 互斥锁(同步更新)或逻辑过期+异步更新(返回旧数据) |
缓存雪崩 | 大量Key同时过期,导致所有请求直达DB。 | 大量Key在同一时刻过期 | 针对大量Key的请求 | 分散过期时间 + 多级缓存 + 限流降级 |