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 最终 中高 首选,业务解耦,数据量大
分布式锁 对一致性要求极高(钱、库存)
相关推荐
YOU OU5 小时前
Spring IoC&DI
java·数据库·spring
Muscleheng6 小时前
Navicat连接postgresql时出现‘datlastsysoid does not exist‘报错
数据库·postgresql
罗超驿7 小时前
18.事务的隔离性和隔离级别:MySQL面试高频考点全解析
数据库·mysql·面试
jran-7 小时前
Redis 命令
数据库·redis·缓存
小江的记录本8 小时前
【Java基础】Java 8-21新特性:JDK21 LTS:虚拟线程、模式匹配switch、结构化并发、序列集合(附《思维导图》+《面试高频考点清单》)
java·数据库·python·mysql·spring·面试·maven
June`8 小时前
多线程redis下如何解决aof重写和rdb持久化的数据一致性问题
数据库·redis·缓存
二宝哥8 小时前
离线安装maven
java·数据库·maven
SZLSDH8 小时前
场景适配论 | 数字孪生IOC建设中渲染技术与智能体能力的协同逻辑
前端·数据库·ai·数字孪生·数据可视化·智能体
这个DBA有点耶8 小时前
SQL改写实战:子查询、CTE、窗口函数性能对比
数据库·mysql·性能优化
@我漫长的孤独流浪9 小时前
数据库完整性约束全解析:从理论到实践
数据库