用 gofrs/flock 为 nutsdb 实现 file lock 的过程记录

最近在 nutsdb v0.12.5 的发布计划中,有要实现 file lock 这一条,来保证只让开启 DB 的这个进程访问数据库文件夹,而其它的进程无法对该文件夹进行操作。

Elliot Chen 的建议下接下了这个任务,一开始觉得可能是要自己实现一个 file lock,但是在考察了其它的项目实现文件锁的方式后,发现了一个库:gofrs/flock,虽然 star 并不是很多,只有 400+,并且已经好几年没有更新过了,但是看了一眼有足足将近 1.2w 的使用者后,当即决定直接用这个库实现。

在 nutsdb 中使用 gofrs/flock

接下来我会详细介绍一下在 nutsdb 中是如何使用 gofrs/flock 的。

gofrs/flock 的接口介绍

这个库提供的接口相当简单易懂。 首先,通过flock包下的New函数创建一个*Flock,注意传进去的是需要上锁的文件路径,并且这个文件一定要是存在的,否则就会报错。

然后,可以调用LockTryLock方法进行上锁,在TryLock的注释中有些到:

TryLock is the preferred function for taking an exclusive file lock.

TryLock方法是一个非阻塞 方法,如果获取锁失败了会返回 false 而不会一直等待 file lock 被释放,所以我采用了TryLock来获取锁。

实际代码实现

在 nutsdb 打开的时候,获取文件锁即可。

go 复制代码
flock := flock.New(filepath.Join(opt.Dir, FLockName))
if ok, err := flock.TryLock(); err != nil {
    return nil, err
} else if !ok {
    return nil, ErrDirLocked
}

db.flock = flock

释放锁也只需要在 nutsdb 关闭的时候调用 Unlock就可以了。

go 复制代码
if !db.flock.Locked() {
    return ErrDirUnlocked
}

err = db.flock.Unlock()
if err != nil {
    return err
}

借助这个库来实现 file lock 还是很简单的,但是中间遇到了一些小问题。在 v0.12.4 的发布计划中提到了 nutsdb 当前的所有测试用例都是公用一个 DB 实例,所以有一些测试用例在打开 DB 实例后并没有调用关闭方法,所以会导致下一个测试用例执行失败,因为无法再次获取到文件锁了。

暂时是通过给所有测试用例都调用关闭方法来解决的,后续要通过重构测试用例彻底解决这个问题。

gofrs/flock 源码解析

仅仅会调接口当然是不行的 🤣,于是我详细看了一下源码来了解实现原理。这个库的代码非常精简,兼容了多个平台也就 1200+ 行代码,看起来也是不怎么费劲。

Windows 系统上的实现

在 Windows 上,gofrs/flock 使用了 kernel32.dll 中的 LockFileExUnlockFileEx 函数来实现文件锁定:

go 复制代码
var (
    kernel32, _         = syscall.LoadLibrary("kernel32.dll")
    procLockFileEx, _   = syscall.GetProcAddress(kernel32, "LockFileEx")
    procUnlockFileEx, _ = syscall.GetProcAddress(kernel32, "UnlockFileEx")
)

procLockFileExprocUnlockFileEx这两个变量是 LockFileExUnlockFileEx 系统调用函数的地址,这两个地址要通过使用syscall.GetProcAddresskernel32.dll中获取,因为在 Windows 系统上,LockFileExUnlockFileEx并不能直接通过syscall包下定义的函数来调用,而要先获取到地址,再使用syscall.syscall6来调用(这个 6 指的是系统调用的参数个数,不过类似于这样的函数已经全部过时了,应该统一使用 syscall.syscallN 来调用)。

那么在 Windows 系统上,是如何实现的共享锁和独占锁呢?是通过一些标志来确定的,在procLockFileEx函数的参数中需要填入这个文件锁对应的锁类型标志,这些标志也定义在flock_winapi.go中:

go 复制代码
const (
    winLockfileFailImmediately = 0x00000001
    winLockfileExclusiveLock   = 0x00000002
    winLockfileSharedLock      = 0x00000000
)

winLockfileExclusiveLockwinLockfileSharedLock分别表示要获取共享文件锁还是独占文件锁。只需要在获取锁时根据需要的类型将对应的flag传入系统调用中就可以了。

解锁也是一样,只需要调用procUnlockFileEx系统调用,只是少传一个flag参数,其余的使用跟procLockFileEx都是一样的。

