Loading

Redis 缓存设计与优化实践

Redis 缓存设计与优化实践:从代码到架构的演进之路

在高并发场景下,缓存是提升系统性能的核心手段,但随之而来的缓存穿透、双写不一致、热点数据冲击等问题也不容忽视。本文结合实际业务代码,从基础缓存实现到多级缓存架构,循序渐进地讲解缓存设计的优化思路与最佳实践。

一、基础缓存设计:解决 "查多写少" 的性能瓶颈

1.1 缓存读写策略:先查缓存再查库

在商品查询场景中,我们采用 "缓存优先" 策略:

  • 查询商品时,先从缓存获取数据
  • 缓存未命中时,查询数据库并回写缓存
  • 缓存命中时直接返回,减少数据库访问
public Product get(Long productId) throws InterruptedException {
    Product product = null;
    String productCacheKey = RedisKeyPrefixConst.PRODUCT_CACHE + productId;

    // 先查缓存
    product = getProductFromCache(productCacheKey);
    if (product != null) {
        return product;
    }
    
    // 缓存未命中,查库并回写缓存(后续优化部分)
    // ...
}

1.2 缓存时效性:随机过期时间避免缓存雪崩

为避免大量缓存同时过期导致的 "缓存雪崩",我们在设置缓存时添加随机过期时间:

// 商品缓存基础超时24小时,随机增加0-5小时偏移量
private Integer genProductCacheTimeout() {
    return PRODUCT_CACHE_TIMEOUT + new Random().nextInt(5) * 60 * 60;
}

// 空对象缓存超时60-90秒,避免长期占用内存
private Integer genEmptyCacheTimeout() {
    return 60 + new Random().nextInt(30);
}

原理:通过时间偏移打散缓存过期点,防止同一时间大量请求穿透到数据库。

1.3 读延期机制:延长热点数据缓存生命周期

在缓存命中时,我们会延长缓存的过期时间(读延期):

private Product getProductFromCache(String productCacheKey) {
    // ...从缓存获取数据逻辑...
    if (!StringUtils.isEmpty(productStr)) {
        // 缓存命中时延长过期时间
        redisUtil.expire(productCacheKey, genProductCacheTimeout(), TimeUnit.SECONDS); 
    }
    return product;
}

优势

  • 热点数据会被频繁访问,通过读延期自动延长生命周期
  • expire命令比重新set值更高效(减少网络传输和内存操作)
  • 实现 "冷热分离":热点数据自动长期驻留,冷数据自然淘汰

二、缓存问题解决方案:从穿透到一致性

2.1 缓存穿透:空对象 + 布隆过滤器

缓存穿透指查询不存在的数据,导致请求直接穿透到数据库。解决方案:

  1. 缓存空对象:对不存在的商品 ID,缓存空对象并设置短期过期
// 查库后发现商品不存在
if (product == null) {
    // 缓存空对象标记,避免重复穿透
    redisUtil.set(productCacheKey, EMPTY_CACHE, genEmptyCacheTimeout(), TimeUnit.SECONDS);
}
  1. 布隆过滤器
    • 启动时将所有商品 ID 加载到布隆过滤器
    • 查询前先通过过滤器判断 ID 是否存在,不存在直接返回
    • 适合数据量固定、新增少的场景(如商品 ID)

2.2 热点缓存重建:双重检查锁 + 分布式锁

热点商品缓存过期时,可能引发大量并发请求同时查库重建缓存,导致数据库压力骤增。解决方案:

// 双重检查锁(DCL)+ 分布式锁
RLock hotCacheLock = redisson.getLock(LOCK_PRODUCT_HOT_CACHE_PREFIX + productId);
hotCacheLock.lock(); // 获取分布式锁
try {
    // 二次检查缓存,避免锁等待期间已重建缓存
    product = getProductFromCache(productCacheKey);
    if (product != null) {
        return product;
    }
    // 真正的查库并重建缓存操作
    // ...
} finally {
    hotCacheLock.unlock();
}

关键点

  • 双重检查:获取锁前后各检查一次缓存,减少无效的锁竞争
  • 分布式锁:保证集群环境下只有一个线程执行缓存重建
  • 锁超时:实际应用中可以使用tryLock(timeout)避免死锁(慎重考虑)

