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 最终 中高 首选,业务解耦,数据量大
分布式锁 对一致性要求极高(钱、库存)
相关推荐
数智化管理手记4 小时前
精益生产中的TPM管理是什么?一文破解设备零故障的密码
服务器·网络·数据库·低代码·制造·源代码管理·精益工程
翊谦4 小时前
Java Agent开发 Milvus 向量数据库安装
java·数据库·milvus
難釋懷5 小时前
OpenResty实现Redis查询
数据库·redis·openresty
别抢我的锅包肉6 小时前
【MySQL】第四节 - 多表查询、多表关系全解析
数据库·mysql·datagrip
Database_Cool_6 小时前
OpenClaw-Observability:基于 DuckDB 构建 OpenClaw 的全链路可观测体系
数据库·阿里云·ai
刘~浪地球6 小时前
Redis 从入门到精通(五):哈希操作详解
数据库·redis·哈希算法
zzh0817 小时前
MySQL高可用集群笔记
数据库·笔记·mysql
Shely20177 小时前
MySQL数据表管理
数据库·mysql
爬山算法7 小时前
MongoDB(80)如何在MongoDB中使用多文档事务?
数据库·python·mongodb
APguantou7 小时前
NCRE-三级数据库技术-第2章-需求分析
数据库·需求分析