因为sync.mutex仅在单进程内存空间内有效,微服务多实例部署时各进程互不感知,无法实现跨节点互斥;分布式锁必须依赖redis等共享外部存储协调,redsync通过set+nx+px、唯一value防误删、lua原子释放及quorum机制保障可靠性。

为什么直接用 sync.Mutex 在微服务里会失效
因为 sync.Mutex 只在单进程内有效。当你把服务部署成多个实例(比如 Kubernetes 里跑 3 个 Pod),每个实例都有自己的内存空间,sync.Mutex 完全互不感知——A 实例加锁了,B 实例照样能进临界区,数据就可能被并发改乱。
分布式锁的本质是:多个进程/机器必须通过一个「共享且串行化」的外部存储来协调加锁行为。常见选型是 Redis、etcd 或 ZooKeeper。Golang 模块集成时,关键不是“怎么写锁”,而是“怎么让锁行为可预测、可重入、可续期、可释放”。
用 redis/go-redis 实现带自动续期的锁最稳妥
社区最常用的是基于 Redis 的 SET key value NX PX 原语实现,但手动处理过期、释放、续期极易出错。推荐直接用封装成熟的库:github.com/go-redsync/redsync/v4(注意 v4 是支持上下文取消的最新稳定版)。
- 它内置了随机 value 防误删、Lua 脚本原子释放、多 Redis 实例 Quorum 机制(防脑裂)
- 必须自己传入
*redis.Client,不能复用全局 client 连接池——每个锁实例应绑定独立的redsync.NewPool,否则连接复用可能导致超时传递混乱 - 加锁要设
context.WithTimeout,否则网络卡住会永久阻塞;释放锁必须用mutex.Unlock(),不能靠过期自动清理——业务逻辑出错时,过期时间一到就丢锁,其他协程可能闯入
pool := redsync.NewPool(client)
mutex := pool.GetMutex("order:12345", redsync.WithExpiry(8*time.Second))
if err := mutex.Lock(); err != nil {
return err
}
defer mutex.Unlock() // 必须 defer,且确保只调用一次
etcd 方案更适合强一致场景,但要注意 Lease 续期频率
如果你的系统已重度依赖 etcd(比如 K8s 原生调度),用 go.etcd.io/etcd/client/v3 自建锁更合适——它提供 Lease + CompareAndSwap 原语,天生支持租约续期和监听失效。
坑点在于:etcd 的 lease 续期不是后台自动做的,你得显式调用 client.KeepAlive() 并处理 channel 关闭;若业务逻辑耗时长,而续期 goroutine 挂了,lease 一过期锁就丢了。
- 不要用
client.Grant()一次性申请 long TTL,而是用client.KeepAlive()持续刷新 - 加锁时用
cmp: clientv3.Compare(clientv3.CreateRevision(key), "=", 0)确保首次创建,避免覆盖已有锁 - 释放锁必须用
client.Delete()+clientv3.WithPrevKV()校验 value 是否匹配,防止删错别人的锁
别忽略锁粒度和错误恢复逻辑
分布式锁不是银弹。锁太粗(比如整个用户 ID)会导致吞吐骤降;锁太细(比如订单项 ID)又可能漏保护。实际模块中,应结合业务语义选 key,例如:"inventory:sku_789:stock" 比 "inventory:sku_789" 更精准。
更关键的是:锁失败后怎么退?是返回错误、降级为本地缓存、还是走异步补偿?这些逻辑不能藏在锁封装里,必须由业务模块显式决策。
另外,所有锁操作都应打结构化日志,记录 key、持有者 ID(如 Pod 名 + Goroutine ID)、实际持有时长——没有可观测性,锁问题根本没法排查。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