锁超时方案,需要结合具体业务,例如评估业务代码执行最多1s,则tryLock设置1秒自动锁失效。若是1秒业务没执行完,则可能大量请求打到数据库

2.3 双写不一致:读写锁优化读多写少场景

更新商品数据时,需要保证数据库与缓存的数据一致性。采用读写锁优化:

// 更新商品时使用写锁
RReadWriteLock readWriteLock = redisson.getReadWriteLock(LOCK_PRODUCT_UPDATE_PREFIX + product.getId());
RLock writeLock = readWriteLock.writeLock();
writeLock.lock();
try {
    // 1. 更新数据库
    productResult = productDao.update(product);
    // 2. 更新缓存
    redisUtil.set(..., JSON.toJSONString(productResult), ...);
} finally {
    writeLock.unlock();
}

// 查询商品时使用读锁
RLock rLock = readWriteLock.readLock();
rLock.lock();
try {
    // 查库操作
} finally {
    rLock.unlock();
}

读写锁优势

  • 读锁共享:多个读请求可同时获取锁,不阻塞
  • 写锁排他:写操作时阻塞所有读写请求,保证原子性
  • 适合读多写少场景(如商品详情查询远多于更新)

三、多级缓存架构:从 Redis 到本地缓存

当并发量进一步提升,单靠 Redis 可能成为瓶颈,此时引入多级缓存

3.1 JVM 本地缓存:抗住瞬时高并发

在代码中添加本地缓存(ConcurrentHashMap)作为一级缓存:

// 本地缓存容器,线程安全
public static Map<String, Product> productMap = new ConcurrentHashMap<>();

// 从缓存获取数据时,优先查本地缓存
private Product getProductFromCache(String productCacheKey) {
    // 1. 先查本地缓存
    Product product = productMap.get(productCacheKey);
    if (product != null) {
        return product;
    }
    // 2. 再查Redis缓存
    // ...
}

优势

  • 本地内存访问,性能比 Redis 高 1-2 个数量级
  • 抗住瞬时流量峰值(如秒杀开始时的突发请求)
  • 减轻 Redis 压力

3.2 本地缓存一致性:MQ 同步与取舍

分布式系统中,多节点的本地缓存需要保持一致:

// 更新商品时,同步更新本地缓存
productMap.put(RedisKeyPrefixConst.PRODUCT_CACHE + productResult.getId(), product);

// 集群环境下,通过MQ通知其他节点更新本地缓存
// 伪代码:
mqTemplate.send("product-update-topic", productResult.getId());

注意事项

  • 本地缓存容量有限,只适合存储热点数据
  • MQ 同步存在延迟,需容忍短期不一致
  • 非核心业务可接受最终一致性,避免过度设计

3.3 进一步优化:专业本地缓存方案

当业务复杂度提升,可考虑成熟的本地缓存框架:

  • Caffeine:支持 LRU/LFU 等淘汰策略,自动清理过期数据
  • JetCache:整合本地缓存与分布式缓存,支持 TTL 和自动刷新
  • 单独服务维护热点数据:通过监听 MQ 更新,解耦业务代码

四、总结:缓存设计的权衡之道

没有完美的缓存方案,只有适合业务的方案。关键权衡点:

  1. 性能与一致性
    • 强一致性:读写锁 + 分布式锁(性能损耗高)
    • 最终一致性:过期时间 + MQ 同步(性能好,适合大多数场景)
  2. 复杂度与收益
    • 简单场景:Redis 单级缓存 + 随机过期即可
    • 高并发场景:多级缓存 + 读写锁 + 布隆过滤器
  3. 监控与调优
    • 关注 Redis 慢查询日志,优化 bigkey 和 O (N) 命令
    • 监控缓存命中率,低于 80% 需排查原因
    • 定期演练缓存失效场景,验证降级策略

通过本文的代码解析与架构演进分析,相信你已掌握缓存设计的核心思路。实际开发中,需结合业务 QPS、数据一致性要求和团队技术栈,选择最合适的方案并持续优化。

posted @ 2025-09-11 19:16  流火无心  阅读(66)  评论(0)    收藏  举报