在主从数据库结构下如何使用延时双删策略?

逻辑分析:数据库读写分离+缓存一致性

数据库读写分离 的架构下,读请求从 从库 查询,写请求更新 主库,然后再通过 主从同步 将数据同步到 从库。但同步有延迟,可能导致查询到旧数据,并错误地写入缓存,造成 缓存脏数据

问题复现

  1. 请求A(写操作)

    • 删除缓存
    • 更新数据库(主库)
    • 主从同步有延迟
  2. 请求B(读操作)

    • 查询缓存,发现数据不存在
    • 从从库读取数据(由于主从同步延迟,数据是旧的)
    • 将旧数据写回缓存
  3. 主从同步完成,但缓存仍然是旧数据,导致后续请求仍然读取错误数据。


解决方案:延时双删策略

核心思路

  1. 第一次删除缓存(避免查询到旧值)
  2. 写入数据库(主库)
  3. 等待一段时间(大于主从同步的延迟)
  4. 第二次删除缓存(确保从库同步完成后缓存不存旧数据)

Demo

1. 读操作(查询)

public Object read(String key) {
    // 1. 先查询缓存
    Object data = redisUtils.get(key);
    if (data != null) {
        return data; // 缓存命中,直接返回
    }

    // 2. 缓存未命中,从数据库(从库)查询
    data = db.readFromSlave(key);

    // 3. 由于主从同步延迟,我们需要对数据进行额外判断
    if (data != null) {
        // 4. 异步任务或延迟写入缓存,避免脏数据
        final Object finalData = data;
        new Thread(() -> {
            try {
                Thread.sleep(200); // 等待主从同步完成(时间根据业务调整)
                redisUtils.set(key, finalData);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }).start();
    }
    return data;
}

2. 写操作(更新)

public void write(String key, Object data) {
    // 1. 删除缓存(防止脏数据)
    redisUtils.del(key);

    // 2. 更新数据库(主库)
    db.updateToMaster(data);

    // 3. 延迟删除缓存(确保主从同步完成)
    new Thread(() -> {
        try {
            Thread.sleep(500); // 这里的时间要大于主从同步时间
            redisUtils.del(key);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }).start();
}
  • 第一次删除缓存(防止旧数据被读取)
  • 写入数据库
  • 等待主从同步完成
  • 第二次删除缓存(防止旧数据回写缓存)

这样可以有效防止 数据库读写分离 造成的 缓存脏数据,保证 缓存与数据库的一致性

问题是这个“Thread.sleep(500);”还需要改进。

如何控制 Thread.sleep(500) 时间?是否可以加分布式锁?

直接使用 Thread.sleep(500) 来等待主从同步完成 并不精准,因为:

  1. 主从同步时间不固定
    • 数据库负载、网络带宽、事务量 等影响,实际同步时间可能大于 500ms 或更短。
  2. 固定等待时间可能导致性能问题
    • 过长:影响写入效率。
    • 过短:缓存删除得太早,仍可能出现缓存脏数据问题。

所以,理想的方法是 动态调整缓存删除的时机,保证数据正确性的同时减少性能损耗。


解决方案

可以用 分布式锁 + 异步回调机制 来更精准地控制缓存删除的时机,而不是简单的 Thread.sleep()

方案 1:使用分布式锁,确保主从同步完成后删除缓存

核心思路

  • 更新数据库时获取分布式锁,保证只有一个线程在操作缓存。
  • 异步任务轮询主从同步状态,当数据同步完成后删除缓存,而不是等待固定时间。

实现示例(基于 Redis 分布式锁 + 轮询检测主从同步状态)

public void write(String key, Object data) {
    // 1. 删除缓存(防止脏数据)
    redisUtils.del(key);

    // 2. 更新数据库(主库)
    db.updateToMaster(data);

    // 3. 使用分布式锁防止并发修改缓存
    String lockKey = "lock:" + key;
    String requestId = UUID.randomUUID().toString();

    if (redisUtils.tryLock(lockKey, requestId, 10, TimeUnit.SECONDS)) { 
        // 异步任务轮询主从同步状态
        new Thread(() -> {
            try {
                int retryCount = 0;
                while (retryCount < 5) { // 最多尝试 5 次
                    Thread.sleep(100); // 每次间隔 100ms

                    // 查询从库是否同步完成
                    Object newData = db.readFromSlave(key);
                    if (newData != null && newData.equals(data)) {
                        redisUtils.del(key); // 4. 确保同步完成后删除缓存
                        break;
                    }
                    retryCount++;
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                redisUtils.releaseLock(lockKey, requestId); // 释放锁
            }
        }).start();
    }
}

方案 2:让数据库提供主从同步完成的事件通知

有些数据库(如 MySQL 8.0+)支持 binlog 事件通知,可以监听主从同步完成的状态,然后再删除缓存。
不过,这种方式需要 额外的消息队列(如 Kafka、RabbitMQ)监听 MySQL Binlog

步骤
  1. 监听 MySQL Binlog(主从同步完成时触发事件)
  2. 收到同步完成事件后删除缓存
实现思路
// 监听 MySQL Binlog 日志,当主从同步完成时删除缓存
public void onReplicationComplete(String key) {
    redisUtils.del(key);
}

缺点:需要额外搭建 Binlog 监听 系统,复杂度较高。


总结

方案方式优点缺点
固定等待时间 (Thread.sleep())简单延迟实现简单主从同步时间不固定,可能太长或太短
分布式锁 + 轮询查询先删除缓存 -> 轮询从库状态,等同步完成再删缓存精确控制缓存删除时间,无需固定 sleep()多次查询数据库,有一定性能消耗
监听 MySQL BinlogMySQL 主从同步完成时触发删除缓存事件驱动,精准控制需要搭建 Binlog 监听系统,较复杂

推荐

  • 如果你的系统支持 Binlog 监听,可以使用 方案 3(监听数据库同步事件),精准删除缓存。
  • 如果不支持 Binlog,建议用 方案 2(分布式锁 + 轮询数据库),在主从同步完成后删除缓存,避免使用 Thread.sleep() 固定延迟。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值