Skip to content

Redis 缓存与数据库一致性

通俗讲解:缓存和数据库,如何保持一致?

问题引入:为什么会出现不一致?


一、缓存读写模式

1. 先写缓存,还是先写数据库?

问题:更新数据时,先更新缓存还是先更新数据库?


2. 旁路缓存模式(Cache-Aside)


二、保证一致性的方案

方案一:延迟双删

解决方案:延迟双删

代码实现:

java
public void updateUser(User user) {
    // 1. 删除缓存
    redis.del("user:" + user.getId());
    
    // 2. 更新数据库
    userMapper.update(user);
    
    // 3. 延迟双删(等待线程B把脏数据写入缓存后再删除)
    threadPool.execute(() -> {
        try {
            Thread.sleep(500);
            redis.del("user:" + user.getId());
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }
    });
}

方案二:分布式锁

代码实现:

java
public void updateUserWithLock(User user) {
    String lockKey = "lock:user:" + user.getId();
    RLock lock = redissonClient.getLock(lockKey);
    
    try {
        // 获取锁(等待10秒,持有30秒)
        if (lock.tryLock(10, 30, TimeUnit.SECONDS)) {
            try {
                // 1. 更新数据库
                userMapper.update(user);
                
                // 2. 删除缓存
                redis.del("user:" + user.getId());
            } finally {
                lock.unlock();
            }
        }
    } catch (InterruptedException e) {
        Thread.currentThread().interrupt();
    }
}

方案三:订阅 Binlog(Canal)

代码实现:

java
@CanalMessageListener(topic = "mysql.product")
public void handleProductChange(CanalEntry.Entry entry) {
    CanalEntry.RowChange rowChange = CanalEntry.RowChange.parseFrom(entry.getStoreValue());
    
    for (CanalEntry.RowData rowData : rowChange.getRowDatasList()) {
        if (rowChange.getEventType() == CanalEntry.EventType.UPDATE) {
            // 获取更新后的 id
            Long id = null;
            for (Column column : rowData.getAfterColumnsList()) {
                if ("id".equals(column.getName())) {
                    id = Long.parseLong(column.getValue());
                }
            }
            if (id != null) {
                // 删除缓存,触发重新加载
                redis.del("product:" + id);
            }
        }
    }
}

三、一致性强度对比

方案一致性性能复杂度适用场景
延迟双删最终一致大多数场景
分布式锁强一致并发较高
Binlog 订阅最终一致大型分布式
直接更新缓存可能不一致不推荐

四、实际项目推荐方案


五、一句话总结

缓存一致性核心原则:

  1. 写操作:先写数据库,再删缓存
  2. 删除缓存失败:使用延迟双删兜底
  3. 并发场景:使用分布式锁保证原子性
  4. 大型系统:使用 Canal 订阅 Binlog 异步同步

记住:缓存是用来"加速"的,不是用来"持久化"的!


六、常见面试题

Q1:为什么是删除缓存,而不是更新缓存?

Q2:延迟双删的延迟时间怎么设置?

经验值:500ms - 1s

考虑因素:

  • 线程B 读取数据库的时间(通常几百毫秒)
  • 系统负载
  • 业务对一致性的要求

简单理解:等待线程B把脏数据写入缓存后,再删除

Q3:可以只用缓存,不走数据库吗?

不能!缓存的特点:

  • 内存存储,可能丢失
  • 数据是 DB 的副本,不是源数据
  • 必须以数据库为准