Javascript Node.js fs.writeFile()清空文件
我有一个更新方法,大约每16-40ms调用一次,里面有以下代码:Javascript Node.js fs.writeFile()清空文件,javascript,node.js,fs,Javascript,Node.js,Fs,我有一个更新方法,大约每16-40ms调用一次,里面有以下代码: this.fs.writeFile("./data.json", JSON.stringify({ totalPlayersOnline: this.totalPlayersOnline, previousDay: this.previousDay, gamesToday: this.gamesToday }), function (err) { if (err) { return
this.fs.writeFile("./data.json", JSON.stringify({
totalPlayersOnline: this.totalPlayersOnline,
previousDay: this.previousDay,
gamesToday: this.gamesToday
}), function (err) {
if (err) {
return console.log(err);
}
});
如果服务器抛出错误,“data.json”文件有时会变为空。如何防止这种情况发生?如果错误是由于输入错误(您要写入的数据)造成的,请确保数据正确无误,然后执行写入文件。 如果错误是由于writeFile失败引起的,即使输入正常,也可以检查函数是否已执行,直到文件写入为止。一种方法是使用async doWhilst函数
async.doWhilst(
writeFile(), //your function here but instead of err when fail callback success to loop again
check_if_file_null, //a function that checks that the file is not null
function (err) {
//here the file is not null
}
);
问题
这不是一个原子操作。下面是一个示例程序,我将在其上运行strace
:
#!/usr/bin/env node
const { writeFile, } = require('fs');
// nodejs won’t exit until the Promise completes.
new Promise(function (resolve, reject) {
writeFile('file.txt', 'content\n', function (err) {
if (err) {
reject(err);
} else {
resolve();
}
});
});
当我在strace-f
下运行它并整理输出以仅显示来自writeFile
操作()的系统调用时,我得到:
如您所见,writeFile
分三步完成
strace
的每个步骤都发生在不同的节点IO线程上。这对我来说意味着,fs.writeFile()
实际上可能在和方面实现。因此,nodejs不会将这个复杂的操作视为任何级别的原子操作,因为它不是原子操作。因此,如果您的节点进程在不等待操作完成的情况下终止,即使是正常终止,操作也可以在上述任何步骤中完成。在您的情况下,您会看到流程在writeFile()完成步骤1之后,但在完成步骤2之前退出
解决方案
用POSIX层以事务方式替换文件内容的常见模式是使用以下步骤:
将数据写入另一个名称不同的文件,即文件(请参阅中的“何时应该fsync?”),然后close()
it
(或者,在Windows上,)在要替换的文件上使用不同名称的文件
使用此算法,无论程序何时终止,都会更新或不更新目标文件。而且,更好的是,日志(现代)文件系统将确保,只要在继续执行步骤2之前在步骤1中对文件执行fsync()
,这两个操作将按顺序进行。也就是说,如果您的程序先执行步骤1,然后执行步骤2,但拔下插头,则在启动时,您会发现文件系统处于以下状态之一:
- 这两个步骤都没有完成。原始文件是完整的(或者如果它以前从未存在过,那么它就不存在)。替换文件要么不存在(执行
writeFile()
算法的步骤1,open()
,实际上从未成功),要么存在但为空(执行writeFile()
算法完成的步骤1),要么存在一些数据(执行writeFile()
算法部分完成的步骤2)
- 第一步完成了。原始文件是完整的(或者如果它以前不存在,现在仍然不存在)。替换文件已存在,包含所需的所有数据
- 这两个步骤都已完成。在原始文件的路径上,您现在可以访问所有替换数据,而不是空白文件。在第一步中写入替换数据的路径不再存在
使用此模式的代码可能如下所示:
const { writeFile, rename, } = require('fs');
function writeFileTransactional (path, content, cb) {
// The replacement file must be in the same directory as the
// destination because rename() does not work across device
// boundaries.
// This simple choice of replacement filename means that this
// function must never be called concurrently with itself for the
// same path value. Also, properly guarding against other
// processes trying to use the same temporary path would make this
// function more complicated. If that is a concern, a proper
// temporary file strategy should be used. However, this
// implementation ensures that any files left behind during an
//unclean termination will be cleaned up on a future run.
let temporaryPath = `${path}.new`;
writeFile(temporaryPath, content, function (err) {
if (err) {
return cb(err);
}
rename(temporaryPath, path, cb);
});
};
这与在任何语言/框架中解决同一问题所使用的解决方案基本相同。我没有用它运行一些真正的测试,我只是在手动重新加载ide时注意到,有时文件是空的。
我首先尝试的是重命名方法,并注意到了相同的问题,但重新创建一个新文件不太理想(考虑到文件监视等)
我的建议或我现在正在做的是在您自己的readFileSync中,我检查文件是否丢失或返回的数据是否为空,然后休眠100毫秒,然后再试一次。我认为一个第三次尝试会有更大的延迟,这真的会把西格玛推到一个缺口,但目前不会这么做,因为增加的延迟可能是不必要的负面影响(在这一点上会考虑一个承诺)。还有其他与您自己的代码相关的恢复选项机会,您可以添加这些选项,以防万一。文件未找到或为空?基本上是另一种重试方式
我的自定义writeFileSync添加了一个标志,可以在使用重命名方法(使用write sub dir.\u new“创建)或普通直接方法之间切换,因为代码的需要可能会有所不同。我的建议是根据文件大小来确定
在这个用例中,文件很小,一次只能由一个节点实例/服务器更新。我可以看到添加随机文件名作为另一个带有重命名的选项,以允许多台机器在需要时为以后编写另一个选项。也可能是重试限制参数
我还认为,您可以写入本地临时文件,然后通过某种方式复制到共享目标文件(也可以在目标文件上重命名以提高速度),当然,还可以进行清理(取消与本地临时文件的链接)。我猜这个想法有点像是把它推到shell命令上,所以不是更好。
无论如何,这里的主要思想仍然是,如果发现是空的,就读两遍。我确信它不会被部分写入,通过nodejs8+加载到一个共享的Ubuntu类型的NFS挂载上,对吗 一,。使用临时名称2写入新文件。将旧文件重命名为3。将新文件重命名为目标名称4。删除旧文件每次写入两次,第一次为
const { writeFile, rename, } = require('fs');
function writeFileTransactional (path, content, cb) {
// The replacement file must be in the same directory as the
// destination because rename() does not work across device
// boundaries.
// This simple choice of replacement filename means that this
// function must never be called concurrently with itself for the
// same path value. Also, properly guarding against other
// processes trying to use the same temporary path would make this
// function more complicated. If that is a concern, a proper
// temporary file strategy should be used. However, this
// implementation ensures that any files left behind during an
//unclean termination will be cleaned up on a future run.
let temporaryPath = `${path}.new`;
writeFile(temporaryPath, content, function (err) {
if (err) {
return cb(err);
}
rename(temporaryPath, path, cb);
});
};