缓存一致性Redis领域建模与架构设计 - 第3节
3.1 缓存穿透:定义与成因
缓存穿透是指客户端请求的数据在缓存层和数据库层均不存在,导致每次请求都直接穿透缓存层,直达数据库。当大量此类请求并发涌入时,数据库将承受巨大压力,甚至引发宕机。
3.1.1 核心成因
- 业务逻辑漏洞:未对非法参数(如负数ID、超长字符串)进行前置校验。
- 恶意攻击或爬虫:攻击者构造大量不存在的Key进行遍历,绕过缓存直接攻击数据库。
- 缓存与数据库数据不同步:数据已被删除,但缓存未及时清理,导致请求持续穿透。
关键指标:当缓存命中率从正常值(通常 > 95%)突然下降至 60% 以下,且数据库 QPS 同步飙升时,极大概率发生了缓存穿透。
3.1.2 穿透流程示意
3.2 布隆过滤器原理与实现
布隆过滤器(Bloom Filter)是一种空间效率极高的概率型数据结构,用于判断一个元素是否可能存在于集合中。它允许一定的误判率(False Positive),但绝不会漏判(False Negative 为零)。
3.2.1 核心原理
- 位数组:一个长度为 m 的二进制位数组,初始所有位为 0。
- 多个哈希函数:使用 k 个相互独立的哈希函数,将元素映射到位数组的 k 个位置。
- 写入过程:将元素经 k 个哈希函数计算,将对应位数组位置置为 1。
- 查询过程:计算元素的 k 个哈希位置,若所有位均为 1,则判定“可能存在”;若任意一位为 0,则判定“一定不存在”。
误判率公式:误判率 p ≈ (1 - e-kn/m)k。当 k = (m/n) * ln2 时,误判率最低。实际应用中,通常将误判率控制在 1% 以内。
3.2.2 布隆过滤器结构示意
3.2.3 代码实现示例(Guava 布隆过滤器)
import com.google.common.hash.BloomFilter;
import com.google.common.hash.Funnels;
import java.nio.charset.Charset;
public class BloomFilterDemo {
public static void main(String[] args) {
// 预期插入 10000 个元素,误判率 1%
BloomFilter<String> bloomFilter = BloomFilter.create(
Funnels.stringFunnel(Charset.defaultCharset()),
10000,
0.01
);
// 预热:将数据库中存在的 ID 写入布隆过滤器
for (long id = 1; id <= 10000; id++) {
bloomFilter.put("user:" + id);
}
// 查询:判断 Key 是否可能存在
String queryKey = "user:99999"; // 不存在的 Key
if (!bloomFilter.mightContain(queryKey)) {
System.out.println("Key 一定不存在,直接拒绝请求");
// 直接返回空结果,不查询数据库
} else {
System.out.println("Key 可能存在,继续查询缓存/数据库");
}
}
}
3.3 缓存空对象策略
缓存空对象策略是指:当数据库查询结果为空时,仍然将该空结果(如 null、空字符串、特殊标记)写入缓存,并设置一个较短的过期时间。后续相同请求在缓存过期前直接返回空结果,避免重复穿透到数据库。
3.3.1 策略对比
| 对比维度 | 布隆过滤器 | 缓存空对象 |
|---|---|---|
| 内存占用 | 极低(位数组) | 较高(存储空值 Key) |
| 实现复杂度 | 中等(需引入额外组件) | 简单(直接修改缓存逻辑) |
| 误判风险 | 有误判率(可配置) | 无误判 |
| 应对恶意攻击 | 优秀(固定内存消耗) | 一般(大量空 Key 会占用缓存空间) |
| 数据一致性 | 需同步更新布隆过滤器 | 空值过期后自动失效 |
3.3.2 缓存空对象实现示例
public Object queryWithNullCache(String key) {
// 1. 查询缓存
Object cacheValue = redisTemplate.opsForValue().get(key);
if (cacheValue != null) {
// 判断是否为缓存的空对象
if ("NULL_VALUE".equals(cacheValue)) {
return null; // 直接返回空
}
return cacheValue;
}
// 2. 缓存未命中,查询数据库
Object dbValue = database.query(key);
// 3. 无论数据库结果是否为空,都写入缓存
if (dbValue == null) {
// 缓存空对象,设置较短过期时间(如 60 秒)
redisTemplate.opsForValue().set(key, "NULL_VALUE", 60, TimeUnit.SECONDS);
} else {
// 缓存正常数据,设置正常过期时间
redisTemplate.opsForValue().set(key, dbValue, 3600, TimeUnit.SECONDS);
}
return dbValue;
}
注意事项:缓存空对象策略需要防范“空值泛滥”问题。当恶意攻击使用大量不同 Key 时,Redis 会存储大量空值 Key,导致内存暴涨。建议:① 设置较短的 TTL(30-120 秒);② 结合布隆过滤器前置过滤,减少空值写入量。
3.4 穿透防护的架构设计
生产环境中,单一防护手段往往不够。推荐采用分层防御架构,在请求链路的多个环节设置屏障,将穿透风险降至最低。
3.4.1 分层防御架构
3.4.2 架构决策矩阵
| 业务场景 | 推荐方案 | 理由 |
|---|---|---|
| 高并发读、Key 集合固定 | 布隆过滤器 + 参数校验 | 内存占用稳定,性能损耗极小 |
| Key 动态变化、数据频繁增删 | 缓存空对象 + 短 TTL | 无需维护布隆过滤器,实现简单 |
| 恶意攻击、Key 随机性强 | 布隆过滤器 + 限流熔断 | 前置过滤 + 后端保护,双重保障 |
| 资源敏感、内存有限 | 布隆过滤器(低误判率配置) | 内存占用远小于缓存空对象 |
3.4.3 生产环境最佳实践
- 预热机制:系统启动时,将数据库中所有存在的 Key 批量加载到布隆过滤器中。
- 增量同步:新增数据时,同步更新布隆过滤器;删除数据时,布隆过滤器无法删除,可配合缓存空对象短 TTL 自动失效。
- 监控告警:监控缓存命中率、数据库 QPS、布隆过滤器误判率,设置阈值告警。
- 降级策略:当布隆过滤器服务异常时,自动降级为缓存空对象模式,确保系统可用性。
推荐组合:参数校验(前置拦截 80% 非法请求)→ 布隆过滤器(拦截 99% 不存在 Key)→ 缓存空对象(兜底剩余 1%)→ 限流熔断(保护数据库)。四层叠加后,穿透到数据库的请求可降低至原始流量的 0.01% 以下。