← 中文资源

简体中文版本

文件锁互斥保护

用非阻塞独占文件锁保护临界区,拒绝第二个持有者,并在释放后重新获取。

阅读英文原文

功能说明

定时任务、数据迁移和单实例守护进程需要保证同一时刻只有一个执行者。文件锁是一种轻量方案:进程打开锁文件并申请独占锁,成功者继续,失败者立即退出。锁随文件描述符存在,进程崩溃时由内核自动释放,不会留下永久死锁。

示例先取得锁,再在仍持锁时尝试第二次获取,预期被拒绝;释放后重新获取成功。withLock 辅助函数用 defer 保证释放,适合包裹实际工作。

关键代码

OpenFile 以创建加读写模式打开锁文件,权限为零六零零,避免其他用户操作。syscall.Flock 的 LOCK_EX 表示独占,LOCK_NB 表示不等待,被占用时立即返回错误;失败分支先关闭文件避免描述符泄漏。Release 先显式解锁再关闭,两步错误都会返回。withLock 在函数返回时调用 Release,即使 work 返回错误也能解锁。

使用注意

flock 只在同一台主机内有效,跨容器或多副本部署需要分布式锁。NFS 等网络文件系统对 flock 支持不一致,使用前应确认。锁文件自身不要删除,删除后新进程会创建新文件并绕过旧锁。本示例依赖 Unix 系统调用,Windows 需要改用 LockFileEx。锁不提供公平队列,第二个进程直接失败,业务上应配合重试与退避。

验证方式

第一次获取成功并打印锁文件名。第二次获取在同一个进程中用新的描述符打开同一文件,也会被拒绝,输出提示为 true。释放后重新获取成功,输出为 true。可以同时运行两个进程,观察只有一个能进入临界区,另一个打印锁被占用。

代码示例

package main

import (
	"fmt"
	"os"
	"path/filepath"
	"syscall"
)

type FileLock struct {
	file *os.File
	path string
}

func AcquireFileLock(path string) (*FileLock, error) {
	file, err := os.OpenFile(path, os.O_CREATE|os.O_RDWR, 0o600)
	if err != nil {
		return nil, fmt.Errorf("open lock file: %w", err)
	}
	if err := syscall.Flock(int(file.Fd()), syscall.LOCK_EX|syscall.LOCK_NB); err != nil {
		file.Close()
		return nil, fmt.Errorf("lock held elsewhere: %w", err)
	}
	return &FileLock{file: file, path: path}, nil
}

func (l *FileLock) Release() error {
	if err := syscall.Flock(int(l.file.Fd()), syscall.LOCK_UN); err != nil {
		l.file.Close()
		return fmt.Errorf("unlock: %w", err)
	}
	return l.file.Close()
}

func withLock(path string, work func() error) error {
	lock, err := AcquireFileLock(path)
	if err != nil {
		return err
	}
	defer lock.Release()
	return work()
}

func main() {
	path := filepath.Join(os.TempDir(), "devexamples-file-lock.lock")
	lock, err := AcquireFileLock(path)
	if err != nil {
		fmt.Println("first acquire failed:", err)
		return
	}
	fmt.Println("locked:", filepath.Base(lock.path))

	if _, err := AcquireFileLock(path); err != nil {
		fmt.Println("second acquire rejected:", err != nil)
	}

	if err := lock.Release(); err != nil {
		fmt.Println("release failed:", err)
	}
	reopened, err := AcquireFileLock(path)
	fmt.Println("reacquired:", err == nil)
	if err == nil {
		_ = reopened.Release()
	}
}