缓存的双写一致性实现方案

对于实时性要求不高 的数据,在 Redis 作为缓存的情况下,保证 Redis 和数据库数据的一致性,可以选择缓存更新策略,常见的方案包括:

1. Cache Aside(旁路缓存,推荐)

适用场景:数据读取多、实时性要求不高的场景(比如商品详情、用户信息等)。

工作流程

  1. 查询数据
    • 先查 Redis,如果缓存命中,直接返回。
    • 如果缓存未命中(Cache Miss),则查询数据库。
  2. 更新缓存
    • 将数据库查询结果写入 Redis,并设置过期时间(TTL)。
  3. 修改数据
    • 先更新数据库,再删除 Redis 缓存(而不是直接更新 Redis)。
  4. 下次查询
    • 由于缓存被删除,下一次请求会触发数据库查询,并重新写入缓存。

为什么删除缓存而不是更新缓存?

  • 数据库和 Redis 更新非原子 ,可能导致缓存和数据库数据不一致
  • 延迟双删策略
    • 第一次删除缓存(更新数据库后)。
    • 短暂延迟(如 500ms)后,再次删除缓存,确保高并发场景下缓存不会被旧值回写。

示例代码

复制代码
// 读取数据
public String getDataFromCache(Long id) {
    String key = "data:" + id;
    String value = redisTemplate.opsForValue().get(key);
    
    if (value != null) {
        return value; // 直接返回缓存数据
    }
    
    // 缓存未命中,查询数据库
    value = database.queryById(id);
    
    // 写入 Redis,并设置过期时间
    redisTemplate.opsForValue().set(key, value, Duration.ofMinutes(10));
    
    return value;
}

// 更新数据
public void updateData(Long id, String newValue) {
    // 先更新数据库
    database.updateById(id, newValue);
    
    // 删除 Redis 缓存
    redisTemplate.delete("data:" + id);

    // 延迟 500ms 再次删除,防止并发问题
    new Timer().schedule(new TimerTask() {
        @Override
        public void run() {
            redisTemplate.delete("data:" + id);
        }
    }, 500);
}

优点

✅ 适用于读多写少 的业务场景,能有效减少数据库压力。

✅ 避免缓存和数据库数据不一致的问题(先删缓存)。

✅ 结合延迟双删策略,在高并发情况下也能减少不一致。


2. Write Through(写穿透)

适用场景读写比例均衡,对一致性要求高的场景。

工作流程

  1. 写入数据时
    • 先写入数据库,然后同步更新 Redis
  2. 读取数据时
    • 直接从 Redis 读取数据,不需要查询数据库。

示例代码

复制代码
public void updateData(Long id, String newValue) {
    // 先更新数据库
    database.updateById(id, newValue);
    
    // 同步更新 Redis
    redisTemplate.opsForValue().set("data:" + id, newValue, Duration.ofMinutes(10));
}

优点

✅ 读性能高,数据实时同步,减少不一致问题。

✅ 适用于读写并发均衡的业务(如用户状态、账户余额)。

缺点

❌ 写入数据时,Redis 也要同步更新 ,增加了写操作的延迟。

❌ 如果 Redis 更新失败,可能导致缓存和数据库数据不一致。


3. Write Behind(异步写回,适用于高吞吐写入场景)

适用场景写入频繁、对数据实时性要求不高的场景,如日志收集、订单队列等。

工作流程

  1. 数据先写入 Redis(不会立刻写数据库)。
  2. 后台异步任务 (定时任务、MQ 消费者等)批量更新数据库
  3. 当 Redis 数据过期或容量不足时,需要持久化到数据库。

示例代码

复制代码
public void writeData(Long id, String newValue) {
    // 先写入 Redis
    redisTemplate.opsForValue().set("data:" + id, newValue);
    
    // 记录变更到队列
    messageQueue.send("update-db", id);
}

// 定时任务(或 MQ 消费者)批量更新数据库
@Scheduled(fixedRate = 5000)
public void batchUpdateDatabase() {
    List<Long> ids = messageQueue.receive("update-db");
    if (!ids.isEmpty()) {
        database.batchUpdate(ids);
    }
}

优点

高吞吐写入 ,减少数据库压力(因为数据库只需批量写入)。

✅ 适用于日志、订单、点击数据等高并发写入的业务

缺点

❌ 数据持久化到数据库有延迟 ,不适用于实时性要求高的业务。

❌ 断电或 Redis 故障可能导致数据丢失,需要配合持久化方案(AOF、RDB、MQ)。


4. 总结

方案 适用场景 一致性 读性能 写性能 复杂度
Cache Aside(推荐) 读多写少,数据实时性要求不高(如商品详情) ⭐⭐⭐ ⭐⭐⭐ ⭐⭐⭐ ⭐⭐
Write Through 读写比例均衡,数据一致性要求高(如用户状态) ⭐⭐⭐⭐ ⭐⭐⭐ ⭐⭐ ⭐⭐
Write Behind 写多读少,允许短时间数据延迟(如日志、订单) ⭐⭐ ⭐⭐⭐ ⭐⭐⭐⭐ ⭐⭐⭐

🚀 推荐方案

  • 绝大多数场景 :使用 Cache Aside(旁路缓存) ,加上延迟双删策略。
  • 一致性要求高的业务 (如账户余额、权限管理):使用 Write Through
  • 高写入吞吐场景 (日志、订单流式数据):使用 Write Behind + 消息队列

这样可以在性能和一致性之间找到平衡! 🎯

相关推荐
程序猿阿越7 小时前
Kafka源码(七)事务消息
java·后端·源码阅读
m0_748248027 小时前
C++20 协程:在 AI 推理引擎中的深度应用
java·c++·人工智能·c++20
笑我归无处7 小时前
强引用、软引用、弱引用、虚引用详解
java·开发语言·jvm
02苏_7 小时前
秋招Java面
java·开发语言
爱吃甜品的糯米团子8 小时前
详解 JavaScript 内置对象与包装类型:方法、案例与实战
java·开发语言·javascript
程序定小飞8 小时前
基于springboot的学院班级回忆录的设计与实现
java·vue.js·spring boot·后端·spring
攀小黑8 小时前
基于若依-内容管理动态修改,通过路由字典配置动态管理
java·vue.js·spring boot·前端框架·ruoyi
青云交9 小时前
Java 大视界 -- 基于 Java 的大数据可视化在城市空气质量监测与污染溯源中的应用
java·spark·lstm·可视化·java 大数据·空气质量监测·污染溯源
森语林溪9 小时前
大数据环境搭建从零开始(十七):JDK 17 安装与配置完整指南
java·大数据·开发语言·centos·vmware·软件需求·虚拟机
郝开10 小时前
Spring Boot 2.7.18(最终 2.x 系列版本)1 - 技术选型:连接池技术选型对比;接口文档技术选型对比
java·spring boot·spring