Redis作为缓存层大幅提升读取性能,但缓存穿透、缓存雪崩、缓存击穿三类故障在高并发场景下频繁出现。本文从问题诊断出发,给出布隆过滤器防穿透、随机过期防雪崩、多级缓存防击穿的完整方案,附带可直接复用的代码实现。
缓存穿透问题诊断与布隆过滤器方案
缓存穿透指查询一个数据库中不存在的数据,缓存和数据库都未命中,每次请求都穿透到数据库。恶意攻击或爬虫高频请求不存在的ID时,数据库连接池可能被耗尽。
传统方案是缓存空值(null),但无法应对海量随机不存在的Key。布隆过滤器(Bloom Filter)在缓存之前增加一道预检层,以极高空间效率判断Key是否可能存在:
// 使用Redisson的布隆过滤器
Config config = new Config();
config.useSingleServer().setAddress("redis://127.0.0.1:6379");
RedissonClient redisson = Redisson.create(config);
RBloomFilter<Long> bloomFilter = redisson.getBloomFilter("product:bloom");
// 初始化:预计元素数量100万,误判率0.01%
bloomFilter.tryInit(1_000_000L, 0.0001);
// 数据预热:将所有存在的商品ID加入布隆过滤器
List<Long> allProductIds = productMapper.selectAllIds();
allProductIds.forEach(bloomFilter::add);
public Product getProduct(Long id) {
// 第一层:布隆过滤器预检
if (!bloomFilter.contains(id)) {
return null; // 一定不存在,直接返回
}
// 第二层:查Redis缓存
String cacheKey = "product:" + id;
Product product = (Product) redisTemplate.opsForValue().get(cacheKey);
if (product != null) {
return product;
}
// 第三层:查数据库
product = productMapper.selectById(id);
if (product != null) {
redisTemplate.opsForValue().set(cacheKey, product, 30, TimeUnit.MINUTES);
} else {
// 缓存空值,短过期时间
redisTemplate.opsForValue().set(cacheKey, "NULL", 60, TimeUnit.SECONDS);
}
return product;
}
布隆过滤器的误判率(false positive)可控但无法消除。tryInit(1_000_000, 0.0001)表示100万元素下误判率0.01%,占用约1.8MB内存。被误判为”可能存在”的Key会进入缓存和数据库查询,但比例极低。布隆过滤器不支持删除,数据删除时需定期重建或使用布谷鸟过滤器(Cuckoo Filter)替代。
缓存雪崩的随机过期时间策略
缓存雪崩指大量Key在同一时间过期,请求全部涌向数据库。常见原因是批量设置缓存时使用了相同的过期时间。解决方案是给过期时间加上随机偏移:
public void cacheProduct(Product product) {
String cacheKey = "product:" + product.getId();
// 基础过期30分钟 + 0~10分钟随机偏移
int baseTTL = 1800;
int randomTTL = ThreadLocalRandom.current().nextInt(0, 600);
redisTemplate.opsForValue().set(
cacheKey,
product,
baseTTL + randomTTL,
TimeUnit.SECONDS
);
}
// 批量预热时的分散策略
public void warmupCache(List<Product> products) {
products.parallelStream().forEach(p -> {
int ttl = 1800 + ThreadLocalRandom.current().nextInt(0, 600);
redisTemplate.opsForValue().set("product:" + p.getId(), p, ttl, TimeUnit.SECONDS);
});
}
随机偏移量建议设为基础过期时间的10%到30%。此外,Redis实例宕机也会引发雪崩,生产环境必须部署Redis Cluster或Sentinel保证高可用。Redis Cluster配置至少3主3从,主节点宕机时从节点自动切换。应用层通过熔断器(如Sentinel或Resilience4j)限制数据库并发查询数,防止缓存层失效时数据库被压垮:
@CircuitBreaker(name = "dbQuery", fallbackMethod = "fallbackQuery")
public Product queryFromDB(Long id) {
return productMapper.selectById(id);
}
// application.yml 熔断配置
resilience4j:
circuitbreaker:
instances:
dbQuery:
sliding-window-size: 100
failure-rate-threshold: 50
wait-duration-in-open-state: 30s
permitted-number-of-calls-in-half-open-state: 10
缓存击穿与热点Key多级缓存
缓存击穿指某个热点Key过期瞬间,大量并发请求同时查数据库。与雪崩的区别在于击穿是单个Key,雪崩是大量Key。解决方案是互斥锁(Mutex)防止缓存重建时的并发穿透:
public Product getHotProduct(Long id) {
String cacheKey = "product:" + id;
Product product = (Product) redisTemplate.opsForValue().get(cacheKey);
if (product != null) {
return product;
}
// 获取分布式锁,只有一个线程查DB
String lockKey = "lock:product:" + id;
String lockValue = UUID.randomUUID().toString();
try {
boolean locked = redisTemplate.opsForValue()
.setIfAbsent(lockKey, lockValue, 10, TimeUnit.SECONDS);
if (locked) {
// 双重检查:拿到锁后再查一次缓存
product = (Product) redisTemplate.opsForValue().get(cacheKey);
if (product != null) {
return product;
}
// 查数据库并重建缓存
product = productMapper.selectById(id);
int ttl = 1800 + ThreadLocalRandom.current().nextInt(0, 600);
redisTemplate.opsForValue().set(cacheKey, product, ttl, TimeUnit.SECONDS);
return product;
} else {
// 未获取锁,短暂等待后重试
Thread.sleep(50);
return getHotProduct(id);
}
} finally {
// 释放锁(Lua脚本保证原子性)
releaseLock(lockKey, lockValue);
}
}
双重检查(Double-Check)是关键:获取锁后再次查询缓存,避免重复构建。锁的超时时间设为略大于数据库查询耗时,防止死锁。等待重试方案在高并发下可能堆积请求,可改用信号量限制并发数。
多级缓存架构整合
将以上策略整合为完整的多级缓存架构。请求链路:本地缓存(Caffeine)→ 分布式缓存(Redis)→ 布隆过滤器预检 → 数据库。本地缓存处理极热点数据,减少Redis网络开销:
@Configuration
public class CacheConfig {
@Bean
public Cache<Long, Product> localCache() {
return Caffeine.newBuilder()
.maximumSize(10_000)
.expireAfterWrite(5, TimeUnit.MINUTES)
.recordStats()
.build();
}
}
public Product getMultiLevelProduct(Long id) {
// L1: 本地缓存 (Caffeine)
Product product = localCache.getIfPresent(id);
if (product != null) {
return product;
}
// 布隆过滤器预检
if (!bloomFilter.contains(id)) {
return null;
}
// L2: 分布式缓存 (Redis) + 互斥锁防击穿
product = getHotProduct(id);
if (product != null) {
// 回填本地缓存
localCache.put(id, product);
}
return product;
}
本地缓存过期时间应短于Redis缓存,保证数据新鲜度。Caffeine的recordStats()开启统计,通过Cache.stats()监控命中率。正常运行的系统,L1命中率应在80%以上,L2命中率95%以上,数据库查询QPS维持在极低水平。当数据库QPS突增时,按L1→L2→布隆过滤器→互斥锁的顺序逐层排查防护是否失效。
原创文章,作者:小编,如若转载,请注明出处:https://www.yunthe.com/redis-huan-cun-chuan-tou-yu-xue-beng-fang-hu-fang-an-bu/