延迟双删不能用 time.Sleep 硬等,因其会阻塞 goroutine、放大 P99 延迟,且进程崩溃导致二次删除丢失;应解耦为 Redis 过期事件监听或有序集合轮询,确保幂等、可靠、不依赖事务。

延迟双删为什么不能用 time.Sleep 硬等
在 HTTP handler 里直接 time.Sleep(500 * time.Millisecond),表面看是“等读请求走完再删”,实际会卡住整个 goroutine,放大接口 P99 延迟,尤其在高并发时容易拖垮服务。更严重的是,如果此时进程被 kill 或崩溃,第二次删就永远丢失,缓存脏数据长期存在。
实操建议:
- 绝不在主流程中调用
time.Sleep做延迟; - 不依赖
context.WithTimeout包裹第二次删——超时只取消 goroutine,不撤回已发的redis.Client.Del命令; - 延迟必须解耦:要么靠 Redis 自身事件(如过期 pub/sub),要么靠后台轮询或消息队列。
用 Redis key 过期事件触发第二次删
这是生产环境最轻量又可靠的方案:写 DB 成功后,立刻 SET cacheKey_delay true EX 300(TTL 300 秒),再起一个 goroutine 订阅 __keyevent@0__:expired,监听到该 key 过期时执行 redis.Client.Del(ctx, cacheKey)。
注意点:
- 必须启用 Redis 的
notify-keyspace-events Ex配置,否则 pub/sub 收不到过期事件; - 订阅需在连接池外单独建连接,避免和业务连接混用导致阻塞;
- 监听 goroutine 要加 recover 和重连逻辑,防止断连后事件丢失;
- key 名建议带业务前缀(如
user:123_delay),避免误删。
没开 keyspace events 怎么办:退化为有序集合轮询
很多线上 Redis 实例出于性能考虑禁用了 keyspace events。这时可用 ZADD delay_queue 把待删 key 写入有序集合,再由常驻 goroutine 每秒执行一次 ZRANGEBYSCORE delay_queue -inf 扫描并批量 DEL。
使用 uber-go/dig 在 Golang 中实现依赖注入——基于反射容器,提供 Provide/Invoke、dig.In/dig.Out 参数与结果对象,支持命名依赖等
关键细节:
- 插入时用
time.Now().Add(500 * time.Millisecond).UnixMilli()作为 score,确保延迟准确; - 扫描后必须用
ZREMRANGEBYSCORE删除已处理项,否则重复触发; - 开发环境可用
KEYS *_delay快速验证,但生产禁用——会阻塞 Redis 单线程; - score 精度用毫秒而非秒,避免多条任务挤在同一秒导致漏删。
第二次删操作必须幂等且不进事务
redis.Client.Del 本身是幂等的,但若中间加了本地缓存层(比如基于 sync.Map 的 LRU),就得自己保证多次 Delete 不 panic、不污染状态。更大的坑是有人试图把两次 DEL 塞进 Redis pipeline 或 MULTI/EXEC 事务——这完全跑偏了。
原因很直接:
- 双删要解决的是“时间点”问题,不是“原子性”问题;
-
pipeline只是打包发命令,无法控制两次删之间的间隔; -
MULTI在 Redis 7.0 前不支持条件执行,防不住并发写覆盖; - DB 事务回滚时,Redis 命令早已发出,反而造成“缓存空、DB 有值”的不一致。
真正要盯紧的,是第二次删前是否能确认 DB 更新已落盘——比如结合乐观锁 version 字段做一次 GET 校验,比纯延迟更准,但也增加了 DB 查询压力。这个权衡点,往往被忽略。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!










