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 最终 中高 首选,业务解耦,数据量大
分布式锁 对一致性要求极高(钱、库存)
相关推荐
jiayou642 小时前
KingbaseES 表级与列级加密完全指南
数据库·后端
用户3074596982071 天前
Redis 延时队列详解
redis
GBASE1 天前
G术时刻 |GBase 8s数据库事务并发控制之封锁技术介绍(下)
数据库
烤代码的吐司君1 天前
Redis 数据结构 ZSet, BIT, HyperLogLog,Geo 空间数据
redis·后端
xiezhr1 天前
逛GitHub发现了一款免费的带AI功能的数据库管理工具
数据库·ai编程·dba
吃糖的小孩2 天前
给 QQ AI 机器人设计“可控记忆”:会话摘要、手动长期记忆与角色卡边界
数据库
笃行3503 天前
金仓数据库数据安全双防线:静态存储加密与传输加密实战
数据库
笃行3503 天前
金仓数据库物理备份实战:sys_rman 全流程演练与误覆盖抢救
数据库
笃行3503 天前
金仓数据库逻辑备份实战:从全库导出到 Schema 替换的完整闭环
数据库