sync.rwmutex仅适用于单进程本地行锁,跨进程或多实例部署时完全失效;必须用redis(set nx+px原子加锁、lua校验token安全释放)或etcd(cas+lease)实现分布式行锁。

Go 中用 sync.RWMutex 实现本地行锁不适用于分布式场景
本地内存锁(如 sync.RWMutex 或 sync.Mutex)只能保护单进程内的并发访问,跨进程、跨机器时完全失效。如果你的应用已部署多实例(比如 Kubernetes 多 Pod、或多个服务器),直接套用 sync 包的锁会立刻出现数据竞争——看似加了锁,实际各实例各自为政,根本没互斥。
常见错误现象:UPDATE account SET balance = balance - 100 WHERE id = 123 在两个服务实例上同时执行,余额被扣两次;或者订单状态从 “pending” 误设为 “paid” 两次。
- 行锁粒度必须绑定具体业务主键(如
user_id、order_id),不能全局一把锁 - 锁生命周期需可控:不能死锁、不能无限期持有、失败时要自动释放
- 本地锁无法感知其他节点是否持有同 key 的锁,这是本质限制
Redis + Lua 是最实用的分布式行锁实现方案
Redis 单线程原子性 + Lua 脚本保证操作不可中断,是 Go 分布式行锁的事实标准。核心不是“用 Redis.Set”,而是用 SET key value NX PX timeout_ms 命令配合唯一 client token(如 UUID),再辅以 Lua 脚本做安全释放。
关键点在于:加锁必须原子(NX 防重入,PX 防死锁),删锁必须校验 token(防止 A 加锁、B 误删),且整个释放逻辑必须在 Lua 中完成。
- 加锁用
redis.Client.Set(ctx, lockKey, token, time.Second*10),但注意:Go redis 客户端(如github.com/redis/go-redis/v9)默认不带 NX/PX 组合,需显式传redis.SetOptions{NX: true, PX: 10000} - 释放锁必须用 Lua:
if redis.call("get", KEYS[1]) == ARGV[1] then return redis.call("del", KEYS[1]) else return 0 end,否则存在「检查+删除」竞态 - token 必须全局唯一且不可预测(推荐
uuid.NewString()),不能用固定字符串或 PID,否则多个 goroutine 可能共用同一 token 导致误删
封装成可复用的 LockProvider 接口比写死 Redis 更可靠
硬编码 Redis 连接或写一堆重复的 Lua 调用,会让模块难以测试和替换。应定义接口隔离实现细节,例如:
type LockProvider interface {
TryLock(ctx context.Context, key, token string, ttl time.Duration) (bool, error)
Unlock(ctx context.Context, key, token string) error
WithLock(ctx context.Context, key string, ttl time.Duration, fn func() error) error
}
这样既能 mock 测试(比如用内存 map 模拟锁),也能轻松切换底层(如未来换用 Etcd 的 txn),还能统一处理超时、重试、日志等横切逻辑。
-
WithLock方法封装了「加锁 → 执行 → 解锁」全流程,自动处理 defer 和 panic 恢复,避免忘记解锁 - TryLock 应返回
bool而非 error:锁不可用是正常业务分支(如返回 HTTP 409),不是异常 - 所有方法必须接收
context.Context,以便上游控制超时(比如 API 接口只愿等 500ms)
分布式锁的 timeout 设置远比想象中敏感
很多人设个 30 秒 TTL 图省事,结果导致:业务逻辑卡住(如下游 HTTP 超时)、锁过早释放(业务还没完锁就没了)、或锁长期滞留(GC 失败后 token 永久残留)。TTL 不是越长越安全。
正确做法是:TTL = 业务最大预期耗时 × 1.5~2,并配合主动续约(如 go routine 每 1/3 TTL 刷新一次)或更激进的 watchdog 检测。但多数简单场景,直接设为「明确上限 + 失败重试」更稳妥。
- 举例:转账操作预期 200ms 完成,设 TTL = 500ms,超时直接报错重试,而不是等锁自动过期
- 不要依赖 Redis 过期自动清理:网络分区时,客户端以为加锁成功,其实命令没到 Redis,此时 TTL 失效
- 如果业务本身有幂等性(如状态机 transition 带 version 检查),可降低对锁严格性的依赖,反而更健壮
真正难的不是写一个能加锁的函数,而是让锁在节点宕机、网络延迟、GC STW、时钟漂移下依然不破坏数据一致性。每层抽象(本地锁、Redis 锁、ZK/Etcd 锁)都在权衡可用性与正确性,选哪个取决于你的 SLA 和运维能力。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