gofrs/flock 分别为这两个系统调用封装成了lockFileExunlockFileEx函数来方便的加锁解锁。

类 Unix 系统上的实现

在 Unix 系统上,file lock 的实现就相对更加简单一点了,因为是可以直接通过syscall包下定义的Flock函数,根据给这个函数传入的第二个参数flag,来决定这个系统调用的具体行为:

go 复制代码
syscall.Flock(int(f.fh.Fd()), flag)

使用到了这些flag

go 复制代码
syscall.LOCK_EX // 获取独占锁
syscall.LOCK_SH // 获取共享锁
syscall.LOCK_UN // 解锁

需要注意的是,在某些类 UNIX 操作系统上,使用Lock方法可能会自动将共享锁替换为独占锁。在使用独占锁(Exclusive Locks)和共享锁(RLock())时要小心,因为调用 Unlock() 可能会意外释放曾经是共享锁的独占锁。

TryLock 的实现

gofrs/flock 有一个非常好的功能就是可以实现非阻塞的获取锁,一旦获取锁失败了就直接返回,那么这是怎么实现的呢?

在 Windows 上,try的实现要稍微简单一些,在调用lockFileEx方法的时候,多添加了一个之前列出来但没提到的标志:winLockfileFailImmediately

go 复制代码
_, errNo := lockFileEx(syscall.Handle(f.fh.Fd()), flag|winLockfileFailImmediately, 0, 1, 0, &syscall.Overlapped{})

有了这个标志就意味着非阻塞的尝试,如果 lockFileEx 返回特定的错误码,如 ErrorLockViolationsyscall.ERROR_IO_PENDING,则表示无法立即获得锁定,函数会立即返回,并返回 false 表示尝试失败。

go 复制代码
if errNo > 0 {
    if errNo == ErrorLockViolation || errNo == syscall.ERROR_IO_PENDING {
        return false, nil
    }

    return false, errNo
}

而在类 UNIX 系统上,在调用FLock时,加上一个标志位syscall.LOCK_NB,如果 syscall.Flock 返回 syscall.EWOULDBLOCK 错误,则表示无法立即获得锁定,函数会立即返回,并返回 false 表示尝试失败。

go 复制代码
err := syscall.Flock(int(f.fh.Fd()), flag|syscall.LOCK_NB)

不过在类 Unix 系统上还实现了一个reopenFDOnError方法,如果Flock返回的错误是syscall.EIOsyscall.EBADF,那么该方法会尝试重新打开文件描述符,并再次进行尝试。这是因为在某些情况下,文件描述符可能会在错误发生后被关闭,所以我们尝试重新打开文件描述符,确保可以继续尝试获取锁定。

这样 gofrs/flock 就实现了非阻塞的文件锁定。

总结

gofrs/flock 是一个功能强大且易于使用的 Go 语言库,用于实现文件锁定。它通过封装底层的系统调用,在不同的操作系统上提供了一致的接口,使得开发者可以轻松地确保多个进程对同一个文件的并发访问是安全的。无论是在 Linux/Unix 还是 Windows 等操作系统上,gofrs/flock 都为文件锁定提供了可靠的解决方案。

虽然不是自己从头开发的🤣,但也是为 nutsdb 完成了一个小目标,希望 nutsdb 可以越来越完善。

相关推荐
大气层煮月亮20 小时前
Oracle EBS ERP开发——报表生成Excel标准模板设计
数据库·oracle·excel
云和数据.ChenGuang20 小时前
达梦数据库的命名空间
数据库·oracle
三三木木七21 小时前
mysql拒绝连接
数据库·mysql
蹦跶的小羊羔21 小时前
sql数据库语法
数据库·sql
唐古乌梁海21 小时前
【mysql】InnoDB的聚簇索引和非聚簇索引工作原理
数据库·mysql
我变秃了也没变强21 小时前
pgsql配置密码复杂度策略
数据库·postgresql
PawSQL21 小时前
企业级SQL审核工具PawSQL介绍(1) - 六大核心能力
数据库·sql·oracle
幼稚园的山代王21 小时前
NoSQL介绍
数据库·nosql
猫林老师21 小时前
HarmonyOS线程模型与性能优化实战
数据库·分布式·harmonyos
沃达德软件21 小时前
视频图像数据库基础服务
数据库·图像处理·人工智能·计算机视觉·视觉检测