【面试题】缓存先删后写如何避免窗口期的旧数据写入缓存
缓存先删后写如何避免窗口期的旧数据写入缓存
方案1:分布式锁控制读写顺序
通过分布式锁(如Redis的SET NX、ZooKeeper等),在更新操作的“删缓存-更数据库”窗口期内,阻塞读请求对缓存的写入,确保数据库更新完成后再允许读请求加载新数据。
流程:
-
更新操作:
- 对目标缓存键(如
user:100)加写锁(互斥锁,同一时间只有一个更新操作能执行)。 - 删除缓存(
DEL user:100)。 - 更新数据库(
UPDATE user SET ... WHERE id=100)。 - 释放写锁。
- 对目标缓存键(如
-
读请求:
- 检测缓存是否存在:若存在,直接返回;若不存在,尝试获取读锁(与写锁互斥,即写锁未释放时,读锁获取失败)。
- 若读锁获取失败(说明有更新操作在执行),则等待一段时间(如100ms)后重试,直到缓存存在或读锁获取成功。
- 若读锁获取成功,从数据库读取数据(此时数据库已更新),写入缓存,最后释放读锁。
优点:
- 严格保证一致性,避免窗口期的旧数据写入缓存。
- 适用于对数据一致性要求高的场景(如交易、库存)。
缺点:
- 分布式锁会增加系统复杂度(需处理锁超时、释放失败等问题)。
- 可能降低并发性能(读请求需等待写锁释放)。
方案2:延迟双删(Delete Twice)
在“先删后写”的基础上,增加一次延迟删除缓存的操作,清除窗口期可能被写入的旧数据。
流程:
- 第一次删除缓存(
DEL user:100)。 - 更新数据库(
UPDATE user SET ... WHERE id=100)。 - 延迟一段时间(如500ms,根据业务接口耗时设置)后,第二次删除缓存(
DEL user:100)。
原理:
- 若窗口期内有读请求写入了旧数据(缓存被旧数据填充),第二次延迟删除会将其清除。
- 后续读请求会重新从数据库读取新数据并写入缓存,最终保证一致性。
优点:
- 实现简单,无需锁机制,对性能影响小。
- 适用于并发量不极高、对短暂不一致可容忍的场景(如用户信息、商品描述)。
缺点:
- 延迟时间难以精确控制(若延迟过短,可能在旧数据写入缓存前执行第二次删除,无效;若过长,会存在短暂不一致)。
- 极端情况下,第二次删除前若有新的读请求,仍可能读到旧缓存。
方案3:读写锁隔离(Read-Write Lock)
通过读写锁区分“更新操作”和“读请求”的权限,确保写操作执行时,读请求只能等待或读取数据库新数据。
流程:
- 写锁:更新操作(删缓存+更数据库)期间持有写锁,此时不允许任何读请求写入缓存(但允许读请求直接读数据库,避免阻塞)。
- 读锁:读请求只有在写锁释放后,才能获取读锁并将数据库数据写入缓存。
具体操作:
-
更新操作:
- 获取写锁 → 删除缓存 → 更新数据库 → 释放写锁。
-
读请求:
- 缓存存在 → 直接返回;
- 缓存不存在 → 尝试获取读锁:
- 若写锁未释放(获取读锁失败),直接读数据库(此时数据库可能已更新)并返回(不写入缓存,避免旧数据);
- 若写锁已释放(获取读锁成功),读数据库(新数据)→ 写入缓存 → 释放读锁 → 返回。
优点:
- 减少锁对读请求的阻塞(读请求可直接读数据库,不强制等待)。
- 平衡一致性和性能,适合读多写少的场景。
缺点:
- 实现较复杂(需设计读写锁的优先级和释放逻辑)。
方案4:版本号机制(Version Control)
给数据库和缓存的数据添加版本号,通过版本号校验避免旧数据覆盖新缓存。
流程:
- 数据库表中增加
version字段(每次更新+1),缓存中存储(value, version)。 - 更新操作:
- 删除缓存 → 更新数据库(
version+1)。
- 删除缓存 → 更新数据库(
- 读请求:
- 缓存不存在 → 读数据库(获取
value和new_version); - 尝试写入缓存时,若缓存此时已被其他请求写入(带有
old_version),则比较版本号:- 若
new_version > old_version:覆盖缓存(写入新数据+新版本); - 若
new_version <= old_version:不写入(避免旧数据覆盖新缓存)。
- 若
- 缓存不存在 → 读数据库(获取
原理:
- 即使窗口期内有旧数据写入缓存,后续读请求的新版本数据会覆盖旧版本,最终保证缓存与数据库一致。
优点:
- 无锁机制,性能好,适合高并发场景。
- 版本号天然支持分布式环境。
缺点:
- 需修改数据库表结构(增加版本号字段)。
- 极端情况下可能存在短暂的版本号不一致(但最终会修正)。
总结与选择建议
- 强一致性场景(如库存、支付):优先用分布式锁,严格控制读写顺序。
- 高并发、可容忍短暂不一致:用延迟双删(简单)或版本号机制(性能好)。
- 读多写少场景:用读写锁隔离,平衡性能和一致性。
