三亩地 三亩地SAN MU DI · CODE DIARY
ARTICLE DETAIL

日记详情

真实记录编程学习的某一天,欢迎挑你感兴趣的翻一翻。

Redis缓存三大问题:穿透、雪崩、击穿解决方案

Redis缓存三大问题:穿透、雪崩、击穿解决方案

1. Redis缓存三大致命问题深度剖析

缓存系统作为现代高并发架构的核心组件,Redis在其中扮演着至关重要的角色。但在实际生产环境中,我们经常会遇到三种典型的缓存异常场景:缓存穿透、缓存雪崩和缓存击穿。这些问题如果处理不当,轻则导致系统响应变慢,重则引发服务完全不可用。

我在电商大促保障和金融支付系统架构中,曾多次亲历这些缓存问题引发的生产事故。比如某次秒杀活动中,由于未做好缓存击穿防护,导致数据库连接池被瞬间打满,整个订单系统瘫痪近20分钟。这些血泪教训让我深刻认识到,只有深入理解这些问题的本质,才能构建真正健壮的缓存体系。

2. 缓存穿透:当查询必然不存在的数据

2.1 问题本质与危害分析

缓存穿透是指查询一个必然不存在的数据,由于缓存中查不到,每次请求都会落到数据库上。这种场景下,缓存完全失去了保护作用,恶意攻击者可以利用这点发起大量请求,直接压垮数据库。

典型场景包括:

  • 恶意攻击:故意构造不存在的ID发起请求
  • 业务缺陷:前端未对参数做校验,传递非法ID
  • 数据淘汰:已删除数据的ID被重复请求

我曾处理过一个典型案例:某社交平台用户主页接口未对用户ID做存在性校验,攻击者用脚本轮询遍历用户ID空间,导致数据库CPU持续100%,正常用户无法访问。

2.2 解决方案全景图

2.2.1 布隆过滤器实现

布隆过滤器是解决穿透问题的银弹。它的核心原理是:

  1. 使用一个bit数组和多个哈希函数
  2. 写入时,对key进行多次哈希,将对应位置1
  3. 查询时,所有哈希位都为1才认为可能存在

Java实现示例:

// 初始化布隆过滤器 BloomFilter<String> bloomFilter = BloomFilter.create( Funnels.stringFunnel(Charset.defaultCharset()), 1000000, // 预期元素数量 0.01 // 误判率 ); // 数据预热时加载合法key for (String validKey : validKeys) { bloomFilter.put(validKey); } // 查询前校验 if (!bloomFilter.mightContain(key)) { return null; // 直接拦截非法请求 }

关键经验:布隆过滤器需要提前预热数据,对于动态新增的数据,需要实现双写机制。误判率设置需要权衡内存和性能,通常0.01是比较平衡的选择。

2.2.2 缓存空对象策略

对于可能频繁查询的不存在数据,可以缓存空对象:

SET user:999999 "NULL" EX 300 # 缓存空值并设置过期时间

注意事项:

  • 空对象过期时间不宜过长(建议5-10分钟)
  • 需要定期清理积累的空对象
  • 可能被攻击者利用耗尽内存,需配合限流
2.2.3 接口层防护措施
  1. 参数校验:对ID格式、范围做严格校验
if (!StringUtils.isNumeric(id) || id.length() > 10) { throw new IllegalArgumentException("Invalid ID"); }
  1. 用户行为分析:识别异常请求模式
  • 同一IP高频访问不存在的key
  • 连续的数字ID遍历请求
  • 非常规时间段的集中访问
  1. 限流措施:
  • Guava RateLimiter实现单机限流
  • Redis+Lua实现分布式限流

3. 缓存雪崩:当大量缓存同时失效

3.1 问题现象与影响范围

缓存雪崩是指大量缓存key在同一时间点失效,导致所有请求直接打到数据库,引发连锁反应。我曾亲历一次典型的雪崩事故:

