开发系统时---如何高效缓存呢?

欢喜 Java每日一问 发布时间:2025-06-13 15:09:22 阅读数:18284 1
下文笔者讲述开发系统时,高效缓存的方法及示例分享,如下所示

避免缓存大对象

例:反例

@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) { /*...*/ }
这种情况,需要拆分缓存对象,比如:将用户基本信息和用户详细信息分开缓存

永远设置过期时间

所有的缓存信息,都应设置过期时间

正确配置:
 @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

并不是所有的缓存都按照这种公式设置,
   我们应采用动态设置缓存时间,如:商品详情页,设置30分钟的缓存时间

空值缓存

在用户请求并发量大的业务场景种
  我们需要把空值缓存起来。
   防止大批量在系统中不存在的用户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;  
}

分布式锁用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;  
}

延迟双删策略

在保证数据库和缓存双写数据一致性的业务场景种,
  可使用延迟双删的策略
例
@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);  
}

热点数据预加载

对经常使用的热点数据
 我们可以提前做数据的预加载
实时监控方案:

// 使用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);  
        }  
    });  
}
定时任务检测热点,并且更新到缓存中

选择合适的数据结构

反例:
  使用String类型存储用户信息 

  错误用String存储对象:
   redis.set("user:123", JSON.toJSONString(user));  
    每次更新单个字段都需要反序列化整个对象。

导致:
  序列化/反序列化开销大
  更新单个字段需读写整个对象
  内存占用高


正确做法
 //使用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", "maomao");  
   productMap.put("price", "888");  
   redis.opsForHash().putAll("product:6666", productMap);  
  部分更新
  redis.opsForHash().put("product:6666", "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); 
版权声明

本文仅代表作者观点,不代表本站立场。
本文系作者授权发表,未经许可,不得转载。

本文链接: https://www.Java265.com/JavaProblem/202506/8494.html

最近发表

热门文章

好文推荐

Java265.com

https://www.java265.com

站长统计|粤ICP备14097017号-3

Powered By Java265.com信息维护小组

使用手机扫描二维码

关注我们看更多资讯

java爱好者