写了一个 NodeJS 版的 fs-lockfile 用来管理应用层上的文件读取和写入。

这样应用内的所有磁盘 IO 都被应用程序统一接管,如果需要做比如权限管理、Bookmark、文件缓存、虚拟文件系统等,这些都可以做一个中间层,也有一定的使用场景。

设计接口

首先设计了六个方法,获取读锁、释放读锁、获取写锁、释放写锁、读文件、写文件。读文件里面封装了读锁的获取和释放,写文件里面封装了写锁的获取和释放。接口的方法如下:

  • obtainReadLock(filePath): Object
  • obtainWriteLock(filePath): Object
  • releaseReadLock(lockMeta): undefined
  • releaseWriteLock(lockMeta): undefined
  • read(filePath): Promise
  • write(filePath, content): Promise

读文件操作的方法: read

展示一下 read 简单封装了 obtainReadLock ,并且在执行读文件操作后,调用 releaseReadLock 释放读锁。

async function read(fp) {
    if (!path.isAbsolute(fp)) throw new Error('Must be an absolute path.')

    const lockMeta = await obtainReadLock(fp)

    try {
        return await fsp.readFile(fp)
    } finally {
        await releaseReadLock(lockMeta)
    }
}

简要介绍 obtainReadLock, obtainWriteLock 的执行流程

obtainReadLock

  • 获取 文件路径的 lockMeta ,然后要等待 lockMeta 中的写锁队列全部释放完成
  • 之后生成一个读锁,返回 lockMeta

obtainWriteLock

  • 获取文件路径的 lockMeta, 获取 lockMeta 中 writeQueue 最后一个写锁 prevLock。
  • 生成一个新的写锁,并且放进 lockMeta 中的 writeQueue
  • 等待所有读锁释放
  • 等待 prevLock 释放

注意:要先把新生成的写锁放在 writeQueue 中,因为其它方法可能会访问这个写队列

另外两个方法也是类似的,这样一个完整的应用层的锁就写好了。

锁是怎么生成的

是一个简单的 Promise 锁。构造一个方法,生成一个 Promise 实例,然后把里面的 resolve 方法引用到一个对象中,当需要释放锁的时候,调用 resolve 方法就能继续执行。如下面的例子:

function generateLock() {
    const lock = {
        obtain: null,
        release: null
    }
    const promise = new Promise(resolve => lock.release = resolve)
    lock.obtain = () => promise
    return lock
}

const lock = generateLock()
setTimeout(async () => {
    await lock.obtain()
    console.log('Till release method invoked, go there.')
}, 0)
setTimeout(() => lock.release(), 3000)

后续问题

这样的设计后,还是有两个扩展的问题

  • 读锁要不要直接访问之前的文件内容,这样让写锁能不与读锁互斥
  • 文件只有一个读锁,如果有一个 readQueue 存储多个读锁,可以对读锁增加更多的控制,但相应的复杂度就会增加不少

以及多线程操作的问题

  • 文件锁是基于线程的,如果是多线程或者多进程操作文件,最好是把所有的锁请求都通过 ipc 传递到一个线程中集中管理

多线程进程的解决方法也好处理:前文说了,read & write 封装是应对一般的需求,如果要做 ipc 通信,或者一些权限处理,或者缓存文件等,可以自行封装和完善。


这里是 npm package 的地址: fs-lockfile