Redis 缓存机制 及问题场景 及解决方案
Redis
- Redis 作为缓存的核心机制
- Redis 缓存击穿 (Cache Breakdown)
- Redis 缓存穿透 (Cache Penetration)
- Redis 缓存雪崩 (Cache Avalanche)
- 解决方案
- 1.缓存空对象 (Cache Null)
- 2.布隆过滤器 (Bloom Filter)
- 3.接口层校验
- 总结
Redis 的缓存机制和缓存穿透问题
Redis 作为缓存的核心机制
Redis 的核心价值之一就是作为高性能的缓存层,位于应用程序和持久化存储(通常是数据库如 MySQL, PostgreSQL)之间。它的工作原理和优势如下:
1.核心思想:
- 快速访问: 利用内存的高速读写能力(远快于磁盘),存储经常被访问的数据副本。
- 减轻后端压力: 拦截对数据库的重复查询,降低数据库的负载,提高整体系统吞吐量和响应速度
- 数据局部性: 服务于热点数据(被频繁访问的数据)。
2.常见缓存读写策略 (Cache-Aside / Lazy Loading):
读操作 (Read): 应用程序首先尝试从 Redis 缓存中读取数据
1.缓存命中 (Cache Hit): 如果数据存在于 Redis 中,直接返回给应用程序
2.缓存未命中 (Cache Miss): 如果数据不在 Redis 中:
a. 应用程序从数据库中查询所需数据
b.将查询到的数据写入 Redis 缓存(通常设置一个过期时间 TTL)
c.将数据返回给应用程序
写操作 (Write): 应用程序直接更新数据库
1.失效 (Invalidate): 同时使 Redis 缓存中对应的数据失效(删除对应的 Key)。下次读取时,会触发缓存未命中流程,从数据库加载最新数据到缓存。
2. 写穿 (Write-Through): 应用程序同时更新数据库和缓存(确保缓存一致性,但写操作变慢)。更常见的是上面的失效策略
关键点: 缓存数据通常设置一个过期时间 (TTL - Time To Live) 这保证了:
1.即使缓存更新失败(写失效未成功),旧数据会在一定时间后被自动清除,下次读取会加载新数据
2.防止冷数据(不再被频繁访问的数据)长期占用宝贵的内存空间
3.Redis 作为缓存的优势:
* 极高的性能: 基于内存,读写速度极快(微秒级)
* 丰富的数据结构: String, Hash, List, Set, Sorted Set 等,能灵活存储各种形式的数据
* 持久化选项: RDB快照和AOF日志,虽然缓存数据通常不强调持久化,但提供了一定的可靠性选项
* 高可用与扩展: Redis Sentinel(主从故障切换), Redis Cluster(分布式分片)。
* 原子操作与 Lua 脚本: 支持复杂操作的原子性执行
Redis 缓存击穿 (Cache Breakdown)
缓存击穿 (Cache Breakdown): 指的是一个热点 Key(大量并发访问)在缓存过期的瞬间,大量请求同时发现缓存失效,瞬间穿透到数据库去查询同一个数据,造成数据库压力陡增。 关键: 数据存在,只是缓存恰好失效了,并且是热点数据
Redis 缓存穿透 (Cache Penetration)
缓存穿透 (Cache Penetration): 查询的数据根本不存在于数据库(和缓存),无论缓存是否过期。关键: 请求的数据本身就不存在
1.缓存穿透的场景:
- 业务逻辑漏洞/参数错误: 用户输入了非法的参数(如不存在的用户 ID、商品 ID)。
- 恶意攻击: 黑客故意构造大量数据库中根本不存在的 Key 发起请求,目的是压垮数据库。
2. 本质问题:
- 这些查询本身是无效的(请求的数据根本不存在)。
- 恶意攻击者或程序错误可能故意、大量地发起这类无效查询
3. 造成的影响:
- 数据库压力剧增: 大量无效请求直接打到数据库,消耗数据库连接和 CPU 资源
- 性能下降: 数据库忙于处理这些无效查询,导致处理正常查询的能力下降,整体系统响应变慢
- 潜在宕机风险: 在极端情况下,如果攻击流量足够大,数据库可能因不堪重负而宕机
Redis 缓存雪崩 (Cache Avalanche)
指在某一时刻,缓存中大量的 Key 同时过期失效,导致所有对这些 Key 的请求瞬间都穿透到数据库,造成数据库压力巨大甚至崩溃
4.与缓存击穿、缓存雪崩的区别:
-
缓存击穿 (Cache Breakdown): 指的是一个热点 Key(大量并发访问)在缓存过期的瞬间,大量请求同时发现缓存失效,瞬间穿透到数据库去查询同一个数据,造成数据库压力陡增。 关键: 数据存在,只是缓存恰好失效了,并且是热点数据。
-
缓存雪崩 (Cache Avalanche): 指在某一时刻,缓存中大量的 Key 同时过期失效,导致所有对这些 Key 的请求瞬间都穿透到数据库,造成数据库压力巨大甚至崩溃。 关键: 大量不同的 Key 同时失效
解决方案
解决缓存穿透的核心思路是:识别并拦截这些针对不存在数据的无效请求
,避免它们穿透到数据库
1.缓存空对象 (Cache Null)
做法: 当数据库查询返回为空(数据不存在)时,仍然将一个特殊的“空值”(如 null
, ""
, 或特定标记对象)写入 Redis 缓存,并为其设置一个相对较短的过期时间(比如 1-5 分钟)
效果: 后续相同的无效请求在短时间内会命中这个缓存的空对象,直接返回(或按空数据处理),而不会访问数据库。防止缓存穿透而造成的数据库压力
优点: 实现简单,对代码侵入性小。
缺点:
1.内存浪费: 如攻击者构造大量不同的无效 Key,会导致缓存中存储大量无意义的空值,占用内存
2.短暂的数据不一致: 如果在空对象缓存有效期内,数据库中实际添加了这个数据,那么在空对象过期前,用户仍然会得到“不存在”的结果
,这种情况可以在如果知道 Key 的前提下,通过在数据写入时主动清除对应的空值缓存来缓解
2.布隆过滤器 (Bloom Filter)
**布隆过滤器(Bloom Filter)**是一种空间效率极高的概率型数据结构
,用于快速判断一个元素是否可能存在于某个集合中。它的核心优势在于用极小的内存空间实现快速查询
,但代价是存在一定的误判率
(False Positive)
原理: 布隆过滤器是一种空间效率极高的概率型数据结构。它利用多个哈希函数
和一个大的位数组(Bit Array)
来实现,核心组件:位数组(Bit Array)
和 多个哈希函数(Hash Functions)
1.位数组(Bit Array)
- 一个长度为
m
的二进制向量(初始值全为0),用于存储数据的存在性信息 - 示例:
[0, 0, 0, 0, 0, 0, 0, 0]
(假设m=8
)
2.多个哈希函数(Hash Functions)
- 一组
k
个独立的哈希函数(如H1, H2, ..., Hk
)。 - 每个函数将输入元素映射到位数组的某个位置范围(范围:
0
到m-1
)
工作流程
1. 添加元素(Insertion)
- 将元素
x
分别通过k
个哈希函数计算,得到k
个哈希值:
H1(x), H2(x), ..., Hk(x)
。 - 将这些哈希值对
m
取模,得到位数组中的k
个位置:
P1 = H1(x) % m, P2 = H2(x) % m, ..., Pk = Hk(x) % m
。 - 将位数组中这
k
个位置的值设为1
。
示例:
添加元素 "apple":
H1("apple") = 5 % 10 = 5
H2("apple") = 97 ('a') % 10 = 7设置位置 5 和 7 为 1:
[0, 0, 0, 0, 0, 1, 0, 1, 0, 0]
2. 查询元素(Lookup)
- 对查询元素
y
执行相同的操作:
计算k
个位置:P1, P2, ..., Pk
- 检查位数组中这些位置的值:
- 如果所有位置的值均为
1
→ 返回"可能存在"
(可能存在误判) - 如果任一位置的值为
0
→ 返回"一定不存在"
(绝对正确)
- 如果所有位置的值均为
此时位数组
[0, 0, 0, 0, 0, 1, 0, 1, 0, 0]检查位置 5 和 7 均为 1 → 返回 "可能存在"(正确)查询元素 "banana"(未插入)
H1("banana") = 6 % 10 = 6 → 位置 6 为 0
发现位置 6 为 0 → 返回 "一定不存在"(正确)查询元素 "grape"(未插入,但发生误判)
H1("grape") = 5 % 10 = 5 → 位置 5 为 1
H2("grape") = 103 ('g') % 10 = 3 → 位置 3 为 0
发现位置 3 为 0 → 返回 "一定不存在"(正确)查询元素 "mango"(未插入,误判场景):
假设 H1("mango") = 5(位置 5 已被设为 1)
假设 H2("mango") = 7(位置 7 已被设为 1)
两个位置均为 1 → 返回 "可能存在"(实际不存在,误判)
关键特性
1.空间效率极高:不存储元素本身,只存储 k 个比特位的状态
示例:
存储 1 亿个元素,误判率 1% 时,仅需约 114MB 内存(传统哈希表需 GB 级内存)
2.查询时间复杂度为 O(k)
仅需计算 k 次哈希并检查位数组,速度极快(常数时间)
3. 误判率(False Positive)
误判场景:
元素 z 不在集合中,但 k 个位置恰好被其他元素设为 1(哈希碰撞)
4.不支持删除操作
将某个位置从 1 改为 0 会影响其他映射到该位置的元素(导致漏判)
解决方案:
使用变种如 计数布隆过滤器(Counting Bloom Filter)(用计数器代替比特位)
布隆过滤器 (Bloom Filter)在缓存穿透中的应用:
1.系统启动时或数据变更时,将所有真实存在的有效 Key 预热到布隆过滤器中。
2.在查询缓存之前,先查询布隆过滤器:
如果布隆过滤器说 Key 不存在(至少有一位是0),则直接返回“数据不存在”给客户端,无需查询缓存和数据库。
如果布隆过滤器说 Key 可能存在(所有位都是1),则继续正常的缓存查询流程(查缓存 -> 未命中则查数据库 -> 回填缓存或缓存空值)。
优点:
空间效率极高: 存储海量 Key 的“存在”信息所需内存远小于直接存储 Key 本身。
查询效率极高: 判断 Key 是否“可能存在”非常快(常数时间复杂度 O(K))。
缺点:
误判率 (False Positive): 存在一定的概率,将不存在的 Key 误判为可能存在(所有位碰巧都被其他 Key 置1了)。这意味着无法完全避免穿透(误判时还是会去查库),但能拦截绝大部分无效请求。
无法删除: 标准的布隆过滤器不支持删除元素(删除一个 Key 会影响其他共享相同位的 Key)
可以使用变种如计数布隆过滤器 (Counting Bloom Filter) 或 布谷鸟过滤器 (Cuckoo Filter) 来支持删除,但代价是更大的空间或计算开销。
需要预热: 需要将有效 Key 初始化到过滤器中。
3.接口层校验
做法: 在请求到达业务逻辑和缓存查询之前,在最外层(如 API 网关、Controller 层)对请求参数进行基础校验。
例子:
1.检查用户 ID 是否符合格式(如必须是数字且在一定范围内)
2.检查商品 ID 是否存在非法字符。
3.对访问频率进行限制(Rate Limiting)。
4.黑名单过滤已知的攻击模式或 IP。
效果: 拦截掉一部分明显非法的请求,防止它们进入后续流程(包括缓存查询)。
优点: 简单直接,能有效拦截低级攻击和错误输入。
缺点: 无法防御精心构造的、参数格式合法的无效请求(如随机生成在合法范围内的不存在的 ID)
总结
- Redis 缓存: 是提升系统性能的关键组件,通过内存存储热点数据,减少数据库访问。
- 缓存穿透: 是查询不存在的数据导致请求绕过缓存直击数据库的问题,危害巨大。
- 解决方案:
- 缓存空对象: 简单有效,适用于已知的不存在 Key,但可能浪费内存且有短暂不一致性。
- 布隆过滤器: 空间效率高,能高效拦截绝大部分无效请求,是防御大规模恶意攻击穿透的首选方案,但有误判率且需预热。
- 接口层校验: 作为第一道防线,拦截明显非法请求,是必要的补充手段。
- 实际应用: 通常组合使用这些方案。例如,在网关层做基础校验和限流,在业务层使用布隆过滤器拦截已知不存在的 Key,对于穿透布隆过滤器的请求(可能是误判也可能是新无效 Key),配合缓存空对象策略。
理解缓存穿透及其解决方案,对于构建高性能、高可用的基于 Redis 缓存的系统至关重要。