某电商平台在凌晨定时刷新商品缓存,由于所有商品设置了相同的TTL,2万多个商品缓存同时失效,数据库QPS瞬间从200飙升到1.2万,导致整个集群过载。

3.2 系统化解决方案

3.2.1 过期时间随机化

核心策略:在基础过期时间上增加随机值

// 原始过期时间 + 随机偏移量(0-30分钟) int expireTime = 3600 + ThreadLocalRandom.current().nextInt(0, 1800); redisTemplate.opsForValue().set(key, value, expireTime, TimeUnit.SECONDS);
3.2.2 多级缓存架构

构建本地缓存+分布式缓存的多级体系:

请求 → 本地Caffeine缓存 → Redis集群 → 数据库

本地缓存配置示例:

Caffeine.newBuilder() .maximumSize(10_000) .expireAfterWrite(5, TimeUnit.MINUTES) .build();
3.2.3 缓存预热与更新策略
  1. 冷启动预热:
@PostConstruct public void init() { List<HotItem> hotItems = loadHotItemsFromDB(); hotItems.forEach(item -> redisTemplate.opsForValue().set( "item:" + item.getId(), item, 30 + ThreadLocalRandom.current().nextInt(0, 60), TimeUnit.MINUTES ) ); }
  1. 异步刷新:
