因为sync.Mutex仅限单进程内生效,而Gin多worker并发时锁失效,导致签到重复;Redis+SETNX通过NX/PX保证互斥与自动过期,配合UUID+Lua脚本实现“谁加锁谁解锁”,兼顾轻量、可靠与原子性。

为什么不能直接用 sync.Mutex 做签到锁
因为 sync.Mutex 只在单进程内有效,Gin 启动多个 worker(比如用 gin.Default().Run("0.0.0.0:8080") 并发处理请求)时,不同 goroutine 可能跑在不同 OS 线程甚至不同机器上——锁完全失效。用户并发签到会出现重复计数、重复发奖励等数据错乱。
Redis + SETNX 是最轻量可靠的选型
分布式锁核心要满足:互斥、防死锁、解铃还须系铃人(谁加锁谁删锁)。Redis 的 SET key value NX PX 30000 天然支持原子性加锁和自动过期,比 ZooKeeper 或 etcd 更适合签到这种短时、高频、无强一致性要求的场景。
-
NX保证只在 key 不存在时设置成功,实现互斥 -
PX 30000设置 30 秒过期,避免服务崩溃导致锁永久残留 - value 必须是唯一标识(如 UUID),后续删锁时用
EVAL脚本校验再删,防止误删他人锁
示例加锁逻辑(Go):
lockKey := "sign:" + userID
lockValue := uuid.New().String()
ok, err := rdb.Set(ctx, lockKey, lockValue, 30*time.Second).Result()
if err != nil || ok != "OK" {
return errors.New("failed to acquire lock")
}
Gin 中如何安全嵌入锁逻辑而不阻塞 HTTP
别在 handler 里裸写 Redis 操作——超时、重试、上下文取消都要考虑。推荐封装成中间件或独立 service 方法,且必须传入 ctx 并设超时。
使用 uber-go/dig 在 Golang 中实现依赖注入——基于反射容器,提供 Provide/Invoke、dig.In/dig.Out 参数与结果对象,支持命名依赖等
立即学习“go语言免费学习笔记(深入)”;
- 用
context.WithTimeout(ctx, 2*time.Second)防止锁等待拖垮整个请求 - 加锁失败直接返回
http.StatusTooManyRequests或友好提示,不要重试(签到是幂等操作,前端可退避重试) - 解锁必须放在
defer里,但注意:不能 defer 调用普通DEL,要用 Lua 脚本确保原子性
解锁脚本示例:
const unlockScript = `
if redis.call("get", KEYS[1]) == ARGV[1] then
return redis.call("del", KEYS[1])
else
return 0
end`
签到业务逻辑里漏掉这步,锁就形同虚设
加锁只是第一步,真正关键的是:锁保护的代码块必须严格包裹「查是否已签到 → 若未签到则写入 → 更新统计」整段逻辑。常见错误是只锁写入,没锁查询,导致两个请求同时查到“未签到”,然后都写入。
- 必须把
GET sign_record:{userID}和SET sign_record:{userID} 1 EX 86400放在同一把锁下 - 统计类字段(如总签到天数)建议用 Redis
INCR,它本身是原子的,无需额外加锁 - 如果用了 MySQL 存签到记录,锁 key 建议包含日期(如
sign:20240520:uid123),避免单日锁竞争过大
锁粒度太粗(比如全站一把锁)会扼杀并发,太细(比如每秒一个 key)又增加 Redis 压力——按 userID + date 分片是最实用的平衡点。

















