延时双删无法解决缓存与数据库一致性根本问题,因第二次删除无法覆盖“旧值回填”盲区;错误在于读请求在主从同步延迟或事务未提交时查库写入旧值,删缓存后仍循环脏读。

延时双删不是“加个定时器就万事大吉”,它解决不了根本问题:缓存删除和数据库更新之间依然存在竞态窗口,且第二次删除的时机无法精确对齐主从同步延迟或读请求回填行为。
为什么延迟500ms后删第二次还是读到旧数据?
这不是延时时间设短了,而是你误判了脏数据产生的真正路径。典型错误时序是:
- 线程A执行
updateProduct():先删缓存 → 更新DB → 启动500ms延迟任务准备二次删 - 线程B在A删完缓存但DB尚未提交(或刚提交)时发起读请求:
redisTemplate.opsForValue().get(key)miss → 查DB拿到**旧值**(因为A事务还没commit,或主从同步未完成)→ 写入缓存 - 500ms后A的延迟任务执行
redisTemplate.delete(key),但此时缓存里已经是B写入的旧值,删掉后下一次读又会重复这个过程
关键点:第二次删除不解决“旧值回填”问题,只清掉可能残留的中间态;而B写入旧值发生在A事务提交前/主从同步前,这是延时无法覆盖的盲区。
Spring中TransactionSynchronizationManager.registerSynchronization()没用对
很多人以为注册了事务同步回调就高枕无忧,但常见错误包括:
Redis 缓存和数据结构管理技能。通过自然语言操作 Redis,支持 String、Hash、List、Set、ZSet、Stream 等数据结构操作。当用户提到 Redis、缓存、消息队列、会话存储时使用此技能。
- 回调里直接调用
redisTemplate.delete(key),但该操作可能走的是连接池中未绑定事务的连接,导致删的是另一个Redis实例(集群下尤其明显) - 用了
@CacheEvict注解却没配key = "product:{#command.productId}"这种带哈希标签的格式,导致Cluster模式下key分散在不同slot,DEL命令只命中部分节点 - 事务传播行为是
REQUIRES_NEW或嵌套事务,registerSynchronization()注册的回调不会触发
正确做法是:在 afterCommit 回调中,用同一个 RedisConnection 实例(或确保Lettuce client配置了 ClusterClientOptions.builder().maxRedirects(5).build())执行 delete,并显式指定key哈希标签。
延迟双删掩盖了更关键的问题:读请求的缓存回填逻辑本身有缺陷
看你的读方法:
if (cache != null) {
return cache;
}
Product product = productMapper.selectById(productId); // ← 这里查的是从库?
redisTemplate.opsForValue().set(key, product, 10, TimeUnit.MINUTES);
如果 productMapper.selectById() 走的是MySQL从库,而更新操作写的是主库,那么即使DB已提交,从库延迟也会让读请求拿到旧值并写入缓存——这时无论删几次都没用。你应该:
- 对强一致性场景的读接口,强制走主库(如MyBatis Plus的
@DS("master")) - 或者在更新后主动等待主从同步(
SHOW SLAVE STATUS+SELECT MASTER_POS_WAIT,仅限可控环境) - 或者干脆放弃“读缓存+写DB”混合架构,改用binlog监听(Canal)或MQ异步更新缓存,把一致性责任交给下游消费者
真正要警惕的,不是“删得够不够多”,而是“谁在什么时机、以什么一致性级别往缓存里写数据”。延时双删只是给脆弱的读写流程打补丁,补丁再厚,也盖不住底下的并发裸奔。

















