redis setnx+lua脚本是最稳的分布式锁落地方式,etcd cas更适合强一致性场景,zookeeper因go生态弱应避免,生产环境推荐本地锁+分布式协调器分层设计。

Redis SETNX + Lua 脚本是目前最稳的落地方式
Go 里直接用 redis.Client.SetNX 做分布式锁,看似简单,实际极易出错:超时未续期、误删别人锁、网络分区导致脑裂。真正可靠的方案是把加锁、续期、释放三个动作收进一个 Lua 脚本里执行,靠 Redis 单线程原子性兜底。
常见错误现象:ERR Error running script (call to f_...): @user_script:1: user_script:1: attempt to compare number with string —— 多半是 Lua 里没校验锁值类型,或 Go 传参时把整数当字符串塞进 EVAL;redis: nil 返回但业务却认为锁拿到了,是因为没检查 SetNX 的布尔返回值。
- 加锁必须带唯一标识(比如 UUID),不能只靠 key 存在与否判断归属
- 过期时间必须设在 Redis 层(
SETEX或SET ... PX ... NX),不能靠客户端 sleep 后主动删 - 释放锁必须用 Lua 比对 value 再 del,否则 A 加的锁可能被 B 误删
- 推荐用
github.com/go-redis/redis/v9,它的Script.Load().Run()对 Lua 支持更干净
etcd CompareAndSwap(CAS)比 Redis 更适合强一致性场景
如果你的系统对锁的持有者变更极其敏感(比如金融类任务调度),Redis 的最终一致性模型会埋雷——主从同步延迟可能导致两个节点同时认为自己持锁。etcd 的 CompareAndSwap 是 Raft 日志驱动的,只要多数节点存活,锁状态就绝对一致。
使用场景:定时任务去重、配置热更新互斥写入、K8s Operator 中的资源状态同步。
-
clientv3.OpPut必须配合clientv3.OpGet和clientv3.Compare一起进Txn(),单次 Put 不构成 CAS - lease TTL 要显式设置并定期
KeepAlive,否则 lease 过期后锁自动释放,但你的 goroutine 可能还在跑 - etcd 锁 key 建议加前缀如
/locks/order_process/12345,避免和业务 key 冲突 - 注意 etcd v3 的
context.Context超时控制粒度比 Redis 更细,网络抖动时容易提前 cancel
别碰 ZooKeeper —— Go 生态支持太弱,运维成本高
ZooKeeper 的临时顺序节点锁语义很清晰,但 Go 客户端 github.com/samuel/go-zookeeper 已多年不维护,TLS、ACL、session 重连都得自己 patch。线上出问题时,你得同时懂 ZK 的 four-letter-word 命令、Go 的 goroutine 泄漏排查、以及 Java 版 Curator 的行为差异。
Colly 是一个用于 Go 语言的快速开源爬取和爬虫框架。它适用于从简单的页面提取到异步爬虫处理大量页面集合,支持请求回调和结构化解析。
错误现象:zk: session has expired 后 client 不自动重建 session,导致锁永远卡住;zk: connection closed 时未监听 ConnStateChan,goroutine 阻塞在 Lock() 上。
- 除非你在用 Java 主站 + Go 辅助服务,且已有成熟 ZK 运维体系,否则纯 Go 项目直接跳过
- 如果必须对接,用
github.com/go-zk/zk(较新 fork),但要自己实现 lock path watch 和重试逻辑 - ZK 的 watcher 是一次性触发,每次
Get后都要重新SetWatcher,漏一次就收不到通知
本地锁 + 分布式协调器才是生产级组合
单纯依赖 Redis 或 etcd 锁,在高并发下性能瓶颈明显——每个操作都要跨网络。真实业务里,锁粒度往往可以分层:先用 sync.Map 或 singleflight.Group 拦住同进程内重复请求,再用分布式锁兜底跨节点冲突。
比如用户余额扣减:先查 sync.Map.LoadOrStore("uid_123", &sync.Once{}),Once.Do 里才走 etcd CAS;又比如模板渲染,用 singleflight.Group.Do("tpl_v2", fn) 避免 N 个 goroutine 同时编译同一份模板。
-
singleflight.Group的 key 必须可比较且尽量短,避免 map hash 冲突放大 - 本地锁不能替代分布式锁,只是降载——etcd 请求量可能从 10k QPS 降到 200 QPS
- 别在
Once.Do里做阻塞操作(如 HTTP 调用),否则整个 group 会被卡死 - 所有锁路径、超时时间、重试次数必须外置为配置项,不能硬编码
最麻烦的从来不是“怎么加锁”,而是“什么时候该放弃锁”——比如任务执行超时后,是强制释放锁继续下一轮,还是保留锁等原 holder 自行清理?这个决策点没有银弹,得看你业务能不能容忍重复执行或长时间等待。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!










