Redis作为缓存层是高并发系统中最常见的架构模式,但缓存穿透、缓存击穿、缓存雪崩三类故障会在流量高峰时直接击穿到数据库层,导致服务不可用。这三类问题的防护需要从缓存策略设计、数据预热、自动恢复机制三个层面系统解决,布隆过滤器是其中性价比最高的防护手段之一。
缓存穿透:查询不存在的数据
缓存穿透指大量请求查询数据库中不存在的key,Redis中自然也没有,每次请求都穿透到数据库。常见场景是恶意攻击或爬虫扫描:
// 缓存穿透漏洞 - 不缓存null值
public User getUserById(Long id) {
String key = "user:" + id;
String cached = redis.get(key);
if (cached != null) {
return "NULL".equals(cached) ? null : JSON.parseObject(cached, User.class);
}
User user = userMapper.selectById(id);
if (user == null) {
redis.setex(key, 60, "NULL"); // 修复:缓存空值短TTL
return null;
}
redis.setex(key, 3600, JSON.toJSONString(user));
return user;
}
缓存空值能阻挡大部分穿透,但占用Redis内存且过期后仍是穿透窗口。布隆过滤器从源头过滤不存在的key:
// 布隆过滤器方案:Redisson实现
RBloomFilter userFilter = redisson.getBloomFilter("user:bloom");
userFilter.tryInit(10_000_000L, 0.0001); // 1000万元素, 0.01%误判率, 约17MB
public void onUserCreated(User user) {
userMapper.insert(user);
userFilter.add(user.getId());
redis.setex("user:" + user.getId(), 3600, JSON.toJSONString(user));
}
public User getUserById(Long id) {
// 第一道防线:布隆过滤器
if (!userFilter.contains(id)) {
return null; // 不存在则一定不存在(无假阴性)
}
// 第二道防线:Redis缓存
String key = "user:" + id;
String cached = redis.get(key);
if (cached != null) {
return "NULL".equals(cached) ? null : JSON.parseObject(cached, User.class);
}
// 第三道防线:数据库
User user = userMapper.selectById(id);
if (user == null) {
redis.setex(key, 60, "NULL"); // 误判时缓存空值
return null;
}
redis.setex(key, 3600, JSON.toJSONString(user));
return user;
}
布隆过滤器的空间效率极高:1000万元素、0.01%误判率仅需17MB内存。假阳性概率可控,假阴性概率为零。
缓存击穿:热点Key过期
缓存击穿指单个热点key过期瞬间,大量并发请求同时穿透到数据库。与穿透的区别是key在数据库中存在,只是Redis恰好过期:
// 互斥锁方案
public Product getHotProductWithMutex(Long id) {
String key = "product:hot:" + id;
String lockKey = "lock:product:" + id;
String cached = redis.get(key);
if (cached != null) return JSON.parseObject(cached, Product.class);
// 尝试获取分布式锁
String requestId = UUID.randomUUID().toString();
Boolean locked = redis.set(lockKey, requestId, "NX", "EX", 10);
if (locked != null && locked) {
try {
// 双重检查
cached = redis.get(key);
if (cached != null) return JSON.parseObject(cached, Product.class);
Product product = productMapper.selectById(id);
redis.setex(key, 300, JSON.toJSONString(product));
return product;
} finally {
// Lua脚本释放锁(保证原子性)
String script = "if redis.call('get',KEYS[1])==ARGV[1] then return redis.call('del',KEYS[1]) else return 0 end";
redis.eval(script, Collections.singletonList(lockKey), Collections.singletonList(requestId));
}
} else {
Thread.sleep(50);
return getHotProductWithMutex(id); // 重试
}
}
缓存雪崩:大量Key同时过期
缓存雪崩与击穿的区别是规模——大量key同一时间过期,或Redis节点宕机:
// 修复TTL随机化
public void preloadProducts(List products) {
Random random = new Random();
for (Product p : products) {
int ttl = 300 + random.nextInt(120); // 300-420秒随机TTL
redis.setex("product:" + p.getId(), ttl, JSON.toJSONString(p));
}
}
// 多级缓存架构
public class MultiLevelCache {
private final CaffeineCache localCache; // L2: 本地缓存
private final RedisTemplate redis; // L1: Redis
private final ProductMapper mapper; // L0: 数据库
public Product getProduct(Long id) {
String key = "product:" + id;
// L2本地缓存
Product cached = localCache.getIfPresent(key);
if (cached != null) return cached;
// L1 Redis
String val = redis.opsForValue().get(key);
if (val != null) {
Product p = JSON.parseObject(val, Product.class);
localCache.put(key, p);
return p;
}
// L0 数据库
Product product = mapper.selectById(id);
if (product != null) {
redis.opsForValue().set(key, JSON.toJSONString(product), 300, TimeUnit.SECONDS);
localCache.put(key, product);
}
return product;
}
}
布隆过滤器的数学原理与容量规划
import math
def bloom_filter_stats(n, p):
m = -n * math.log(p) / (math.log(2) ** 2)
k = (m / n) * math.log(2)
return int(m / 8), math.ceil(k)
for n, p in [(1_000_000, 0.001), (10_000_000, 0.0001), (100_000_000, 0.00001)]:
mem_kb, k = bloom_filter_stats(n, p)
print(f"n={n:>10} p={p} mem={mem_kb/1024:.1f}KB k={k}")
# n=1000000 p=0.001 mem=179.7KB k=10
# n=10000000 p=0.0001 mem=1797.2KB k=14
# n=100000000 p=0.00001 mem=26853KB k=17
误判率从0.1%降到0.001%内存翻倍但仍是可接受开销。实际项目中0.01%误判率已是够用防护。
热点Key发现与动态防护
// 滑动窗口计数检测热点Key
public class HotKeyDetector {
private final Map counters = new ConcurrentHashMap<>();
private final long threshold = 1000;
@Scheduled(fixedRate = 10_000)
public void slideWindow() {
counters.entrySet().removeIf(e -> {
if (e.getValue().sum() > threshold) {
promoteToHotKey(e.getKey());
return true;
}
return false;
});
}
public boolean isHotKey(String key) {
counters.computeIfAbsent(key, k -> new LongAdder()).increment();
return counters.get(key).sum() > threshold;
}
}
熔断降级兜底方案
// Resilience4j熔断器保护数据库
CircuitBreakerConfig config = CircuitBreakerConfig.custom()
.failureRateThreshold(50)
.slowCallRateThreshold(80)
.slowCallDurationThreshold(Duration.ofMillis(500))
.minimumNumberOfCalls(50)
.waitDurationInOpenState(Duration.ofSeconds(30))
.build();
CircuitBreaker breaker = CircuitBreaker.of("dbQuery", config);
public Product getProductWithFallback(Long id) {
Supplier supplier = () -> {
String key = "product:" + id;
String cached = redis.get(key);
if (cached != null) return JSON.parseObject(cached, Product.class);
Product product = productMapper.selectById(id);
if (product != null) redis.setex(key, 300, JSON.toJSONString(product));
return product;
};
Supplier decorated = CircuitBreaker.decorateSupplier(breaker, supplier);
try {
return decorated.get();
} catch (Exception e) {
Product fallback = localCache.getIfPresent("product:" + id);
return fallback != null ? fallback : getDefaultProduct(id);
}
}
缓存系统的稳定性设计遵循防御纵深原则:布隆过滤器挡住不存在的key查询,Redis缓存挡住大部分请求,互斥锁挡住热点key并发穿透,多级缓存挡住Redis故障,熔断器挡住数据库过载。每一层防护都是下一层的缓冲,缺失任何一层都可能在极端流量下引发级联故障。
原创文章,作者:小编,如若转载,请注明出处:https://www.yunthe.com/redis-huan-cun-ce-lyue-shi-zhan-chuan-tou-ji-chuan-xue-beng/