問題現象
Redis整體命中率98%,但監控發現特定Key(如user:1000:profile
)的命中率從99%驟降至40%,引發服務延遲上升。
排查步驟
1. 確認現象與定位Key
// 通過Redis監控工具獲取Key指標
public void monitorKey(String key) {Jedis jedis = new Jedis("localhost");// 使用Redis命令分析Key訪問模式System.out.println("Key訪問統計: " + jedis.objectEncoding(key));System.out.println("Key剩余TTL: " + jedis.ttl(key) + "秒");
}
- 使用
redis-cli --hotkeys
或monitor
命令確認Key訪問頻率 - 檢查監控系統(如Grafana)觀察命中率下降時間點
2. 檢查業務變更
// 檢查新上線代碼:緩存讀寫邏輯是否變化
@Service
public class UserService {// 變更前代碼:正常緩存讀取@Cacheable(value = "userProfile", key = "#userId")public User getProfile(Long userId) { /* 查數據庫 */ }// 變更后問題代碼:錯誤覆蓋了緩存Keypublic void updateProfile(Long userId) {userDao.update(userId);// 錯誤:未清除舊緩存,直接寫入新KeyredisTemplate.opsForValue().set("user_profile_" + userId, newData); }
}
- 排查點:
- 是否新增繞過緩存的直接DB查詢?
- Key生成規則是否改變(如
user:{id}
→user_profile_{id}
)? - 緩存清理邏輯是否遺漏(如@CacheEvict注解缺失)
3. 分析緩存失效策略
// 檢查TTL設置:確認是否設置過短
@Configuration
public class RedisConfig {@Beanpublic RedisCacheManager cacheManager(RedisConnectionFactory factory) {RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig()// 問題點:全局設置10分鐘TTL.entryTtl(Duration.ofMinutes(10)); return RedisCacheManager.builder(factory).cacheDefaults(config).build();}
}
- 關鍵問題:
- TTL設置不合理:熱點Key過期時間過短
- 批量失效:定時任務導致關聯Key集中清除
- 內存淘汰策略:檢查
maxmemory-policy
是否主動刪除Key
4. 驗證數據一致性
// 典型緩存不一致場景:先更庫后刪緩存失敗
public void updateUser(Long userId) {// 步驟1:更新數據庫userDao.update(userId);// 步驟2:刪除緩存(可能失敗)try {redisTemplate.delete("user:" + userId);} catch (Exception e) {// 未處理異常導致緩存未刪除!logger.error("緩存刪除失敗", e);}
}
- 一致性陷阱:
- 緩存穿透:惡意請求不存在的Key(如
user:-1
) - 緩存擊穿:熱點Key失效瞬間大量請求穿透
- 更新順序:DB更新成功但緩存刪除失敗
- 緩存穿透:惡意請求不存在的Key(如
針對性解決方案
方案1:緩存穿透 → 空值緩存
public User getProfile(Long userId) {String key = "user:" + userId;User user = redisTemplate.opsForValue().get(key);if (user == null) {user = userDao.findById(userId);// 緩存空值防止穿透redisTemplate.opsForValue().set(key, user != null ? user : "NULL", 5, TimeUnit.MINUTES);}return "NULL".equals(user) ? null : user;
}
方案2:緩存擊穿 → 互斥鎖
public User getProfileWithLock(Long userId) {String key = "user:" + userId;User user = redisTemplate.opsForValue().get(key);if (user == null) {String lockKey = "lock:" + key;if (redisTemplate.opsForValue().setIfAbsent(lockKey, "1", 30, TimeUnit.SECONDS)) {try {user = userDao.findById(userId); // 查DBredisTemplate.opsForValue().set(key, user, 1, TimeUnit.HOURS);} finally {redisTemplate.delete(lockKey); // 釋放鎖}} else {// 其他線程等待重試Thread.sleep(50);return getProfileWithLock(userId);}}return user;
}
方案3:一致性保障 → 雙刪策略
public void updateUser(Long userId) {// 1. 先刪緩存redisTemplate.delete("user:" + userId); // 2. 更新數據庫userDao.update(userId); // 3. 延時二次刪除(應對主從延遲)executor.schedule(() -> {redisTemplate.delete("user:" + userId);}, 1, TimeUnit.SECONDS);
}
預防措施
- 監控預警:
- 對核心Key設置命中率閾值告警(如<90%觸發)
- 日志記錄緩存刪除失敗操作
- 架構優化:
- 使用
Redisson
實現分布式鎖 - 采用
Caffeine
實現本地二級緩存
- 使用
- 策略配置:
# Redis配置調整 config set maxmemory-policy allkeys-lru # 內存不足時LRU淘汰 config set notify-keyspace-events Ex # 訂閱Key過期事件
經驗總結:80%的命中率下降源于業務變更和失效策略不當。通過代碼審查 + 實時監控 + 防御性編程,可快速定位并解決此類問題。