Redis缓存策略设计中,缓存穿透、缓存击穿和缓存雪崩是三类经典问题。这三个问题如果不做工程化处理,在高并发场景下会导致数据库连接池耗尽、响应超时甚至服务雪崩。本文通过布隆过滤器、互斥锁、随机TTL和多级缓存四种方案,给出完整的工程化解决方案和Java代码实现。
缓存穿透问题与布隆过滤器
缓存穿透指大量请求查询不存在的数据,请求绕过缓存直接打到数据库。常见原因是恶意攻击(如用不存在的ID发请求)或业务Bug。数据库频繁查询不存在的记录,会导致连接池耗尽和响应超时。
解决方案分两层:布隆过滤器拦截和空值缓存。布隆过滤器通过位数组和哈希函数判断元素是否存在。判断不存在则一定不存在,判断存在可能有误判。将数据库中存在的Key预热到布隆过滤器中,请求先经过布隆过滤器校验:
// Redisson布隆过滤器实现
@Configuration
public class BloomFilterConfig {
@Bean
public RBloomFilter<Long> userBloomFilter(RedissonClient redisson) {
RBloomFilter<Long> filter = redisson.getBloomFilter("user:bloom");
// 初始化:预计元素100万,误判率0.01%
filter.tryInit(1_000_000L, 0.0001);
return filter;
}
}
// 查询逻辑
@Service
public class UserService {
@Autowired
private RBloomFilter<Long> userBloomFilter;
@Autowired
private RedisTemplate<String, User> redisTemplate;
@Autowired
private UserMapper userMapper;
public User getUserById(Long id) {
// 第一层:布隆过滤器校验
if (!userBloomFilter.contains(id)) {
return null; // 一定不存在,直接返回
}
// 第二层:查缓存
String key = "user:" + id;
User user = redisTemplate.opsForValue().get(key);
if (user != null) {
return user;
}
// 第三层:查数据库
user = userMapper.selectById(id);
if (user != null) {
redisTemplate.opsForValue().set(key, user, 30, TimeUnit.MINUTES);
} else {
// 空值缓存,短TTL
redisTemplate.opsForValue().set(key, new User(), 5, TimeUnit.MINUTES);
}
return user;
}
}
空值缓存设置较短的TTL(5分钟),防止数据库后续新增了该记录但缓存中一直是空值。布隆过滤器的误判率设置越低,位数组占用内存越大。100万数据量下,0.01%误判率约需要1.2MB内存。
缓存击穿与互斥锁方案
缓存击穿指某个热点Key在缓存中过期的瞬间,大量并发请求同时查询该Key,全部穿透到数据库。与穿透不同,击穿的数据是真实存在的,只是缓存恰好失效。
互斥锁(Mutex Lock)是解决缓存击穿的经典方案。同一时间只允许一个线程查询数据库并重建缓存,其他线程等待或重试:
public User getUserWithMutex(Long id) {
String key = "user:" + id;
User user = redisTemplate.opsForValue().get(key);
if (user != null) {
return user;
}
// 缓存未命中,尝试获取互斥锁
String lockKey = "lock:user:" + id;
try {
// 尝试加锁,等待3秒,锁自动释放10秒
boolean locked = redisTemplate.opsForValue()
.setIfAbsent(lockKey, "1", 10, TimeUnit.SECONDS);
if (locked) {
try {
// 双重检查:获取锁后再次查缓存
user = redisTemplate.opsForValue().get(key);
if (user != null) {
return user;
}
// 查数据库
user = userMapper.selectById(id);
if (user != null) {
// 随机TTL防止雪崩
int ttl = 1800 + ThreadLocalRandom.current().nextInt(600);
redisTemplate.opsForValue().set(key, user, ttl, TimeUnit.SECONDS);
}
return user;
} finally {
// 释放锁
redisTemplate.delete(lockKey);
}
} else {
// 未获取到锁,短暂等待后重试
Thread.sleep(50);
return getUserWithMutex(id);
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new RuntimeException("获取用户信息被中断", e);
}
}
双重检查(Double Check)是关键设计:获取锁后再次查询缓存,避免前一个持锁线程已经重建缓存但当前线程仍执行数据库查询。锁的自动过期时间(10秒)需要大于数据库查询时间,防止锁提前释放导致多个线程同时查库。
缓存雪崩与随机TTL策略
缓存雪崩指大量Key在同一时间过期,或Redis节点宕机,导致大量请求同时打到数据库。与击穿的区别在于雪崩是大量Key同时失效,而非单个热点Key。
核心预防措施是给TTL添加随机偏移量,避免大量Key同时过期:
public void cacheUser(User user) {
String key = "user:" + user.getId();
// 基础TTL 30分钟 + 随机0-10分钟
int baseTTL = 1800;
int randomTTL = ThreadLocalRandom.current().nextInt(600);
redisTemplate.opsForValue().set(key, user, baseTTL + randomTTL, TimeUnit.SECONDS);
}
// 批量缓存时同样需要随机化
public void batchCacheUsers(List<User> users) {
users.forEach(user -> {
String key = "user:" + user.getId();
int ttl = 1800 + ThreadLocalRandom.current().nextInt(600);
redisTemplate.opsForValue().set(key, user, ttl, TimeUnit.SECONDS);
});
}
Redis节点宕机导致的雪崩需要从架构层面解决。Redis Cluster提供自动failover能力,但当主节点切换期间,大量请求会失败。兜底方案是本地缓存(Caffeine)作为二级缓存:
@Configuration
public class MultiLevelCacheConfig {
@Bean
public Cache<String, User> localCache() {
return Caffeine.newBuilder()
.maximumSize(10_000)
.expireAfterWrite(5, TimeUnit.MINUTES)
.recordStats()
.build();
}
}
@Service
public class MultiLevelCacheService {
@Autowired
private Cache<String, User> localCache;
@Autowired
private RedisTemplate<String, User> redisTemplate;
public User getUser(Long id) {
String key = "user:" + id;
// L1:本地缓存
User user = localCache.getIfPresent(key);
if (user != null) {
return user;
}
// L2:Redis缓存
user = redisTemplate.opsForValue().get(key);
if (user != null) {
localCache.put(key, user);
return user;
}
// L3:数据库
user = userMapper.selectById(id);
if (user != null) {
int ttl = 1800 + ThreadLocalRandom.current().nextInt(600);
redisTemplate.opsForValue().set(key, user, ttl, TimeUnit.SECONDS);
localCache.put(key, user);
}
return user;
}
}
本地缓存的TTL应短于Redis缓存,确保数据一致性。本地缓存命中率可通过Caffeine的recordStats()监控,如果命中率低于20%,说明缓存Key分散度过高,需要调整maximumSize或预热策略。
缓存监控与告警配置
Redis缓存策略的效果需要通过监控数据验证。关键监控指标包括缓存命中率、内存使用率、连接数和慢查询。生产环境建议使用Redis Exporter + Prometheus + Grafana实现自动化监控告警:
# Prometheus告警规则
groups:
- name: redis_cache
rules:
- alert: RedisLowHitRate
expr: |
redis_keyspace_hits_total{instance="redis:9121"}
/ (redis_keyspace_hits_total{instance="redis:9121"}
+ redis_keyspace_misses_total{instance="redis:9121"})
* 100 < 80
for: 5m
labels:
severity: warning
annotations:
summary: "Redis缓存命中率低于80%"
- alert: RedisHighMemoryUsage
expr: redis_memory_used_bytes{instance="redis:9121"}
/ redis_memory_max_bytes{instance="redis:9121"} * 100 > 85
for: 2m
labels:
severity: critical
annotations:
summary: "Redis内存使用率超过85%"
命中率告警阈值为80%,持续5分钟触发。内存使用率告警阈值85%,持续2分钟触发critical级别告警。收到告警后,需要排查是否存在缓存Key设计不合理(如缓存粒度过细导致命中率低)、或者是否需要扩容Redis实例。数据库高可用架构设计中,Redis缓存作为数据库的前置防线,其稳定性直接影响整个系统的可用性。
原创文章,作者:小编,如若转载,请注明出处:https://www.yunthe.com/redis-huan-cun-ce-lyue-she-ji-chuan-tou-ji-chuan-yu-xue/