写点什么

高效缓存的 10 条军规

  • 2025-05-22
    福建
  • 本文字数:3589 字

    阅读完需:约 12 分钟

前言


"首页崩了!"


凌晨三点接到电话时,我正梦见自己成了缓存之神。


打开监控一看:

缓存命中率:0%  数据库QPS:10万+  线程阻塞数:2000+
复制代码


根本原因竟是之前有同事写的这段代码:

public Product getProduct(Long id) {      return productDao.findById(id); }
复制代码


直连数据库,未加缓存。


这一刻我意识到:不会用缓存的程序员,就像不会刹车的赛车手


今天这篇文章跟大家一起聊聊使用缓存的 10 条军规,希望对你会有所帮助。


军规 1: 避免大 key


反例场景

@Cacheable(value = "user", key = "#id")  public User getUser(Long id) {      return userDao.findWithAllRelations(id); }
复制代码


这里一次查询出了用户及其所有关联对象,然后添加到内存缓存中。


如果通过 id 查询用户信息的请求量非常大,会导致频繁的 GC。


正确实践

@Cacheable(value = "user_base", key = "#id")  public UserBase getBaseInfo(Long id) { /*...*/ }  
@Cacheable(value = "user_detail", key = "#id") public UserDetail getDetailInfo(Long id) { /*...*/ }
复制代码


这种情况,需要拆分缓存对象,比如:将用户基本信息和用户详细信息分开缓存。

缓存不是存储数据的垃圾桶,需要根据数据访问频率、读写比例、数据一致性要求进行分级管理。

大对象缓存会导致内存碎片化,甚至触发 Full GC。

建议将基础信息(如用户 ID、名称)与扩展信息(如订单记录)分离存储。


军规 2: 永远设置过期时间


血泪案例:某系统将配置信息缓存设置为永不过期,导致修改配置后三天才生效。


正确配置

@Cacheable(value = "config", key = "#key",             unless = "#result == null",             cacheManager = "redisCacheManager")  public String getConfig(String key) {      return configDao.get(key);  }
复制代码


Redis 配置如下:

spring.cache.redis.time-to-live=300000 // 5分钟  spring.cache.redis.cache-null-values=false
复制代码


需要指定 key 的存活时间,比如:time-to-live 设置成 5 分钟。


TTL 设置公式

最优TTL = 平均数据变更周期 × 0.3
复制代码


深层思考:过期时间过短会导致缓存穿透风险,过长会导致数据不一致。


建议采用动态 TTL 策略。


例如电商商品详情页可设置 30 分钟基础 TTL+随机 5 分钟抖动。


军规 3: 避免批量失效


典型事故:所有缓存设置相同 TTL,导致每天凌晨集中失效,数据库瞬时被打爆。


解决方案

使用基础 TTL + 随机抖动的方案:

public long randomTtl(long baseTtl) {      return baseTtl + new Random().nextInt(300); }  
复制代码


TTL 增加 0-5 分钟随机值。

使用示例

redisTemplate.opsForValue().set(key, value, randomTtl(1800), TimeUnit.SECONDS);
复制代码


失效时间分布



军规 4: 需要增加熔断降级


我们在使用缓存的时候,需要增加熔断降级策略,防止万一缓存挂了,不能影响整个服务的可用性。


Hystrix 实现示例

