flock 在 nfs 上不可靠,因 nfsv3 不转发锁调用,nfsv4 锁机制常被禁用或配置不一致,导致跨主机加锁无互斥;应改用原子 lockfile+租约或 redis/etcd 分布式锁。

跨主机共享文件系统(如 NFS、CIFS、GlusterFS)上,syscall.Flock 和 os.File.Lock 基本失效,不能用于进程间互斥 —— 这不是 Go 的 bug,而是底层文件系统协议不保证锁的跨节点语义。
为什么 flock 在 NFS 上根本不可靠
flock 是内核级的 advisory lock,依赖本地 VFS 层维护锁状态;NFSv3 及更早版本不转发 flock 调用,NFSv4 虽支持部分锁协议(如 nlm),但实际部署中常被禁用或配置不一致。结果就是:两个挂载同一 NFS 目录的主机,flock 可能同时返回成功,完全失去互斥能力。
- 现象:A 主机加锁成功后,B 主机调用
syscall.Flock(fd, syscall.LOCK_EX|syscall.LOCK_NB)仍返回nil - 原因:NFS 客户端把锁当成本地缓存操作,服务端不参与协调
- 验证方式:在两台机器上分别运行带
flock的程序,写同一个文件,观察是否出现数据覆盖
替代方案:基于文件原子性的 lockfile + 租约机制
唯一可落地的跨主机文件锁,是利用 os.OpenFile(path, os.O_CREATE|os.O_EXCL|os.O_WRONLY, 0644) 的原子性创建锁文件,并配合租约(lease)防止死锁。
- 关键点:必须用
O_EXCL,不能先os.Stat再os.Create(竞态窗口) - 锁文件内容建议写入:
hostname+pid+unix timestamp+random nonce,便于 debug 和清理 - 必须设置租约超时(例如 30 秒),并在持有锁期间定期
touch锁文件或更新时间戳,否则其他节点可判定为“僵死锁”并强删 - 解锁时不是删除锁文件,而是先
os.WriteFile标记状态(如写released),再os.Remove,避免删错别人持有的锁
如何检测并安全清理陈旧锁文件
跨主机场景下,进程崩溃或网络分区会导致锁文件残留,必须主动识别和回收。
- 读取锁文件内容,提取
pid和hostname;若hostname不可达(ping -c1或 DNS 查询失败),或pid在对应主机上已不存在(SSH 执行kill -0 $pid),则视为可清理 - 不要直接
os.Remove:先尝试os.Rename(lockfile, lockfile+".stale"),成功后再删,避免误删刚创建的新锁 - 加锁前做一次“卫生检查”:扫描所有
*.lock文件,对超时未更新的发起清理流程,但需加全局竞争保护(比如用一个固定路径的主锁文件控制清理入口)
分布式锁才是正解,别硬扛文件系统
如果你的业务逻辑真正需要跨主机强一致性,就别在共享文件系统上折腾锁了 —— 直接上 Redis 的 SET key value NX EX seconds 或 etcd 的 lease + compare-and-swap。它们提供原子性、租约、watch 机制,且性能远优于反复轮询文件系统。
- Redis 方案:用
github.com/go-redis/redis/v9的SetNX方法,value 设为随机 token,释放时用 Lua 脚本校验 token 后 del - etcd 方案:用
clientv3.LeaseGrant创建 lease,clientv3.KV.Put时绑定 lease ID,自动过期 - 注意:文件锁只适合“单机多进程 + 共享存储”的弱一致性场景;跨主机高可靠需求,请接受“引入外部依赖”的事实
最易被忽略的一点:即使你实现了完美的 lockfile 租约,NFS 缓存延迟也可能导致锁状态在不同客户端视图不一致 —— 所以任何基于共享文件系统的方案,都必须容忍短暂的“双主”窗口。真要零容忍,只能切分布式协调服务。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











