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 最终 中高 首选,业务解耦,数据量大
分布式锁 对一致性要求极高(钱、库存)
相关推荐
Jelena1577958579224 分钟前
电商运营分析数据比价接口实战:多平台价格监控与智能决策系统
java·大数据·数据库
流星白龙2 小时前
【Redis】3.Redis安装与命令行客户端
数据库·redis·缓存
凌虚3 小时前
基于 PostgreSQL WAL 构建 CDC 系统:原理与工程实现
数据库·后端·postgresql
BullSmall3 小时前
PostgreSQL 14 pg_dumpall 完整备份指南
数据库·postgresql·oracle
一只枫林4 小时前
MySQL内、外连接知识点汇总
java·前端·数据库
暮暮祈安4 小时前
Celery 新手入门指南
java·数据库·python·flask·httpx
xiaowang1234shs5 小时前
怪兽轻断食技术深度测评:从断食计时引擎到AI识别算法的工程实践解析
数据库·人工智能·算法·macos·机器学习·p2p·visual studio
Quincy_Freak5 小时前
银河麒麟 aarch64 环境下轻量 SQLite 管理方案实践:SQLiteGo 落地体验
数据库·sqlite·sqlitego
灵机一物5 小时前
企业选型参考:2026 CXL 内存扩展方案六强测评与落地指南
服务器·网络·数据库·人工智能
秋风渡.5 小时前
MySQL_数据类型知识
数据库·mysql