flock是最轻量的单例保障手段,因其是内核级文件锁、无需外部依赖、自动释放且原子性强;但要求锁文件指向同一inode、不支持windows、需手动调用syscall实现,并注意nfs等不支持场景。

为什么 flock 是最轻量的单例保障手段
Go 本身不提供跨进程的锁原语,flock 是 Linux/Unix 系统级文件锁,内核保证原子性,且无需额外服务依赖。它比 Redis 锁、数据库行锁更轻,也比基于 PID 文件的手动校验更可靠——后者在进程崩溃后残留 PID 文件就会导致死锁。
关键点:锁必须作用于同一个 inode(即同一物理文件),不能是软链接或不同路径下的同名文件;锁在进程退出时自动释放,但若程序 panic 未正常 close 文件描述符,锁可能滞留(实际中极少发生,因内核会回收 fd)。
-
flock是建议性锁,所有参与者必须主动调用才能生效,无法阻止恶意绕过 - Windows 不支持
flock,需改用syscall.LockFileEx或第三方库如github.com/jacobsa/fuse(不推荐) - Go 标准库无直接封装,需通过
syscall或golang.org/x/sys/unix调用
如何用 unix.Flock 实现带超时的锁获取
定时任务常运行在 cron 或 systemd timer 下,启动时需快速判断是否已有实例在跑。直接阻塞等待不可取,应设超时并失败退出。
示例逻辑:打开锁文件(/tmp/myjob.lock),尝试非阻塞加锁,失败则直接 return;成功则 defer 解锁并执行任务:
file, err := os.OpenFile("/tmp/myjob.lock", os.O_CREATE|os.O_RDWR, 0644)
if err != nil {
log.Fatal(err)
}
defer file.Close()
if err := unix.Flock(int(file.Fd()), unix.LOCK_EX|unix.LOCK_NB); err != nil {
if err == unix.EWOULDBLOCK {
log.Println("another instance is running, exit")
return
}
log.Fatal(err)
}
defer unix.Flock(int(file.Fd()), unix.LOCK_UN)
注意:unix.LOCK_NB 表示非阻塞,unix.LOCK_EX 是排他锁。不要用 os.Chmod 改锁文件权限——flock 不关心权限,只认 inode。
定时任务中锁文件路径选哪里最稳妥
锁文件位置直接影响可靠性。常见错误是写入 /tmp 下但没处理系统自动清理,或写入用户 home 目录却忽略多用户场景。
- 推荐路径:
/var/run/myapp/jobname.lock(需 root 权限)或/tmp/myapp-jobname.lock - 避免使用
os.TempDir()返回的路径,它可能指向 RAM-based tmpfs,重启即清空,但锁文件本就不该持久化 - 若任务由 systemd 启动,可用
%t占位符(对应/run),并在 service 文件里配RuntimeDirectory=myapp自动创建目录 - 绝对不要用当前工作目录(
.)或相对路径,cron 默认 cwd 是 root home,易混乱
为什么不能只靠 os.Pid 文件 + 进程检查
有人用写 PID 文件 + kill -0 检查进程是否存在来模拟单例,这有三个硬伤:
- PID 可能被复用:旧进程退出后,新进程恰好拿到相同 PID,
kill -0返回 true,误判为还在运行 - 权限问题:非 root 用户无法对其他用户的进程发信号,
kill -0失败不等于进程不存在 - 竞态窗口:检查 PID 存在 → 启动任务之间存在时间差,两个实例仍可能同时进入临界区
flock 的加锁操作是原子的,没有中间状态。只要所有实例都走同一套锁逻辑,就不可能出现两个同时持锁的情况——这才是单例的本质保障。
真正麻烦的是锁文件所在文件系统不支持 flock(比如某些 NFS 配置、overlayfs 容器层),这时得退回到基于分布式协调服务的方案,但那已超出“单机定时任务”的范畴了。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











