Redis缓存雪崩与预防

Redis缓存雪崩与预防

Redis 的五种数据结构各有特色,用对了才能发挥它的优势。很多人只用到了 String 和 Hash,却不知道 List、Set、ZSet 在特定场景下更合适。本文从应用场景出发,讲什么时候用什么类型。

一、缓存雪崩场景

#

1.1 同时失效场景

场景:大量key设置了相同的过期时间

09:00:00 key1过期
09:00:00 key2过期
09:00:00 key3过期
... ...
09:00:00 key10000过期

瞬间:
请求1 → key1未命中 → 查数据库
请求2 → key2未命中 → 查数据库
...
请求10000 → key10000未命中 → 查数据库

数据库QPS瞬间从1000飙升到10000+,数据库宕机!

#

1.2 Redis宕机场景

场景:Redis集群故障或重启

Redis宕机

├── 所有缓存失效

└── 所有请求打到数据库
└── 数据库瞬间被压垮

#

1.3 与缓存击穿、穿透的区别

问题 影响范围 原因
缓存穿透 单个key 查询不存在的数据
缓存击穿 单个key 热点key过期
缓存雪崩 大量key 大量key同时过期或Redis宕机

二、过期时间打散

#

2.1 随机过期时间

@Service
public class ProductService {

@Autowired
private StringRedisTemplate redis;
@Autowired
private ProductMapper productMapper;

private static final int BASE_TTL = 3600; // 基础过期时间1小时
private static final int RANDOM_RANGE = 300; // 随机范围5分钟

public Product getProduct(Long id) {
String key = "product:" + id;
String json = redis.opsForValue().get(key);

if (json != null) {
return JSON.parseObject(json, Product.class);
}

Product product = productMapper.findById(id);
if (product != null) {
// 基础时间 + 随机偏移
int ttl = BASE_TTL + ThreadLocalRandom.current().nextInt(RANDOM_RANGE);
redis.opsForValue().set(key, JSON.toJSONString(product), ttl, TimeUnit.SECONDS);
}

return product;
}
}

#

2.2 按业务维度设置不同过期时间

@Service
public class CacheService {

// 不同业务使用不同的过期时间
private static final Map<String, Integer> TTL_CONFIG = new HashMap<>();

static {
TTL_CONFIG.put("user", 7200); // 用户数据2小时
TTL_CONFIG.put("product", 3600); // 商品数据1小时
TTL_CONFIG.put("config", 86400); // 配置数据24小时
TTL_CONFIG.put("session", 1800); // Session 30分钟
}

public void setWithBusinessTtl(String business, String key, Object value) {
int baseTtl = TTL_CONFIG.getOrDefault(business, 3600);
int randomOffset = ThreadLocalRandom.current().nextInt(300);
redis.opsForValue().set(key, JSON.toJSONString(value),
baseTtl + randomOffset, TimeUnit.SECONDS);
}
}

#

2.3 定时刷新避免集中过期

@Component
public class CacheRefreshScheduler {

@Autowired
private StringRedisTemplate redis;
@Autowired
private ProductMapper productMapper;

// 每隔一段时间刷新一批即将过期的key
@Scheduled(fixedRate = 300000) // 每5分钟
public void refreshExpiringKeys() {
// 扫描即将过期的key(通过额外记录过期时间)
Set<String> expiringKeys = findExpiringKeys(600); // 10分钟内过期

for (String key : expiringKeys) {
Long id = extractId(key);
Product product = productMapper.findById(id);
if (product != null) {
int ttl = 3600 + ThreadLocalRandom.current().nextInt(300);
redis.opsForValue().set(key, JSON.toJSONString(product), ttl, TimeUnit.SECONDS);
}
}
}
}

三、多级缓存

#

3.1 本地缓存 + Redis + 数据库

