如何检测Go语言中通过flock加锁的文件是否处于锁定状态?
解决方案
1. 检测文件是否被加锁的实现方法
你使用的flock是内核维护的文件锁,天然支持非阻塞检测能力:持有锁的进程无论任何原因退出,内核都会自动释放该进程持有的所有flock锁,不存在死锁残留的问题。
你只需要在读取进程中对目标文件尝试加非阻塞共享锁(LOCK_SH | LOCK_NB)即可判断锁状态:
- 加锁成功:证明当前没有任何进程持有该文件的排他写锁,文件处于可用状态
- 加锁失败且错误为
syscall.EWOULDBLOCK:证明当前有活跃进程正在持锁写入,文件未完成
检测逻辑的示例代码如下:
// CheckFileLock 检测文件是否被加写锁,返回true代表被加锁,false代表无锁,error代表检测异常 func CheckFileLock(filePath string) (bool, error) { f, err := os.OpenFile(filePath, os.O_RDONLY, 0400) if err != nil { if os.IsNotExist(err) { return false, nil } return false, err } defer f.Close() // 尝试加非阻塞共享锁 err = syscall.Flock(int(f.Fd()), syscall.LOCK_SH|syscall.LOCK_NB) if err != nil { if err == syscall.EWOULDBLOCK { // 加锁失败,当前有排他写锁 return true, nil } return false, err } // 加锁成功,主动释放共享锁 _ = syscall.Flock(int(f.Fd()), syscall.LOCK_UN) return false, nil }
2. 现有实现的问题修复(匹配半成品文件检测需求)
你当前的实现存在两个逻辑漏洞,会导致无法正确识别异常退出产生的半成品文件:
- 直接对最终目标文件进行写入和加锁:如果写入进程异常退出,内核会自动释放锁,但半写入的文件还留在路径中,读取进程检测无锁后会把它当成完整文件处理
- 错误处理缺失:
downloadFile中OpenFile的错误被忽略,且io.Copy失败时没有主动删除半成品文件
推荐使用「临时文件+原子重命名」的模式修复,逻辑如下:
- 写入时先创建后缀为
.tmp的临时文件,对临时文件加写锁 - 所有数据写入完成后,原子重命名临时文件为最终的正式文件名
- 如果任何步骤出错,直接删除临时文件即可
该方案下只要正式文件名存在,就一定是已经完整写入的文件,不会出现半成品的问题,检测逻辑也更简单。
3. 修复后的完整示例代码
首先调整FileLock的逻辑,增加半成品文件清理方法:
type FileLock struct { filePath string f *os.File } func (l *FileLock) Lock() error { if l.f == nil { f, err := os.OpenFile(l.filePath, os.O_WRONLY|os.O_CREATE|os.O_EXCL, 0200) if err != nil { return err } l.f = f } err := syscall.Flock(int(l.f.Fd()), syscall.LOCK_EX|syscall.LOCK_NB) if err != nil { _ = l.f.Close() _ = os.Remove(l.filePath) return fmt.Errorf("cannot flock file %s - %s", l.filePath, err) } return nil } func (l *FileLock) Unlock() error { defer func() { _ = l.f.Close() }() return syscall.Flock(int(l.f.Fd()), syscall.LOCK_UN) } // Purge 清理锁关联的半成品文件 func (l *FileLock) Purge() error { _ = l.Unlock() return os.Remove(l.filePath) }
修改后的downloadFile逻辑:
func downloadFile(response *http.Response, finalPath string) error { // 先写临时文件 tmpPath := finalPath + ".tmp" output, err := os.OpenFile(tmpPath, os.O_WRONLY|os.O_CREATE|os.O_EXCL, 0600) if err != nil { return err } localFileLock := &FileLock{filePath: tmpPath, f: output} // 加写锁 if err := localFileLock.Lock(); err != nil { return err } // 写入完成前任何错误都清理临时文件 defer func() { if err := recover(); err != nil { _ = localFileLock.Purge() panic(err) } }() if _, err := io.Copy(output, response.Body); err != nil { _ = localFileLock.Purge() return fmt.Errorf("problem saving %s to %s: %s", response.Request.URL, finalPath, err) } // 刷盘保证数据写入磁盘 if err := output.Sync(); err != nil { _ = localFileLock.Purge() return err } // 解锁 if err := localFileLock.Unlock(); err != nil { _ = localFileLock.Purge() return err } // 原子重命名为正式文件,这一步之后文件才对外可见 return os.Rename(tmpPath, finalPath) }
调整后读取进程逻辑可以简化为:
- 只读取匹配最终格式、没有
.tmp后缀的文件即可 - 如果需要清理异常退出残留的半成品,用前面提供的
CheckFileLock方法检测.tmp文件是否被加锁,如果无锁就可以直接删除。
内容的提问来源于stack exchange,提问作者IsaIkari
相关产品推荐
相关产品推荐

