多级缓存,本地缓存和Redis缓存怎么保证数据一致性?

在多级缓存架构中(如 本地缓存 + Redis),保证数据一致性是核心挑战。以下是常见方案及实现细节,结合 缓存更新策略失效机制分布式协调 来确保一致性:


1. 多级缓存典型架构

┌─────────────┐    ┌─────────────┐    ┌─────────────┐
│  本地缓存   │ ←→ │    Redis    │ ←→ │    数据库    │
└─────────────┘    └─────────────┘    └─────────────┘
   (Caffeine)        (分布式缓存)        (MySQL/MongoDB)

2. 数据一致性解决方案

方案 1:缓存删除(Cache-Aside + 双删)
  • 核心逻辑

    • 写操作:先更新数据库,再删除 Redis 和本地缓存(严格顺序)。
    • 读操作:先读本地缓存 → 未命中读 Redis → 仍未命中读数据库,并回填缓存。
  • 关键代码(伪代码):

    // 写操作示例
    public void updateData(Data data) {
        // 1. 更新数据库
        db.update(data);
        // 2. 删除 Redis 缓存
        redis.delete(data.id);
        // 3. 延迟双删(应对并发场景)
        asyncDelay(() -> {
            redis.delete(data.id);
            localCache.delete(data.id);
        }, 500ms);
    }
    
    // 读操作示例
    public Data getData(String id) {
        // 1. 读本地缓存
        Data data = localCache.get(id);
        if (data != null) return data;
        
        // 2. 读 Redis
        data = redis.get(id);
        if (data != null) {
            localCache.put(id, data); // 回填本地缓存
            return data;
        }
        
        // 3. 读数据库
        data = db.query(id);
        if (data != null) {
            redis.set(id, data);     // 回填 Redis
            localCache.put(id, data); // 回填本地缓存
        }
        return data;
    }
    
  • 适用场景

    • 对一致性要求较高,但可容忍短暂不一致(如电商商品详情页)。
    • 延迟双删可减少并发更新导致的脏数据概率。

方案 2:发布/订阅通知(Pub/Sub)
  • 核心逻辑

    • 使用 Redis 的 Pub/Sub消息队列(如 Kafka)通知所有节点失效本地缓存。
    • 写操作后发布消息,各节点订阅消息并删除本地缓存。
  • 实现步骤

    1. 写数据库后,发布缓存失效事件(如频道 cache_invalidate)。
    2. 各服务节点订阅该频道,收到消息后删除本地缓存对应数据。
  • 示例代码

    // 写操作发布消息
    public void updateData(Data data) {
        db.update(data);
        redis.delete(data.id);
        redis.publish("cache_invalidate", data.id); // 发布失效事件
    }
    
    // 订阅端(每个服务节点)
    public void initCacheListener() {
        redis.subscribe("cache_invalidate", (channel, message) -> {
            localCache.delete(message); // 收到消息后删除本地缓存
        });
    }
    
  • 优点:实时性较高,适合分布式环境。

  • 缺点:需维护消息通道,可能因网络问题丢失消息(需配合重试机制)。


方案 3:本地缓存短过期时间 + 被动刷新
  • 核心逻辑

    • 本地缓存设置较短的 TTL(如 30 秒),依赖 Redis 作为权威数据源。
    • 通过短过期时间容忍不一致,到期后自动重新从 Redis 加载。
  • 配置示例(Caffeine):

    Caffeine.newBuilder()
        .expireAfterWrite(30, TimeUnit.SECONDS) // 短过期时间
        .build();
    
  • 适用场景

    • 对一致性要求不高,但需要极高读取性能(如热点数据缓存)。
    • 适合数据变更不频繁的场景。

方案 4:版本号或时间戳控制
  • 核心逻辑

    • 缓存数据携带版本号或更新时间戳。
    • 读数据时对比本地缓存和 Redis 的版本,若 Redis 版本更新则覆盖本地缓存。
  • 实现示例

    public Data getData(String id) {
        LocalCacheEntry localEntry = localCache.get(id);
        RedisCacheEntry redisEntry = redis.get(id);
        
        if (localEntry == null || redisEntry.version > localEntry.version) {
            localCache.put(id, redisEntry); // 更新本地缓存
            return redisEntry.data;
        }
        return localEntry.data;
    }
    
  • 优点:精确控制一致性,适合版本化数据。

  • 缺点:需维护版本号,增加存储开销。


3. 一致性保障的补充措施

  1. 读写锁(Read/Write Lock)
    • 写操作时加分布式锁(如 Redis 的 SETNX),防止并发写导致缓存与数据库不一致。
  2. 定时任务兜底
    • 定期扫描数据库与 Redis 差异,修复不一致数据(最终一致性)。
  3. 本地缓存自动刷新
    • 使用 Caffeine 的 refreshAfterWrite 定期异步刷新缓存。

4. 方案选型建议

场景 推荐方案 一致性等级
高并发读,允许短暂不一致 本地缓存短TTL + 被动刷新 最终一致性
读写均衡,强一致性要求 双删策略 + 发布/订阅 近实时一致性
分布式环境,多节点协同 Redis Pub/Sub 通知失效 近实时一致性
版本化数据(如配置信息) 版本号/时间戳对比 强一致性

5. 经典问题与解决方案

  • 问题:脏读(先更新数据库,但缓存删除失败)
    解决:引入重试机制或异步删除队列。
  • 问题:缓存击穿(大量请求同时失效)
    解决:使用互斥锁(如 Redis SETNX)或缓存预热。

总结

本地缓存 + Redis 的多级缓存中,保证一致性的核心是:

  1. 写操作:优先更新数据库,再失效或更新多级缓存(严格顺序)。
  2. 读操作:通过版本控制、发布/订阅或短TTL减少不一致时间窗口。
  3. 容错机制:兜底策略(如定时校对、重试)确保最终一致性。

根据业务场景选择合适方案,平衡 性能一致性实现复杂度

你可能感兴趣的:(八股文汇总,Redis,缓存,redis,数据库,面试)