@Component
public class MultiLevelCache {

// L1: 本地缓存(Caffeine)
private Cache<String, Object> localCache = Caffeine.newBuilder()
.maximumSize(10000)
.expireAfterWrite(30, TimeUnit.SECONDS)
.build();

@Autowired
private StringRedisTemplate redis; // L2: Redis

@Autowired
private ProductMapper productMapper; // L3: Database

public Product getProduct(Long id) {
String key = "product:" + id;

// L1: 本地缓存
Product product = (Product) localCache.getIfPresent(key);
if (product != null) {
return product;
}

// L2: Redis
String json = redis.opsForValue().get(key);
if (json != null) {
product = JSON.parseObject(json, Product.class);
localCache.put(key, product);
return product;
}

// L3: 数据库
product = productMapper.findById(id);
if (product != null) {
redis.opsForValue().set(key, JSON.toJSONString(product), 3600, TimeUnit.SECONDS);
localCache.put(key, product);
}

return product;
}
}

#

3.2 缓存更新策略

@Component
public class CacheUpdateService {

@Autowired
private Cache<String, Object> localCache;
@Autowired
private StringRedisTemplate redis;

// 更新数据时同步更新多级缓存
public void updateProduct(Product product) {
String key = "product:" + product.getId();

// 1. 更新数据库
productMapper.update(product);

// 2. 更新Redis
redis.opsForValue().set(key, JSON.toJSONString(product), 3600, TimeUnit.SECONDS);

// 3. 删除本地缓存(或更新)
localCache.invalidate(key);

// 4. 发送消息通知其他节点删除本地缓存(分布式环境)
redis.convertAndSend("cache:invalidate", key);
}

// 监听缓存失效消息
@RedisListener(channel = "cache:invalidate")
public void onInvalidate(String key) {
localCache.invalidate(key);
}
}

四、熔断降级

#

4.1 Sentinel熔断

@Service
public class ProductService {

@SentinelResource(
value = "getProduct",
fallback = "getProductFallback",
blockHandler = "getProductBlockHandler"
)
public Product getProduct(Long id) {
// 查询逻辑
return productCache.get(id);
}

// 熔断降级方法
public Product getProductFallback(Long id, Throwable ex) {
// 返回兜底数据(如静态数据、默认值)
return getDefaultProduct(id);
}

// 限流处理方法
public Product getProductBlockHandler(Long id, BlockException ex) {
// 返回缓存数据或默认值
return getCacheProduct(id);
}
}

#

4.2 Hystrix熔断

@Service
public class ProductService {

@HystrixCommand(
fallbackMethod = "getProductFallback",
commandProperties = {
@HystrixProperty(name = "circuitBreaker.requestVolumeThreshold", value = "20"),
@HystrixProperty(name = "circuitBreaker.sleepWindowInMilliseconds", value = "5000"),
@HystrixProperty(name = "circuitBreaker.errorThresholdPercentage", value = "50")
}
)
public Product getProduct(Long id) {
return productCache.get(id);
}

public Product getProductFallback(Long id) {
return getDefaultProduct(id);
}
}

五、高可用架构

#

5.1 Redis高可用

Redis Sentinel架构:

┌─────────┐
│Sentinel │
└────┬────┘

┌────┴────┐
▼ ▼
┌─────────┐ ┌─────────┐
│ Master │ │ Slave │
└─────────┘ └─────────┘

Redis Cluster架构:

┌─────────┐ ┌─────────┐ ┌─────────┐
│ Master0 │ │ Master1 │ │ Master2 │
│Slave0 │ │Slave1 │ │Slave2 │
└─────────┘ └─────────┘ └─────────┘

#

5.2 数据库连接池保护

# HikariCP配置
spring:
datasource:
hikari:
maximum-pool-size: 50 # 最大连接数
minimum-idle: 10 # 最小空闲连接
connection-timeout: 3000 # 连接超时3秒
max-lifetime: 1800000 # 最大生命周期
leak-detection-threshold: 60000 # 泄漏检测

#

5.3 数据库读写分离

@Configuration
public class DataSourceConfig {

@Bean
public DataSource routingDataSource() {
DynamicRoutingDataSource routingDataSource = new DynamicRoutingDataSource();

Map<Object, Object> targetDataSources = new HashMap<>();
targetDataSources.put("master", masterDataSource());
targetDataSources.put("slave", slaveDataSource());

routingDataSource.setTargetDataSources(targetDataSources);
routingDataSource.setDefaultTargetDataSource(masterDataSource());

return routingDataSource;
}
}

