redlock通过在n个独立redis实例上执行多数派(n/2+1)加锁并校验总耗时小于锁过期时间来实现高可用分布式锁,其核心是避免单点故障和主从异步复制导致的锁丢失,释放锁时需用lua脚本原子性校验value后删除。

为什么不能直接封装 sync.Mutex 到模块里
因为 sync.Mutex 只在单进程内存生效,你把它包进一个 go module 里,10 个服务实例各跑一个副本,每个都拿到自己的 Mutex,锁形同虚设——DB 还是会被并发写、定时任务照样重复触发。模块封装的是逻辑,不是物理隔离,跨进程互斥必须靠外部存储的原子能力。
Redis 方案:用 SETNX + Lua 封装成可复用的 Lock 结构体
最轻量、上线率最高的做法是把加锁、续期、释放收进一个结构体,而不是暴露零散函数。关键点不在“能不能”,而在“怎么防踩坑”:
-
Lock实例必须持有唯一标识(如uuid.NewString()),不能复用固定字符串,否则解锁时会误删别人锁 - 加锁必须用
rdb.SetNX(ctx, key, value, ttl),禁用SET + EXPIRE两步写法——中间崩溃就死锁 - 解锁必须走 Lua 脚本:
if redis.call("get", KEYS[1]) == ARGV[1] then return redis.call("del", KEYS[1]) else return 0 end,否则非原子操作等于裸奔 - 自动续期要另起 goroutine,每
ttl/3轮询一次,且每次续期前用EVAL校验当前锁是否仍属自己(脚本返回 1 才续) - 模块导出的
TryLock方法应返回(bool, error),调用方必须检查布尔值,不能只看err == nil
etcd 方案:用 Txn + Lease 封装更稳的租约锁
如果你的业务不允许任何“短暂双持锁”(比如扣款、库存预占),etcd 的线性一致性比 Redis 更可靠。封装时注意:
Go 配置库,使用 spf13/viper — 分层优先级(flag > env >file > KV > default),提供 BindPFlag/BindPFlags、SetEnvPrefix + SetEnvKeyReplace 等功能。
- 加锁必须用
client.Txn(ctx).If(clientv3.Compare(clientv3.CreateRevision(key), "=", 0)).Then(clientv3.OpPut(key, value, clientv3.WithLease(leaseID))).Commit(),缺了Compare就不是 CAS - lease 必须由
client.Grant(ctx, ttl)申请,不能自己生成时间戳或硬编码 ID;续租要用client.KeepAliveOnce(ctx, leaseID),失败需主动Revoke - 解锁就是
client.Delete(ctx, key),但得确保上下文带超时(context.WithTimeout(ctx, 2*time.Second)),避免阻塞 - 模块应提供
WithKeyPrefix("/locks/")选项,防止业务 key 和锁 key 冲突 - etcd v3 的
context.Context超时比 Redis 更敏感,网络抖动容易触发context.DeadlineExceeded,错误处理不能只打印日志
Redlock 模块封装:别为“看起来高大上”引入复杂度
redlock-go 看似开箱即用,但封装进模块前得先问自己三个问题:
- 你的 Redis 实例是否真正物理隔离?
Endpoints里填的是同一集群的主从地址,等于没用多数派 - 业务执行时间是否稳定?如果波动大,
ttl设短了锁提前丢,设长了故障后释放慢——此时不如用带续期的单节点 Redis 锁 - 是否真需要容忍 Redis 主从切换?大部分场景下,用 Redis Sentinel 或 Cluster 已足够,Redlock 带来的延迟和运维成本常被低估
- 调用
dl.Lock("key", ttl)后必须立即defer lock.Unlock(ctx),且ctx要带明确 timeout,否则Unlock()在某节点不可达时会永久阻塞 goroutine
真正容易被忽略的是时钟漂移——Redlock 依赖各节点本地时间对齐,NTP 同步不到位时,“总耗时小于锁过期时间”这个前提就崩了。生产环境不建议默认启用 Redlock,除非你已监控并压测过所有 Redis 节点的时钟偏差。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!










