sync.mutex不能用于进程互斥,因其仅在单个go进程内存中生效,跨进程、跨机器时互不感知;真正跨进程互斥需依赖系统调用如flock或fcntl,分布式场景则必须使用redis等共享存储实现。

为什么 sync.Mutex 不能用于进程互斥
因为 sync.Mutex 只在单个 Go 进程的内存中生效。你起两个 go run main.go 实例,或部署到不同 Pod,它们各自持有一把完全独立的 sync.Mutex —— 彼此毫无感知,文件照样被并发写烂。
真正需要的是操作系统级的锁机制,让内核知道“这个文件正被某个进程占用”。跨进程互斥必须依赖系统调用,比如 Linux/macOS 的 flock(2) 或 fcntl(2)。
-
flock是文件描述符级、劝告锁,简单可靠,但只对“也遵守 flock”的进程起作用 -
fcntl支持字节范围锁,粒度更细,但实现复杂,且 NFS 上行为不一致 - Windows 没有
flock,得用LockFileEx,无法和 Unix 代码共用
flock 非阻塞加锁的正确写法
直接调 syscall.Flock(fd, syscall.LOCK_EX) 很容易卡死——它默认是阻塞的。生产环境几乎都该用非阻塞模式,否则一个锁不释放,整个 goroutine 就挂住。
关键三步:用 os.O_CREATE | os.O_RDWR 打开文件、加 syscall.LOCK_NB 标志、显式检查 syscall.EWOULDBLOCK 错误。
file, err := os.OpenFile("state.lock", os.O_CREATE|os.O_RDWR, 0644)
if err != nil {
log.Fatal(err)
}
defer file.Close()
err = syscall.Flock(int(file.Fd()), syscall.LOCK_EX|syscall.LOCK_NB)
if err != nil {
if err == syscall.EWOULDBLOCK {
log.Println("锁已被占用,跳过")
return
}
log.Fatal(err)
}
- 只读打开(
os.O_RDONLY)在某些内核下无法获得写锁,务必用os.O_RDWR - 锁绑定的是 fd,不是路径;同一文件多次
OpenFile会得到不同 fd,互不影响 - 进程退出时内核自动释放锁,但显式调
syscall.Flock(fd, syscall.LOCK_UN)更利于调试和资源清理
别用 os.O_EXCL 创建临时文件模拟锁
看似原子:os.OpenFile("lock", os.O_CREATE|os.O_EXCL|os.O_WRONLY, 0644)。实际在 NFS、容器挂载卷、或进程崩溃未清理时,会出问题。
典型故障现象:permission denied 却仍能创建成功、锁文件残留导致服务永远起不来、多个实例同时认为自己“抢到了锁”。
- NFSv3 及更早版本不保证
O_EXCL的跨节点原子性 - 锁文件被误删后,互斥完全失效,且无持有者信息可追溯
- 没有超时机制,崩溃进程留下的锁无法自动回收
- 相比
flock,它不提供任何锁状态查询或强制释放能力
分布式场景下,flock 完全不够用
如果你的服务跑在多台机器上,或用了 Kubernetes 多副本,flock 一丁点用都没有——它只在单机内核有效,跨机器之间零感知。
此时必须换外部协调服务:Redis 的 SET key value EX seconds NX 是最轻量的选择,但要注意 value 必须是唯一标识(如 uuid.NewString()),解锁必须用 Lua 脚本校验 value 后再 DEL,否则 A 加的锁可能被 B 误删。
-
flock解决不了“谁在写”,也解决不了“写到一半机器宕机”后的锁续期问题 - Redis 锁的过期时间不能拍脑袋设,建议 30–60 秒,并配后台 goroutine 定期
PEXPIRE续期 - 续期失败时不能静默忽略,必须主动
Unlock并报错,否则任务可能悬挂
真正难的从来不是“怎么加锁”,而是“怎么安全地持有、续期、识别持有者、异常清理”——这些细节漏掉任意一个,锁就从保险丝变成定时炸弹。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











