redis面试:如何保证双写一致 一、实际问题在实际开发中我们通常会用 Redis 作为缓存来加速数据读取。但当数据需要更新时就面临一个经典问题应该先删除缓存还是先更新数据库遗憾的是无论选择哪种顺序在并发场景下都可能产生脏数据。以先删缓存再更新数据库为例考虑以下时序线程 A 删除缓存线程 B 读取缓存未命中从数据库读到旧值写入缓存线程 A 更新数据库为新值此时数据库中是新值而缓存中却是线程 B 写入的旧值数据不一致就此产生。反过来先更新数据库再删缓存同样存在类似的竞态窗口。由于线程调度的不确定性这类问题在高并发下并非小概率事件。要解决它常见的思路有两种延迟双删和读写锁各有适用场景。二、延迟双删1. 删除 Redis 缓存 ← 第一次删 2. 更新数据库 3. 发送一条延时消息到 MQ或者Canal监听binlog日志 4. MQ 消费者收到消息后再次删除缓存 ← 第二次删延迟双删的核心思想很简单既然一次删除缓存无法保证一致性那就删两次中间给数据库更新留出时间窗口。基本流程删除 Redis 缓存 ←第一次删除更新数据库发送一条延迟消息到 MQ或通过 Canal 监听 binlogMQ 消费者收到消息后再次删除缓存 ←第二次删除第一次删除是为了让后续读请求能走到数据库更新数据库后再补一次删除把可能在第一步和第二步之间被脏数据回填的缓存清理掉。两种实现方式MQ 延迟消息业务代码中更新数据库后向消息队列发送一条延迟消息消费者到期后执行缓存删除。常用 RocketMQ 的延迟消息或 RabbitMQ 的 TTL 死信队列来实现。Canal 监听 binlogCanal 伪装为 MySQL 的从节点通过读取 binlog 日志感知数据变更自动触发缓存删除。这种方式的优势在于不需要修改业务代码对应用层完全透明。两个需要注意的点第一第二步发送的应该是延迟消息而非立即删除。这是为了给数据库主从复制留出同步时间——如果主库刚写完、从库还没同步此时删缓存后读请求打到从库读到的可能还是旧值缓存又会被旧数据回填。第二延迟双删并不能做到强一致两次删除之间仍然存在一个短暂的不一致窗口。如果业务对一致性要求不高比如缓存热点商品数据、文章浏览量等这种方案完全够用且实现成本低、性能影响小。三、读写锁如果对数据一致性有严格要求延迟双删的短暂不一致窗口是不可接受的这时可以考虑读写锁方案。Redis 提供了两种锁的语义读锁共享锁多个线程可以同时持有读锁进行读操作但读锁存在时写操作必须等待。写锁排他锁一个线程持有写锁后其他线程的读操作和写操作全部阻塞。使用方式读数据时加读锁从缓存读取缓存未命中则加写锁从数据库加载并回填缓存。写数据时加写锁先更新数据库再删除或更新缓存。读写锁能够完全保证数据一致性因为同一时刻要么只有读操作在并发执行要么只有一个写操作在执行从根本上消除了竞态条件。代价是性能——写操作会阻塞所有读写请求在读多写少的场景下影响尤为明显。因此读写锁适合对一致性要求极高的业务比如优惠券计算、订单交易、库存扣减等场景。四、方案对比延迟双删读写锁一致性最终一致存在短暂窗口强一致性能影响小异步删除写操作阻塞读写性能下降明显实现复杂度中等需引入 MQ 或 Canal较低Redis 原生支持适用场景热点数据、一致性要求不高的读多场景交易、库存等强一致性场景实际选型时不需要非此即彼。同一个系统中对不同业务模块可以根据一致性要求分别采用不同策略——大部分读场景用延迟双删关键写场景用读写锁这样能在一致性和性能之间取得较好的平衡。