Redis作为缓存层承担了数据库前端的绝大部分读取压力。当缓存大面积失效(雪崩)、热点Key过期(击穿)或恶意请求穿透(穿透)时,流量直接打到数据库导致服务雪崩。本文从多级缓存架构设计、缓存策略配置到熔断降级实现,梳理高可用缓存系统的工程方案。
缓存雪崩的根因与防护方案
缓存雪崩指大量Key在同一时间过期,或Redis节点宕机导致缓存集体失效,所有请求打到数据库。核心防护策略是过期时间随机化和服务降级。
// 过期时间随机化——避免同时失效
public class CacheUtil {
private static final int BASE_EXPIRE = 3600; // 基础过期时间1小时
private static final int RANDOM_RANGE = 600; // 随机偏移量10分钟
public static int getRandomExpire() {
return BASE_EXPIRE + ThreadLocalRandom.current().nextInt(RANDOM_RANGE);
}
// 批量设置缓存时使用随机过期时间
public <T> void batchSet(Map<String, T> dataMap) {
dataMap.forEach((key, value) -> {
redisTemplate.opsForValue().set(
key, value,
getRandomExpire(),
TimeUnit.SECONDS
);
});
}
}
Redis集群宕机场景下的降级策略——本地缓存兜底。使用Caffeine作为二级缓存,Redis不可用时自动切换到本地缓存:
// 多级缓存实现
@Service
public class MultiLevelCacheService {
@Autowired
private RedisTemplate<String, Object> redisTemplate;
// Caffeine本地缓存
private final Cache<String, Object> localCache = Caffeine.newBuilder()
.maximumSize(10000)
.expireAfterWrite(Duration.ofSeconds(300))
.recordStats()
.build();
private final AtomicBoolean redisAvailable = new AtomicBoolean(true);
public <T> T get(String key, TypeReference<T> type, Supplier<T> dbLoader) {
// 1. 先查本地缓存
Object localValue = localCache.getIfPresent(key);
if (localValue != null) {
return (T) localValue;
}
// 2. 查Redis缓存
if (redisAvailable.get()) {
try {
Object redisValue = redisTemplate.opsForValue().get(key);
if (redisValue != null) {
localCache.put(key, redisValue);
return (T) redisValue;
}
} catch (RedisConnectionFailureException e) {
redisAvailable.set(false);
log.warn("Redis连接失败,切换到本地缓存降级模式");
scheduleRedisRetry();
}
}
// 3. 查数据库
T dbValue = dbLoader.get();
if (dbValue != null) {
localCache.put(key, dbValue);
if (redisAvailable.get()) {
try {
redisTemplate.opsForValue().set(
key, dbValue,
CacheUtil.getRandomExpire(), TimeUnit.SECONDS
);
} catch (Exception ignored) {}
}
}
return dbValue;
}
private void scheduleRedisRetry() {
executor.schedule(() -> {
try {
redisTemplate.getConnectionFactory().getConnection().ping();
redisAvailable.set(true);
log.info("Redis恢复,切回正常模式");
} catch (Exception e) {
scheduleRedisRetry();
}
}, 5, TimeUnit.MINUTES);
}
}
缓存击穿的热点Key保护方案
缓存击穿指单个热点Key过期瞬间,大量并发请求同时穿透到数据库。解决方案是互斥锁(Mutex Lock)和逻辑过期。
// 互斥锁方案——防止缓存重建并发
@Service
public class CacheBreakdownProtection {
@Autowired
private StringRedisTemplate stringRedisTemplate;
public <T> T getWithMutex(String key, Supplier<T> dbLoader, long expireSeconds) {
String value = stringRedisTemplate.opsForValue().get(key);
if (value != null) {
return JSON.parseObject(value, new TypeReference<T>(){});
}
// 缓存未命中,获取互斥锁
String lockKey = "lock:" + key;
String lockValue = UUID.randomUUID().toString();
try {
Boolean locked = stringRedisTemplate.opsForValue()
.setIfAbsent(lockKey, lockValue, 10, TimeUnit.SECONDS);
if (Boolean.TRUE.equals(locked)) {
// 获取锁成功,查数据库并重建缓存
T dbValue = dbLoader.get();
stringRedisTemplate.opsForValue().set(
key, JSON.toJSONString(dbValue),
expireSeconds, TimeUnit.SECONDS
);
return dbValue;
} else {
// 获取锁失败,短暂等待后重试
Thread.sleep(50);
return getWithMutex(key, dbLoader, expireSeconds);
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new RuntimeException("缓存重建被中断", e);
} finally {
// 释放锁(Lua脚本保证原子性)
releaseLock(lockKey, lockValue);
}
}
private void releaseLock(String lockKey, String lockValue) {
String luaScript =
"if redis.call('get', KEYS[1]) == ARGV[1] " +
"then return redis.call('del', KEYS[1]) " +
"else return 0 end";
stringRedisTemplate.execute(
new DefaultRedisScript<>(luaScript, Long.class),
Collections.singletonList(lockKey),
lockValue
);
}
}
逻辑过期方案不设置TTL,在value中存储逻辑过期时间。查询时判断逻辑过期,过期则异步重建缓存,当前请求返回旧数据。适用于可以容忍短暂数据不一致的场景:
// 逻辑过期方案
public <T> T getWithLogicalExpire(String key, Supplier<T> dbLoader) {
String json = stringRedisTemplate.opsForValue().get(key);
if (json == null) {
return dbLoader.get();
}
CacheData<T> cacheData = JSON.parseObject(json, new TypeReference<>(){});
if (cacheData.getExpireTime().isAfter(LocalDateTime.now())) {
return cacheData.getData();
}
// 已过期,尝试获取锁异步重建
String lockKey = "lock:" + key;
Boolean locked = stringRedisTemplate.opsForValue()
.setIfAbsent(lockKey, "1", 10, TimeUnit.SECONDS);
if (Boolean.TRUE.equals(locked)) {
executor.submit(() -> {
try {
T newValue = dbLoader.get();
CacheData<T> newData = new CacheData<>(
newValue,
LocalDateTime.now().plusHours(1)
);
stringRedisTemplate.opsForValue().set(key, JSON.toJSONString(newData));
} finally {
stringRedisTemplate.delete(lockKey);
}
});
}
return cacheData.getData();
}
缓存穿透的布隆过滤器防护
缓存穿透指查询不存在的数据,每次请求都穿透到数据库。布隆过滤器在缓存层之前做存在性判断:
// Redisson布隆过滤器实现
@Service
public class BloomFilterService {
@Autowired
private RedissonClient redissonClient;
private RBloomFilter<Long> productIdFilter;
@PostConstruct
public void init() {
productIdFilter = redissonClient.getBloomFilter("product:id:filter");
// 预计元素数量100万,误判率0.01%
productIdFilter.tryInit(1_000_000L, 0.0001);
List<Long> allIds = productMapper.selectAllIds();
allIds.forEach(id -> productIdFilter.add(id));
}
public Product getProduct(Long id) {
if (!productIdFilter.contains(id)) {
return null; // 肯定不存在,直接返回
}
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,
CacheUtil.getRandomExpire(), TimeUnit.SECONDS
);
} else {
// 空值缓存,防止同一Key反复穿透
redisTemplate.opsForValue().set(
cacheKey, "NULL", 60, TimeUnit.SECONDS
);
}
return product;
}
}
熔断降级与Sentinel集成方案
当缓存层和数据库都承受不住压力时,需要通过熔断器快速失败保护系统不雪崩。Sentinel的慢调用比例熔断策略适合数据库保护场景:
// Sentinel熔断规则配置
@PostConstruct
public void initFlowRules() {
List<DegradeRule> rules = new ArrayList<>();
DegradeRule dbRule = new DegradeRule("dbQuery")
.setGrade(CircuitBreakerStrategy.SLOW_REQUEST_RATIO.getType())
.setCount(500)
.setSlowRatioThreshold(0.6)
.setMinRequestAmount(20)
.setStatIntervalMs(10000)
.setTimeWindow(10);
rules.add(dbRule);
DegradeRule errorRule = new DegradeRule("dbQuery")
.setGrade(CircuitBreakerStrategy.ERROR_RATIO.getType())
.setCount(0.5)
.setMinRequestAmount(10)
.setStatIntervalMs(5000)
.setTimeWindow(15);
rules.add(errorRule);
DegradeRuleManager.loadRules(rules);
}
@SentinelResource(value = "dbQuery",
fallback = "fallbackQuery",
blockHandler = "blockHandler")
public Product queryProductFromDb(Long id) {
return productMapper.selectById(id);
}
public Product fallbackQuery(Long id) {
return Product.defaultProduct();
}
public Product blockHandler(Long id, BlockException ex) {
log.warn("数据库查询被熔断, productId={}", id);
return Product.defaultProduct();
}
生产环境的多级缓存架构配置建议:Caffeine本地缓存容量10000条,TTL 5分钟;Redis集群缓存容量按业务数据量配置,TTL 30-60分钟带随机偏移;数据库查询通过Sentinel熔断保护,慢调用阈值500ms,熔断窗口10秒。压测数据显示,该架构下单节点QPS从2000提升到15000,缓存命中率95%以上,数据库QPS降低90%。当Redis集群整体宕机时,Caffeine本地缓存扛住30%的读流量,剩余70%通过熔断降级快速返回,数据库不被压垮。
原创文章,作者:小编,如若转载,请注明出处:https://www.yunthe.com/redis-huan-cun-xue-beng-yu-ji-chuan-fang-hu-shi-zhan-duo-ji/