如何在异步写入文件时锁定文件

14
我有两个 Node 线程正在运行,一个负责监视目录以消费文件,另一个负责将文件写入指定的目录。通常它们不会在同一个目录上操作,但是在我正在处理的边缘情况下,它们会在同一个目录上操作。看起来消费应用程序正在在文件完全写入之前获取这些文件,导致文件损坏。是否有一种方法可以锁定文件直到写入完成?我已经研究了 lockfile 模块,但不幸的是我不认为它适用于此特定应用程序。
完整代码远不止这里所示,但要点如下: 1. 应用程序启动观察器和监听器 2. 监听器:监听将文件添加到 db,使用 fs.writeFile 导出它。 3. 观察者: - 观察者使用 chokidar 监视每个观察目录中添加的文件。 - 发现后,调用 fs.access 确保我们可以访问该文件。fs.access 看起来似乎不受文件正在被写入的影响。 - 通过 fs.createReadStream 消耗文件,然后将其发送到服务器。我们需要文件哈希值,因此需要文件流。 在这种情况下,文件被导出到观察目录,然后由监视进程重新导入。

1
这是两个独立的程序,还是只是两个不同的函数? - Datsik
1
虽然它是一个应用程序,但观察类并不知道其他类的存在。一旦完全实现,它们将被移动到生成的线程中。 - andrew.carpenter
1
如果您能提供一些代码,那就太好了。人们往往会对没有代码的东西进行投票否决。 - Datsik
3个回答

16

我会使用proper-lockfile来完成这个任务。你可以指定重试次数或使用重试配置对象来使用指数退避策略,这样你就可以处理两个进程需要同时修改同一个文件的情况。

下面是一个带有一些重试选项的简单示例:

const lockfile = require('proper-lockfile');
const Promise = require('bluebird');
const fs = require('fs-extra');
const crypto = require('crypto'); // random buffer contents

const retryOptions = {
    retries: {
        retries: 5,
        factor: 3,
        minTimeout: 1 * 1000,
        maxTimeout: 60 * 1000,
        randomize: true,
    }
};

let file;
let cleanup;
Promise.try(() => {
    file = '/var/tmp/file.txt';
    return fs.ensureFile(file); // fs-extra creates file if needed
}).then(() => {
    return lockfile.lock(file, retryOptions);
}).then(release => {
    cleanup = release;

    let buffer = crypto.randomBytes(4);
    let stream = fs.createWriteStream(file, {flags: 'a', encoding: 'binary'});
    stream.write(buffer);
    stream.end();

    return new Promise(function (resolve, reject) {
        stream.on('finish', () => resolve());
        stream.on('error', (err) => reject(err));
    });
}).then(() => {
    console.log('Finished!');
}).catch((err) => {
    console.error(err);
}).finally(() => {
    cleanup && cleanup();
});

1
优雅的解决方案:Promises链,谢谢。 - João Pimentel Ferreira

4
编写锁状态系统实际上非常简单。我找不到自己的代码在哪里,但是基本思路如下:
  1. 在获取锁时创建锁文件
  2. 解除锁定后删除锁文件
  3. 超时后删除锁文件
  4. 如果请求一个已经存在锁文件的文件的锁,则抛出异常
锁文件只是单个目录中的空文件。每个锁文件的名称都来自所表示文件的完整路径的哈希值。我使用了 MD5(相对较慢),但只要你确信对于路径字符串不存在冲突,任何哈希算法都应该可以。
这并不是100%线程安全的,因为(除非我漏掉了一些愚蠢的东西)在 Node 中无法原子性地检查文件是否存在并创建它,但在我的用例中,我持有锁定10秒或更长时间,所以微秒级竞争条件似乎不那么具有威胁性。如果您每秒持有和释放数千个同一文件的锁,则此竞争条件可能适用于您。
这些仅是建议性锁定,因此由您确保您的代码请求锁定并捕获预期的异常。

6
这几乎是正确的,但现在它完全错误了。锁文件不是目录中的空文件,而是目录中的符号链接(至少在类Unix的Linux、Mac等系统上)。这是因为像您所述,创建、检查、读取和删除文件时没有原子保证。但是,创建符号链接时有原子保证。因此,写入者和读取者都会创建锁定文件。写入者创建锁定文件来检查它是否正在被读取,并将其锁定。读取者创建锁定文件来检查并锁定它。 - slebetman
1
我正在查看lockfile模块,但我不清楚在先锁定文件后如何写入。似乎应该内置fs.writeFile - chovy
1
@Andrew,但是其他进程怎么办?它们不遵循整个“锁定过程”,直接开始向目录写入数据。 - Pacerier
1
@Pacerier,你的代码必须友好。对我来说,这意味着为应用程序中的每个fs-writing服务分配特定的目录或路径,并始终在服务需要写入时实现路径验证。如果LogService可以访问~/avatars/,它只能覆盖AvatarService的锁定文件,所以不要让它这样做。如果你在所有地方都保持一致,那么你只需要在需要的服务中实现锁定行为,就不用担心覆盖了。如果有两个服务可以写入同一个路径,而只有一个服务锁定了,你应该预料到会出现问题。 - Andrew
1
你可以使用 { flag: 'wx' } 写入文件,如果文件已经存在,则会返回一个带有 { code: 'EEXIST' } 的错误,这时你就知道锁无法被获取。如果我没记错的话,这个操作是原子性的。 - Thai

-1
重命名文件是原子性的。 使用特定名称(例如扩展名)编写文件,当编写完成且文件关闭时,将其重命名为另一个特定名称。仅查找具有第二个特定名称的文件。 或将文件重命名为另一个(子)目录中的文件。 唯一可能出现的问题是底层操作系统会暴露部分刷新但已关闭的文件,但这不太可能发生。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接