结论:三点我都接受,尤其 fencing 那条是我漏掉的——但得说清楚,没有 fencing 的分布式锁本质是「大概率不重复」,不是「不重复」,这跟幂等是两回事。
先认 ctx 那条。你那句「context.WithTimeout 是建议不是强制」确实是我表述里藏起来的假设,db.Exec 不改 ExecContext、os.WriteFile、第三方 SDK 不带 ctx,超时都是空文。顺带补一句:我说的「TTL 按 P99×1.3」本身也带着同一个前提——P99 之外那 1%,恰恰就是最容易超 TTL 的那批,如果它们不响应 cancel,等于按概率撞锁。所以倍数是给「会响应取消」的任务调的参数,不是给不响应的任务兜底。你说的三条硬性检查我完全认同:IO 全走 ctx 版本、循环体里 select { case <-ctx.Done(): return }、TTL 有数据支撑。
续期那个折中方案我接受,比我原来的更闭环——关键差异是你把「续期失败」和「任务自杀」绑定了,我原来反对的只是「续期失败后还在裸跑」。补两个实现细节:① 续期 goroutine 要挂在任务父 ctx 上,并 defer 收掉,否则任务 panic 后 goroutine 泄漏会把锁续成僵尸锁,比过期更难查;② 续期失败建议同时打日志+通知,因为「任务自杀」在生产上的语义是这次跑批没完成,得有人知道。
fencing 你提得对。etcd 的 Revision、Redis 用 INCR 拿的 epoch 都天然单调,写库时带上,DB 侧用 WHERE token < ? 或版本列拒绝落后写入,这是唯一能真正挡住 GC 停顿/网络分区窗口的手段,k8s Lease 的 resourceVersion 也能近似当 token 使。延伸一句:多人协作的项目里,锁 key 记得带 {env}:{service}:{job} 前缀,dev/prod 共用一套 Redis 时互抢锁查起来很费时间。
正式会员
认证极客






