使用Golang实现文件锁_防止多进程并发写入冲突

go中跨进程文件锁最可靠方式是用flock(linux/macos)或lockfileex(windows),需基于os.file.fd()对已打开文件加锁,推荐使用github.com/nightlyone/lockfile包自动处理跨平台差异与锁生命周期。

使用Golang实现文件锁_防止多进程并发写入冲突

Go 中用 flock 实现跨进程文件锁最可靠

标准库 sync.Mutex 只作用于单个进程内,多进程同时写同一个文件时完全无效。真正起作用的是系统级的 flock(Linux/macOS)或 LockFileEx(Windows),Go 通过 syscall 或封装好的第三方包调用它们。os.File.Fd() 是关键入口——必须基于打开的文件描述符加锁,不能对路径字符串操作。

常见错误是:先 os.OpenFile 再用路径去调 flock,结果锁的是另一个文件实例;或者忘记在 deferUnlock,导致锁残留、后续进程死等。

  • 只对已打开的 *os.File 调用 Lock/Unlock,不是对文件名
  • 加锁前确保文件以读写模式打开(os.O_RDWR),只读打开可能被某些系统拒绝加锁
  • Windows 下 flock 不可用,需用 golang.org/x/sys/windowsLockFileEx,或直接换用兼容包
  • 锁是建议性(advisory)的,所有参与进程都得主动检查并遵守,强制性锁(mandatory)在 Linux 上默认关闭且不推荐启用

github.com/nightlyone/lockfile 避开 syscall 差异

自己手撸 syscall 跨平台适配太容易翻车:比如 macOS 的 F_SETLK 和 Linux 行为略有差异,Windows 完全另一套 API。直接用成熟封装包更省心,lockfile 包就是专干这事的——它内部自动选 syscall、处理 EBUSY、支持超时,还带 .lock 文件约定。

典型误用是把锁文件路径和业务文件路径搞混。这个包默认在目标路径后加 .lock 后缀,比如传 /data/config.json,它实际锁的是 /data/config.json.lock。如果你手动创建了同名文件,反而会干扰它。

立即学习go语言免费学习笔记(深入)”;

  • 初始化:lf, err := lockfile.New("/path/to/file"),传入的是业务文件路径,不是锁文件路径
  • 阻塞加锁:err := lf.Lock();非阻塞尝试:err := lf.TryLock()(返回 lockfile.ErrBusy
  • 务必调用 lf.Unlock(),它会删掉 .lock 文件;程序 panic 时记得用 defer lf.Unlock()
  • 该包不持有文件句柄,加锁后你仍需自己用 os.OpenFile 操作原文件

os.Chmodos.Rename 可能绕过文件锁

文件锁只对「打开-读写-关闭」流程生效。但 Go 常见写法是「写临时文件 → os.Rename 替换原文件」,这招很安全,可它完全不经过原文件的 fd,flock 压根管不了。同样,os.Chmodos.Remove 这些元数据操作也不触发锁检查。

所以别以为加了锁就万事大吉。如果业务逻辑包含原子替换(如配置热更新)、权限修改或删除重建,锁只保护了「正在写」的那个瞬间,前后动作仍是裸奔状态。

  • 若必须原子替换,锁对象应是临时文件所在目录(用 flock 锁目录 fd),或改用信号+进程协调
  • os.Rename 在同文件系统内是原子的,但不保证锁同步;跨文件系统会变成 copy+remove,风险更高
  • 想锁整个操作流程,得把锁范围扩大到包含 rename 前后的所有步骤,而不是只锁 open 的那一刻

并发量大时 flock 性能不是瓶颈,但锁粒度要小心

flock 本身很快,微秒级,不会成为性能热点。真正卡住的是锁竞争:比如所有 worker 进程都去抢同一个日志文件的锁,结果串行写,吞吐归零。这时候不是锁的问题,是设计问题。

容易忽略的一点是:锁的生命周期和业务逻辑耦合太紧。例如一个 HTTP handler 里加锁写文件,但 handler 耗时主要在外部 API 调用,锁却一直占着,白白拖慢其他请求。

  • 锁的范围尽量窄——只包住 Write + Sync,别裹进网络请求、数据库查询
  • 高频写场景优先考虑分片:按日期生成日志文件,或用 hash(key) % N 分散到多个锁文件
  • 不要用全局锁保护多个无关文件;每个文件独立锁,避免不必要阻塞
  • fsync 是耗时大户,如果允许丢少量日志,可关掉 File.Sync(),靠 OS 缓冲

锁机制本身很轻量,麻烦永远出在“以为锁住了就安全”——而忘了锁之外的动作、锁的范围、以及所有参与者是否真的在配合。