@HystrixCommand(fallbackMethod = "getProductFallback",                 commandProperties = {                     @HystrixProperty(name = "circuitBreaker.requestVolumeThreshold", value = "20"),                     @HystrixProperty(name = "circuitBreaker.sleepWindowInMilliseconds", value = "5000")                 })  public Product getProduct(Long id) {      return productDao.findById(id);  }  
public Product getProductFallback(Long id) { return new Product().setDefault(); // 返回兜底数据 }
复制代码


熔断状态机



▶ 军规 5: 空值缓存


在用户请求并发量大的业务场景种,我们需要把空值缓存起来。

防止大批量在系统中不存在的用户 id,没有命中缓存,而直接查询数据库的情况。

典型代码

public Product getProduct(Long id) {      String key = "product:" + id;      Product product = redis.get(key);      if (product != null) {          if (product.isEmpty()) { // 空对象标识              return null;          }          return product;      }  
product = productDao.findById(id); if (product == null) { redis.setex(key, 300, "empty"); // 缓存空值5分钟 return null; }
redis.setex(key, 3600, product); return product; }
复制代码


空值缓存原理



需要将数据库中返回的空值,缓存起来。

后面如果有相同的 key 查询数据,则直接从缓存中返回空值。

而无需再查询一次数据库。


军规 6: 分布式锁用 Redisson


用 Redis 做分布式锁的时候,可能会遇到很多问题。

建议大家使用 Redisson 做分布式锁。

Redisson 分布式锁实现

public Product getProduct(Long id) {      String key = "product:" + id;      Product product = redis.get(key);      if (product == null) {          RLock lock = redisson.getLock("lock:" + key);          try {              if (lock.tryLock(3, 30, TimeUnit.SECONDS)) {                  product = productDao.findById(id);                  redis.setex(key, 3600, product);              }          } finally {              lock.unlock();          }      }      return product;  }
复制代码


锁竞争流程图



军规 7: 延迟双删策略


在保证数据库和缓存双写数据一致性的业务场景种,可以使用延迟双删的策略。


例如:

@Transactional  public void updateProduct(Product product) {      // 1. 先删缓存      redis.delete("product:" + product.getId());  
// 2. 更新数据库 productDao.update(product);
// 3. 延时再删 executor.schedule(() -> { redis.delete("product:" + product.getId()); }, 500, TimeUnit.MILLISECONDS); }
复制代码


军规 8: 最终一致性方案


延迟双删可能还有其他的问题。

我们可以使用最终一致性方案。

基于 Binlog 的方案



DB 更新数据之后,Canal 会自动监听数据的变化,它会解析数据事件,然后发送一条 MQ 消息。

在 MQ 消费者中,删除缓存。


军规 9: 热点数据预加载


对于一些经常使用的热点数据,我们可以提前做数据的预加载。

实时监控方案

// 使用Redis HyperLogLog统计访问频率  public void recordAccess(Long productId) {      String key = "access:product:" + productId;      redis.pfadd(key, UUID.randomUUID().toString());      redis.expire(key, 60); // 统计最近60秒  }  
// 定时任务检测热点 @Scheduled(fixedRate = 10000) public void detectHotKeys() { Set<String> keys = redis.keys("access:product:*"); keys.forEach(key -> { long count = redis.pfcount(key); if (count > 1000) { // 阈值 Long productId = extractId(key); preloadProduct(productId); } }); }
复制代码


定时任务检测热点,并且更新到缓存中。


军规 10: 根据场景选择数据结构


血泪案例:某社交平台使用 String 类型存储用户信息。

错误用 String 存储对象:

redis.set("user:123", JSON.toJSONString(user));  
复制代码


每次更新单个字段都需要反序列化整个对象。

导致问题:

  1. 序列化/反序列化开销大

  2. 更新单个字段需读写整个对象

  3. 内存占用高

正确实践:

// 使用Hash存储  redis.opsForHash().putAll("user:123", userToMap(user));  
// 局部更新 redis.opsForHash().put("user:123", "age", "25");
复制代码


数据结构选择矩阵:



各数据结构最佳实践:


1.String

计数器

redis.opsForValue().increment("article:123:views");  
复制代码


分布式锁

redis.opsForValue().set("lock:order:456", "1", "NX", "EX", 30);  
复制代码


2.Hash

存储商品信息

Map<String, String> productMap = new HashMap<>();  productMap.put("name", "iPhone15");  productMap.put("price", "7999");  redis.opsForHash().putAll("product:789", productMap);  
复制代码

部分更新

redis.opsForHash().put("product:789", "stock", "100");  
复制代码


3.List

消息队列

redis.opsForList().leftPush("queue:payment", orderJson);  
复制代码

最新 N 条记录

redis.opsForList().trim("user:123:logs", 0, 99); 
复制代码


4.Set

标签系统

redis.opsForSet().add("article:123:tags", "科技", "数码");  
复制代码

共同好友

redis.opsForSet().intersect("user:123:friends", "user:456:friends"); 
复制代码


5.ZSet

排行榜

redis.opsForZSet().add("leaderboard", "player1", 2500);  redis.opsForZSet().reverseRange("leaderboard", 0, 9);  
复制代码

延迟队列

redis.opsForZSet().add("delay:queue", "task1", System.currentTimeMillis() + 5000); 
复制代码


总结


缓存治理黄金法则



最后忠告:缓存是把双刃剑,用得好是性能利器,用不好就是定时炸弹。

当你准备引入缓存时,先问自己三个问题:

  1. 真的需要缓存吗?

  2. 缓存方案是否完整?

  3. 有没有兜底措施?


文章转载自:苏三说技术

原文链接:https://www.cnblogs.com/12lisu/p/18888400

体验地址:http://www.jnpfsoft.com/?from=001YH

用户头像

还未添加个人签名 2025-04-01 加入

还未添加个人简介

评论

发布
暂无评论
高效缓存的10条军规_缓存_电子尖叫食人鱼_InfoQ写作社区