redis单实例分布式锁必须用set key value nx px原子加锁+lua脚本校验value后删除,因setnx不带过期逻辑,分步设锁与过期易致死锁;解锁若非原子校验会误删他人锁;文件操作还需os.o_trunc、f.chmod、f.sync等系统级防护。

Redis 单实例 + SET key value NX PX 原子加锁 + Lua 安全解锁,配合文件系统级防护(os.O_TRUNC、f.Chmod、f.Sync),是 Golang 中实现分布式文件锁最务实、可落地的方案。etcd 更强但重,Redlock 过度设计且难兜底,不推荐在文件锁场景滥用。
为什么不能只用 SETNX 加锁
因为 SETNX 本身不带过期逻辑,单独调用后必须补 EXPIRE,而这两步非原子:进程在 SETNX 成功后、EXPIRE 前崩溃,key 就永远卡在 Redis 里,锁永久死锁。
- 必须用一条原子命令:
SET lock:file:config.json "a1b2c3" NX PX 15000(15 秒毫秒级 TTL) -
go-redis/v9的rdb.SetNX(ctx, key, value, ttl)底层会自动转为该命令;redigo 需手动拼Do("SET", key, value, "NX", "PX", 15000) - 返回
"OK"才算真正抢锁成功;nil或 error 表示失败,不能只靠 error 判断
解锁必须用 Lua 脚本校验 value
直接 DEL key 或先 GET 再 DEL 是高危操作——A 读到自己的 value,还没来得及删,B 已续上新锁,A 一删就把 B 的锁干掉了。
初始化并管理阿里云SDK Go 客户端,涵盖 sync.Once 单例、goroutine 安全、endpoint 与 region 配置、VPC 端点及调试。
- 固定解锁脚本(建议用
embed.FS管理):if redis.call("get", KEYS[1]) == ARGV[1] then return redis.call("del", KEYS[1]) else return 0 end - Go 中调用:
redis.NewScript(unlockScript).Run(ctx, rdb, []string{key}, clientID) - 返回
int64(1)表示成功;0表示锁已不属于你,此时应立即中止业务,而非重试
文件打开与写入时的本地防护要点
分布式锁只管“谁有权限改”,但多个进程仍可能同时 open 同一文件导致覆盖或权限绕过。必须叠加操作系统层控制。
- 写操作打开文件必须用:
os.O_RDWR | os.O_CREATE | os.O_TRUNC,确保清空再写 - 打开后立刻执行:
f.Chmod(0600),防止其他非锁进程通过宽松权限读写 - 务必检查
os.IsNotExist(err),而不是 panic——锁存在但文件被上游清理了,需重建 - 写完必须调
f.Sync(),避免 page cache 滞留导致另一进程读到脏数据
长任务必须配看门狗续期,且续期也要校验所有权
文件处理(如压缩、解析、生成报告)常超 10 秒,但锁 TTL 不能设太长(主从延迟、故障恢复慢),必须动态续期。
- 续期也必须用 Lua 脚本,校验
value后再EXPIRE:if redis.call("get", KEYS[1]) == ARGV[1] then return redis.call("expire", KEYS[1], ARGV[2]) else return 0 end - 续期间隔建议为 TTL / 3(如 TTL=15s,则每 5s 续一次);太密压 Redis,太疏易掉锁
- 业务函数
return前必须显式停止续期 goroutine,否则锁释放后还在后台续——变成“幽灵续期” - context 超时要设得比 TTL 长一点(比如 TTL=15s,context.WithTimeout 设 20s),防网络抖动误 cancel
真正容易被忽略的不是加锁逻辑,而是锁释放后的文件状态清理和续期 goroutine 的生命周期管理——它们不出错时一切正常,一出错就是数据错乱,且难以复现。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!










