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; private static final int RANDOM_RANGE = 300; 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); TTL_CONFIG.put("product", 3600); TTL_CONFIG.put("config", 86400); TTL_CONFIG.put("session", 1800); } 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; @Scheduled(fixedRate = 300000) public void refreshExpiringKeys() { Set<String> expiringKeys = findExpiringKeys(600); 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 { private Cache<String, Object> localCache = Caffeine.newBuilder() .maximumSize(10000) .expireAfterWrite(30, TimeUnit.SECONDS) .build(); @Autowired private StringRedisTemplate redis; @Autowired private ProductMapper productMapper; public Product getProduct(Long id) { String key = "product:" + id; Product product = (Product) localCache.getIfPresent(key); if (product != null) { return product; } String json = redis.opsForValue().get(key); if (json != null) { product = JSON.parseObject(json, Product.class); localCache.put(key, product); return product; } 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(); productMapper.update(product); redis.opsForValue().set(key, JSON.toJSONString(product), 3600, TimeUnit.SECONDS); localCache.invalidate(key); 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 数据库连接池保护
spring: datasource: hikari: maximum-pool-size: 50 minimum-idle: 10 connection-timeout: 3000 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 告警规则
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不宕机 |
高 |
| 预热机制 |
提前加载数据 |
低 |
| 监控告警 |
及时发现问题 |
低 |
缓存雪崩防御策略:
预防层:
保护层:
恢复层:
核心原则:
- 不要让大量key同时过期
- 建立多级缓存防线
- 异常情况保护数据库
- 持续监控及时响应
核心要点
String:简单的键值对,适合缓存、计数器
Hash:存储对象属性,适合用户信息、配置
List:有序列表,适合消息队列、最新列表
Set:无序去重,适合共同好友、抽奖
ZSet:有序集合,适合排行榜、积分系统
总结
选择合适的数据结构是使用 Redis 的关键。在实际项目中,根据业务需求选择合适的类型,可以提升性能和开发效率。