Redis如何与数据库保持双写一致性

一、核心问题与基础策略

问题根源

并发场景下,"更新数据库" 与 "操作缓存" 是两个独立动作,无法保证原子性,可能出现:

  1. 线程 A 删了缓存,还没更新数据库,线程 B 读了旧数据到缓存。
  2. 线程 A 更新了数据库,还没删缓存,线程 B 读了旧缓存。

基础原则

不建议 "更新缓存",只建议 "删除缓存"。因为更新缓存容易在并发下产生脏数据,而删除缓存后,下次读取会从数据库重新加载最新数据(Cache-Aside Pattern)。


二、方案一:延迟双删

流程

  1. 先删除缓存
  2. 更新数据库
  3. 休眠一小段时间(如 500ms - 1s)再次删除缓存

解决的问题

如果在步骤 1 和 2 之间有并发读把旧数据写回了缓存,步骤 3 的 "延迟二次删除" 可以把这个脏数据删掉。

Java 代码示例

复制代码
@Service
public class ProductService {

    @Autowired
    private RedisTemplate<String, Object> redisTemplate;
    @Autowired
    private ProductMapper productMapper;

    // 自定义线程池用于异步延迟删除
    private final ExecutorService executor = Executors.newSingleThreadExecutor();

    @Transactional(rollbackFor = Exception.class)
    public void updateProduct(Product product) {
        String key = "product:" + product.getId();

        // 1. 先删除缓存
        redisTemplate.delete(key);

        // 2. 更新数据库
        productMapper.updateById(product);

        // 3. 异步延迟二次删除 (防止并发脏读)
        executor.submit(() -> {
            try {
                Thread.sleep(500); // 时间根据业务耗时评估
                redisTemplate.delete(key);
                System.out.println("延迟双删成功: " + key);
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            }
        });
    }
}

三、方案二:基于消息队列的异步删缓存

如果 "延迟双删" 的第二次删除失败了,数据就不一致了。为了保证删除动作一定执行,可以引入消息队列(MQ)。

  1. 更新数据库。
  2. 数据库操作成功后,发送一条 "删除缓存" 的消息到 MQ。
  3. 消费者接收消息,执行删除缓存操作。
  4. 配合消费者的重试机制(如果 Redis 挂了或删除失败,不断重试直到成功)。

四、方案三:基于 Binlog 订阅

核心思路

  1. 业务代码只需要更新数据库,不关心缓存。
  2. 利用 MySQL 的 Binlog 日志,使用中间件伪装成 MySQL Slave 监听 Binlog。
  3. 监听到数据变更后,通过程序解析 Binlog,异步删除 Redis 中对应的缓存。

优点

  • 业务代码极其清爽,没有任何缓存逻辑。
  • 只要数据库更新成功,缓存最终一定会被删除。

五、方案四:分布式锁

如果业务对数据一致性要求极高(比如金融、订单核心),不允许任何中间状态的脏读,可以使用分布式锁。(强一致性带来的坏处就是性能较差)

流程

  1. 读数据:加分布式锁 -> 读缓存 -> 没命中则读数据库并写回缓存 -> 释放锁。
  2. 写数据:加分布式锁 -> 删缓存 -> 更新数据库 -> 释放锁。

Java代码示例

复制代码
@Autowired
private RedissonClient redissonClient;

public void updateWithLock(Product product) {
    String lockKey = "lock:product:" + product.getId();
    RLock lock = redissonClient.getLock(lockKey);
    
    try {
        // 尝试加锁
        lock.lock();
        
        // 1. 删缓存
        // 2. 更数据库
        
    } finally {
        // 释放锁
        lock.unlock();
    }
}

六、总结与选型建议

表格

方案 一致性 性能 维护成本 适用场景
延迟双删 最终 并发量一般,业务简单
消息队列 最终 并发量高,需要可靠性保证
Binlog 最终 中高 首选,业务解耦,数据量大
分布式锁 对一致性要求极高(钱、库存)
相关推荐
闻哥1 小时前
深入Redis的RDB和AOF两种持久化方式以及AOF重写机制的分析
java·数据库·spring boot·redis·spring·缓存·面试
培小新2 小时前
MySQL 集群技术(环境+一主二从配置)
数据库·mysql
ruanyongjing2 小时前
Spring TransactionTemplate 深入解析与高级用法
java·数据库·spring
fengxin_rou2 小时前
[Redis从零到精通|第六篇]:Redis的主从同步
java·数据库·redis·缓存
java干货2 小时前
拒绝全表扫描灾难:用 SSCAN 安全遍历 Redis 亿级 Set 集合
数据库·redis·安全
刘一说2 小时前
使用 CLion 搭建 Redis 6.x 源码调试环境:从零开始的完整指南
数据库·redis·缓存
人道领域2 小时前
苍穹外卖:菜品分页查询与删除功能(保姆级详解)
java·开发语言·数据库·后端·spring
Navicat中国2 小时前
利用 PostgreSQL 的强大力量:Supabase 简介
数据库·postgresql·navicat·supabase
yqzyy3 小时前
Redis 设置密码无效问题解决
数据库·redis·缓存