go语言无开箱即用分布式锁,sync.mutex仅限单进程;跨实例需redis/etcd等外部存储,redis常用但须原子加锁(setnx+lua)与校验value解锁,etcd适合强一致场景,锁粒度和超时时间需匹配业务p99并设看门狗续期。

Go语言框架本身不提供开箱即用的分布式锁,sync.Mutex 和 sync.RWMutex 仅限单进程内生效,跨实例部署时完全失效——这不是配置问题,是设计边界。
为什么不能直接用 sync.Mutex 做分布式锁
多个 Go 实例(比如 Kubernetes 中的多个 Pod)各自运行独立进程,内存不共享。sync.Mutex 只锁住当前 goroutine 所在的那块内存,其他实例完全感知不到。你看到“加锁成功”,只是本地锁住了,业务仍会并发写 DB、重复发消息、扣双倍库存。
- 常见错误现象:
sync.Mutex在本地单元测试里一切正常,上线后秒杀超卖、定时任务跑两遍、订单状态被覆盖 - 根本原因:锁的作用域错配——你要的是“全局唯一持有”,它只提供“本进程唯一持有”
- 真正能用的方案必须依赖外部存储的原子能力:Redis、etcd 或 ZooKeeper,三者选其一,没有第四种“纯 Go 框架方案”
go-redis/v9 的 SetNX + Lua 是最常用落地方式
Redis 因部署简单、性能高、客户端成熟,成为大多数 Go 项目首选。但必须避开两个经典坑:
- 加锁不能分两步:
SETNX成功后再EXPIRE——中间若崩溃或网络中断,key 永久残留,死锁 - 解锁不能直接
DEL——A 加锁、B 误删,导致 A 还没处理完就被释放,引发并发冲突 - 正确做法:
rdb.SetNX(ctx, key, value, ttl)一行完成原子加锁;解锁必须用 Lua 脚本校验value相等再删,脚本形如:if redis.call("get", KEYS[1]) == ARGV[1] then return redis.call("del", KEYS[1]) else return 0 end -
value必须全局唯一(推荐uuid.NewString()),禁用时间戳、固定字符串、进程 PID 等弱唯一标识
etcd Txn + Lease 更适合强一致场景
如果你的业务对数据一致性要求极高(如金融类状态更新、配置中心变更),etcd 是比 Redis 更稳的选择,因为它的事务模型天然防竞争:
- 加锁必须走
Txn().If(Compare(Version(key), "=", 0)).Then(OpPut()),否则多个客户端同时Put会全部成功,锁形同虚设 -
LeaseID必须由client.Grant()获取,不能自己拼字符串或用时间戳——否则续期失败、过期不触发、Watch 断连后无法 resume - 抢锁失败时,用
WithPrevKV可直接拿到当前持有者的 value,方便排查谁卡住了锁,而 Redis 需额外 GET,多一次 RTT - Watch 锁 key 删除事件可实现秒级唤醒,但必须处理断连重试逻辑(
WithRev指定起始 revision),否则 Watch 失效后 goroutine 无限挂起
锁粒度和超时时间才是最容易被忽略的复杂点
很多人花大力气封装了完美的加锁/解锁逻辑,却在两个地方翻车:
- 锁 key 缺少业务上下文,比如用
"order_lock"而不是"order_lock:123456",结果所有订单串行执行,吞吐归零 - 超时时间拍脑袋填
30 * time.Second,但实际 P99 业务耗时是 42 秒——锁提前释放,后续请求闯入;或者设成 5 分钟,节点宕机后锁残留太久,整个服务卡死 - 正确做法:超时时间 = 业务 P99 耗时 × 2~3,并配合看门狗机制自动续期(
RefreshLock),且续期 goroutine 必须做状态控制,避免同一把锁启动多个续期协程
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











