sync.Mutex在多实例部署下完全失效,因其仅作用于单机进程内,无法跨机器协调;需改用Redis的SET NX PX或Etcd的Lease+CompareAndSwap实现分布式锁。

为什么 sync.Mutex 在多实例部署下完全失效
单机进程内的 sync.Mutex 或 sync.RWMutex 对分布式系统毫无意义——当你的 Echo 服务横向扩到 3 台机器,每个请求可能落到任意一台,锁只在本机生效,其他实例照样并发写 DB 或改状态。这不是“锁得不够严”,而是根本不在一个锁域里。
真正需要的是跨进程、跨机器的协调机制,常见选型只有两类:基于 Redis 的 SET key value NX PX(推荐),或基于 Etcd 的 Lease + CompareAndSwap。前者轻量、响应快、运维成本低;后者强一致但引入新依赖,除非你已有 Etcd 生态,否则不建议为锁单独上。
- 别用 Redis
SETNX(已废弃),必须用带过期时间的SET key value NX PX 30000,避免死锁 - value 必须是唯一随机字符串(如 UUIDv4),不能写死,否则释放时无法校验所有权
- 释放锁必须用 Lua 脚本原子执行:
if redis.call("get",KEYS[1]) == ARGV[1] then return redis.call("del",KEYS[1]) else return 0 end
在 Echo 中封装可复用的 Redis 分布式锁中间件
不要在每个 handler 里手写 redis.Client.Set + redis.Client.Eval,容易漏释放、超时设置错、value 冲突。应该抽象成 Lock 和 Unlock 方法,并支持上下文超时控制。
示例结构:
初始化并管理阿里云SDK Go 客户端,涵盖 sync.Once 单例、goroutine 安全、endpoint 与 region 配置、VPC 端点及调试。
立即学习“go语言免费学习笔记(深入)”;
type RedisLock struct {
client *redis.Client
prefix string // 可选,避免 key 冲突,如 "lock:order:"
}
func (r *RedisLock) TryLock(ctx context.Context, key string, ttl time.Duration) (string, error) {
value := uuid.NewString()
// 注意:SET 命令必须用 ctx 控制网络超时,否则阻塞整个 handler
status := r.client.Set(ctx, r.prefix+key, value, ttl)
if status.Err() != nil {
return "", status.Err()
}
if status.Val() != "OK" {
return "", errors.New("lock failed")
}
return value, nil
}
func (r *RedisLock) Unlock(ctx context.Context, key, value string) error {
script := redis.NewScript(`if redis.call("get",KEYS[1]) == ARGV[1] then return redis.call("del",KEYS[1]) else return 0 end`)
result, err := script.Run(ctx, r.client, []string{r.prefix + key}, value).Int64()
if err != nil {
return err
}
if result == 0 {
return errors.New("unlock failed: key not owned")
}
return nil
}
- 务必传入
ctx到所有 Redis 操作,防止锁获取卡住导致 handler hang 住 -
TryLock返回的value必须原样传给Unlock,不可缓存或共享,每个锁实例独立 - 不建议在 middleware 中自动加锁(比如按 path 加),因为锁粒度和业务逻辑强耦合,应在具体 handler 内按需调用
典型错误:用错 key 导致锁失效或误杀
最常犯的错误是把锁 key 写成固定字符串,比如 "order_process",结果所有订单都抢同一个锁,吞吐归零;或者 key 拼错字段,比如用 req.ID 却没校验非空,导致空字符串 key 被反复覆盖。
- key 应该携带业务唯一标识:订单场景用
"order:" + orderID,库存扣减用"stock:" + skuID - 避免在 key 中拼接用户输入未过滤的内容,防止 Redis 注入(虽然概率低,但 key 名含空格或控制字符会破坏协议)
- 别在锁内做耗时操作:DB 查询、HTTP 调用、大文件读写——锁持有时间越长,争抢越激烈,失败率越高;应只锁「临界区」,即真正修改共享状态的几行代码
- 锁超时时间(
PX)必须明显大于业务最大执行时间(建议 ×2~3),但也不能设成几小时,否则故障时锁残留太久
如何验证锁是否真的生效
光看日志“lock acquired”没用。要实测并发冲突是否被拦截,最简单方式是写个压测脚本,同时发 50 个相同订单 ID 的请求,观察 DB 中对应记录是否只有一条 insert / update 生效。
- 在锁获取后、业务逻辑前加
log.Printf("acquired lock for %s", key),并在 unlock 后打释放日志,对比时间戳确认无重叠 - 用
redis-cli monitor实时看 SET / EVAL 命令是否按预期发出,尤其注意 NX 和 PX 参数是否出现 - 故意让某个 handler panic,在 defer 中调用
Unlock,验证锁是否能及时释放(否则靠 TTL 过期,延迟不可控)
锁的可靠性不取决于代码写了多少行,而在于 value 唯一性、释放原子性、超时合理性这三个点是否全部守住。少一个,就等于没锁。

















