Skip to content
第 194 / 250 章中间件⏱ 12 分钟阅读

第 194 章:Redis 缓存模式与一致性

学习目标

  • 掌握 4 种缓存读写模式
  • 理解缓存一致性问题
  • 学会 Cache Aside Pattern
  • 了解 Read/Write Through、Write Behind

一、缓存的四种经典模式

模式
Cache-Aside应用控制应用控制
Read-Through缓存层控制应用控制
Write-Through缓存层控制缓存层同步
Write-Behind缓存层控制缓存层异步

二、Cache-Aside(旁路缓存,最常用)

2.1 流程

2.2 实现

java
@Service
public class UserService {

    @Autowired
    private RedisTemplate<String, User> redisTemplate;

    @Autowired
    private UserMapper userMapper;

    public User findById(Long id) {
        String key = "user:" + id;
        // 1. 查缓存
        User user = redisTemplate.opsForValue().get(key);
        if (user != null) {
            return user;
        }

        // 2. 查 DB
        user = userMapper.selectById(id);
        if (user == null) {
            return null;
        }

        // 3. 写缓存(设过期时间防永久脏)
        redisTemplate.opsForValue().set(key, user, 30, TimeUnit.MINUTES);

        return user;
    }

    public void update(User user) {
        // 1. 更新 DB
        userMapper.updateById(user);

        // 2. 删除缓存(不是更新!)
        redisTemplate.delete("user:" + user.getId());
    }

    public void delete(Long id) {
        userMapper.deleteById(id);
        redisTemplate.delete("user:" + id);
    }
}

2.3 为什么删除而不是更新

java
// ❌ 更新缓存:两个并发写会出现脏数据
// 线程 A: update DB = 1, set cache = 1
// 线程 B: update DB = 2, set cache = 2
// 如果 A 后写缓存,cache=1 但 DB=2,脏数据

// ✅ 删除缓存:下次读才回源,且能容忍并发
// 线程 A: update DB = 1, del cache
// 线程 B: update DB = 2, del cache (幂等)
// 下次读:cache miss → 从 DB=2 加载,正确

三、Read-Through

应用只跟缓存交互,缓存自己读 DB。

java
public class ReadThroughCache {
    private final Cache cache;
    private final DB db;

    public User find(Long id) {
        return cache.get(id, () -> db.findById(id));   // 缓存层自动回源
    }
}

Spring Cache 的 @Cacheable 模式类似,只是回源是直接调用方法。

四、Write-Through

写操作同步写 DB 和缓存,缓存层代理。

java
public void save(User user) {
    cache.put(user.getId(), user);    // 先写缓存
    cache.getWriter().write(user);    // 缓存层同步写 DB
}

特点:一致性高,但写慢。

五、Write-Behind(异步写)

写缓存,缓存异步批量写 DB

特点:

  • ✅ 写极快
  • ❌ 数据可能丢失(宕机)
  • 场景:点赞数、阅读量(允许少量丢失)

六、缓存一致性问题

6.1 三大来源

6.2 双写不一致(经典场景)

java
// ❌ 先写 DB 再写缓存,可能不一致
public void update(User user) {
    userMapper.updateById(user);    // DB = 2
    redisTemplate.opsForValue().set("user:" + user.getId(), user);  // 缓存 = 2
}
// 假设:DB 写完,缓存写之前,另一个读线程读到 DB=2,写入缓存=2
// 没事啊?问题在写过程中...

场景 1:写完 DB 后,还没来得及删/写缓存,进程挂了 → 永久脏数据。

场景 2:并发读写导致脏数据(删除缓存方案也可能):

T1: 写 DB = 1
T2: 删缓存
T3: 读 cache miss → 查 DB = 1 → 写 cache = 1 ✅

T1: 写 DB = 2
T2: 删缓存
T3: 读 cache miss → 查 DB = 1(还没到 T1 提交?)→ 写 cache = 1 ❌

6.3 解决方案

方案 1:延迟双删

java
public void update(User user) {
    userMapper.updateById(user);

    // 第一次删
    redisTemplate.delete("user:" + user.getId());

    // 延迟再删(异步)
    CompletableFuture.runAsync(() -> {
        try { Thread.sleep(500); } catch (InterruptedException e) {}
        redisTemplate.delete("user:" + user.getId());
    });
}

方案 2:异步监听 binlog(Canal)

java
// Canal 订阅
canal.subscribe("user", event -> {
    if (event.isUpdate() || event.isInsert()) {
        redisTemplate.delete("user:" + event.getId());
    }
});

方案 3:版本号 / 分布式锁

java
public void updateWithLock(User user) {
    String lockKey = "lock:user:" + user.getId();
    RLock lock = redissonClient.getLock(lockKey);
    try {
        lock.lock();
        userMapper.updateById(user);
        redisTemplate.delete("user:" + user.getId());
    } finally {
        lock.unlock();
    }
}

方案 4:短 TTL + 最终一致

最实用:接受秒级不一致,靠短 TTL 自动过期。

java
redisTemplate.opsForValue().set(key, value, 30, TimeUnit.SECONDS);

七、缓存粒度

7.1 全量 vs 字段

java
// 全量缓存:整个 User
User user = userMapper.selectById(id);
redisTemplate.opsForValue().set("user:" + id, user);

// 字段缓存:单独 name
String name = userMapper.selectName(id);
redisTemplate.opsForValue().set("user:" + id + ":name", name);
粒度优点缺点
全量简单浪费内存,字段更新全失效
字段灵活key 多,代码复杂

7.2 推荐:全量 + 短 TTL

大多数业务场景,简单胜过优化。

八、缓存预热

java
@Component
public class CacheWarmup implements ApplicationRunner {

    @Override
    public void run(ApplicationArguments args) {
        List<User> hotUsers = userMapper.selectHotUsers();
        hotUsers.forEach(user ->
            redisTemplate.opsForValue().set("user:" + user.getId(), user, 1, TimeUnit.HOURS)
        );
    }
}

九、本章小结

模式一致性性能复杂度
Cache-Aside最终
Read-Through最终
Write-Through
Write-Behind极高

推荐

大多数场景用 Cache-Aside + 延迟双删 + 短 TTL

动手练习

  1. 实现 Cache-Aside 模式的用户服务
  2. 用延迟双删解决双写不一致
  3. 实现启动时缓存预热
  4. 用 Lua 脚本保证删除缓存的原子性

推荐阅读


下一章:第 195 章:Redis 分布式锁

本站基于 VitePress 构建 · 由 Codebook 团队维护