@Scheduled(fixedRate = 10_000) public void refreshCache() { // 分批更新缓存,避免集中失效 }
3.2.4 熔断降级机制

配置Hystrix熔断策略:

@HystrixCommand( fallbackMethod = "getProductFallback", commandProperties = { @HystrixProperty(name = "circuitBreaker.requestVolumeThreshold", value = "20"), @HystrixProperty(name = "circuitBreaker.sleepWindowInMilliseconds", value = "5000") } ) public Product getProduct(String id) { // 正常业务逻辑 }

4. 缓存击穿:热点key失效引发的风暴

4.1 问题特征分析

缓存击穿是指某个热点key突然失效,此时有大量并发请求这个key,所有请求都落到数据库上。与雪崩的区别在于:

  • 击穿是单个热点key
  • 雪崩是大面积key同时失效

典型场景:

  • 顶流明星突然发布微博,用户主页缓存失效
  • 秒杀商品开售瞬间
  • 重大新闻事件爆发时相关内容缓存失效

4.2 高并发解决方案

4.2.1 互斥锁实现

Redis分布式锁实现方案:

public String getData(String key) throws InterruptedException { // 1. 尝试从缓存获取 String value = redis.get(key); if (value != null) { return value; } // 2. 获取分布式锁 String lockKey = "lock:" + key; boolean locked = redis.setnx(lockKey, "1", 10, TimeUnit.SECONDS); if (locked) { try { // 3. 再次检查缓存(双重检查) value = redis.get(key); if (value != null) { return value; } // 4. 查询数据库 value = db.query(key); redis.setex(key, 3600, value); return value; } finally { redis.del(lockKey); } } else { // 5. 未获取到锁,短暂等待后重试 Thread.sleep(100); return getData(key); } }

关键细节:锁的过期时间要大于业务处理时间但不宜过长,通常5-10秒;必须实现锁的释放;考虑锁续期机制。

4.2.2 逻辑过期方案

在value中存储实际过期时间:

{ "data": "...", "expire": 1672531200 }

查询逻辑:

String json = redis.get(key); if (json != null) { CacheItem item = JSON.parseObject(json, CacheItem.class); if (item.getExpire() > System.currentTimeMillis()/1000) { return item.getData(); } else { // 异步重建缓存 threadPool.execute(() -> rebuildCache(key)); return item.getData(); // 返回旧数据 } }
4.2.3 热点key发现与特殊处理
  1. 监控识别热点key:
  • Redis的hotkeys参数
  • 监控QPS异常波动
  • 业务预测(如活动预告)
  1. 热点key特殊处理:
// 对已知热点key设置永不过期 redis.set("hot:item:123", value); // 通过后台任务定期更新 @Scheduled(fixedRate = 30_000) public void refreshHotItems() { // 更新热点key }

5. 生产环境综合防护策略

5.1 监控告警体系建设

  1. 关键监控指标:
  • 缓存命中率(低于80%告警)
  • Redis CPU/Memory使用率
  • 数据库QPS突增
  • 慢查询数量
  1. 告警规则配置示例(PromQL):
# 缓存命中率下降 100 - (redis_keyspace_hits_total / (redis_keyspace_hits_total + redis_keyspace_misses_total)) * 100 > 20 # Redis内存使用率 redis_memory_used_bytes / redis_memory_max_bytes * 100 > 85

5.2 压测与演练方案

  1. 模拟穿透攻击:
# 使用wrk模拟恶意请求 wrk -t10 -c1000 -d60s --script=malicious.lua http://api.example.com
  1. 雪崩场景测试:
// 批量删除缓存键 @SpringBootTest class CacheSnowflakeTest { @Test void testCacheFailure() { List<String> keys = redisTemplate.keys("product:*"); redisTemplate.delete(keys); // 模拟批量失效 // 观察系统表现... } }

5.3 架构设计最佳实践

  1. 多AZ部署:Redis Cluster跨可用区部署
  2. 读写分离:读多写少场景使用副本节点
  3. 连接池优化:合理设置最大连接数
JedisPoolConfig config = new JedisPoolConfig(); config.setMaxTotal(200); // 根据实际负载调整 config.setMaxIdle(50); config.setMinIdle(10);
  1. 缓存分层设计:
  • L1:本地缓存(Caffeine/Guava)
  • L2:分布式缓存(Redis Cluster)
  • L3:持久化存储(数据库)

6. 典型业务场景解决方案

6.1 电商秒杀系统防护

秒杀系统三件套:

  1. 库存预热:
SET stock:sku_1001 500 EX 3600
  1. 分段锁优化:
public boolean deductStock(String sku, int num) { int segment = ThreadLocalRandom.current().nextInt(0, 16); String lockKey = "lock:" + sku + ":" + segment; try { // 获取分段锁... // 扣减对应分段库存 } finally { // 释放锁 } }
  1. 限流措施:
RateLimiter rateLimiter = RateLimiter.create(1000); // 每秒1000个许可 if (!rateLimiter.tryAcquire()) { throw new BusinessException("活动太火爆,请稍后再试"); }

6.2 社交平台热点Feed流

  1. 多级缓存策略:
  • 个人缓存:用户最近访问的100条内容
  • 热点缓存:全站最热1000条内容
  • 长尾内容:按需查询
  1. 缓存更新策略:
@Async public void updateFeedCache(String userId, String postId) { // 异步更新用户时间线 redisTemplate.opsForList().leftPush("feed:" + userId, postId); redisTemplate.opsForList().trim("feed:" + userId, 0, 99); }

6.3 金融账户系统保障

  1. 强一致性方案:
@Transactional public void transfer(String from, String to, BigDecimal amount) { // 1. 数据库操作 accountMapper.deduct(from, amount); accountMapper.add(to, amount); // 2. 删除缓存 redisTemplate.delete("account:" + from); redisTemplate.delete("account:" + to); }
  1. 资金账户缓存设计:
public Account getAccount(String accountNo) { Account account = redisTemplate.opsForValue().get("account:" + accountNo); if (account == null) { synchronized (this) { account = redisTemplate.opsForValue().get("account:" + accountNo); if (account == null) { account = accountMapper.selectByNo(accountNo); redisTemplate.opsForValue().set( "account:" + accountNo, account, 5 + ThreadLocalRandom.current().nextInt(0, 10), TimeUnit.MINUTES ); } } } return account; }

在金融级系统中,我们通常会采用更保守的缓存策略,比如:

  • 资金账户信息缓存时间不超过1分钟
  • 所有写操作同步失效相关缓存
  • 关键操作走数据库主库
  • 实现缓存与数据库的最终一致性方案
← 返回列表