六、预热机制

#

6.1 系统启动预热

@Component
public class CachePreheatRunner implements ApplicationRunner {

@Autowired
private ProductMapper productMapper;
@Autowired
private StringRedisTemplate redis;

@Override
public void run(ApplicationArguments args) {
// 启动时预热热点数据
List<Long> hotProductIds = Arrays.asList(1L, 2L, 3L, 4L, 5L);

for (Long id : hotProductIds) {
Product product = productMapper.findById(id);
if (product != null) {
int ttl = 3600 + ThreadLocalRandom.current().nextInt(300);
redis.opsForValue().set("product:" + id,
JSON.toJSONString(product), ttl, TimeUnit.SECONDS);
}
}

System.out.println("Cache preheat completed");
}
}

#

6.2 定时预热

@Component
public class ScheduledPreheat {

@Autowired
private ProductMapper productMapper;
@Autowired
private StringRedisTemplate redis;

// 每天凌晨预热
@Scheduled(cron = "0 0 3 * * ?")
public void dailyPreheat() {
List<Product> hotProducts = productMapper.findHotProducts(100);

for (Product product : hotProducts) {
int ttl = 7200 + ThreadLocalRandom.current().nextInt(600);
redis.opsForValue().set("product:" + product.getId(),
JSON.toJSONString(product), ttl, TimeUnit.SECONDS);
}
}
}

七、监控告警

#

7.1 Redis监控

# 监控缓存命中率
redis-cli INFO stats | grep keyspace

# 监控连接数
redis-cli INFO clients | grep connected_clients

# 监控内存使用
redis-cli INFO memory | grep used_memory_human

#

7.2 应用监控

@Component
public class CacheMonitor {

@Autowired
private MeterRegistry meterRegistry;

public void recordCacheMiss(String cacheName) {
meterRegistry.counter("cache.miss", "name", cacheName).increment();
}

public void recordCacheHit(String cacheName) {
meterRegistry.counter("cache.hit", "name", cacheName).increment();
}

public void recordDbQuery(String queryName) {
meterRegistry.counter("db.query", "name", queryName).increment();
}
}

#

7.3 告警规则

# Prometheus告警规则
groups:
- name: cache
rules:
- alert: CacheHitRateLow
expr: rate(cache_miss[5m]) / (rate(cache_hit[5m]) + rate(cache_miss[5m])) > 0.5
for: 5m
labels:
severity: warning
annotations:
summary: "缓存命中率低于50%"

- alert: DatabaseQuerySpike
expr: rate(db_query[1m]) > 10000
for: 1m
labels:
severity: critical
annotations:
summary: "数据库查询量激增"

八、总结

方案 作用 实施成本
过期时间打散 避免同时失效
多级缓存 增加缓存层次
熔断降级 保护数据库
高可用架构 Redis不宕机
预热机制 提前加载数据
监控告警 及时发现问题

缓存雪崩防御策略:

  1. 预防层

    • 过期时间打散
    • 定时预热
    • 多级缓存
  2. 保护层

    • 熔断降级
    • 限流
    • 数据库连接池保护
  3. 恢复层

    • 高可用架构
    • 快速重启
    • 监控告警

核心原则:

  • 不要让大量key同时过期
  • 建立多级缓存防线
  • 异常情况保护数据库
  • 持续监控及时响应

核心要点

  1. String:简单的键值对,适合缓存、计数器

  2. Hash:存储对象属性,适合用户信息、配置

  3. List:有序列表,适合消息队列、最新列表

  4. Set:无序去重,适合共同好友、抽奖

  5. ZSet:有序集合,适合排行榜、积分系统

总结

选择合适的数据结构是使用 Redis 的关键。在实际项目中,根据业务需求选择合适的类型,可以提升性能和开发效率。


   转载规则


《Redis缓存雪崩与预防》 小乐 采用 知识共享署名 4.0 国际许可协议 进行许可。
  目录