我正在node.js中制作一个Discord机器人,需要存储一些每个公会的数据。我想使用内置的fs/promises
将其存储在data/<guild.id>.json
中的单独JSON文件中。
我有一个函数setGuildData(guildID: string, dataToAssign: object)
,它读取当前的JSON文件,将JSON解析为fileData
,然后使用Object.assign()
分配新数据,最终字符串化JSON并覆盖文件。
但我担心,如果两个异步函数调用同时试图编辑这个JSON文件,它们会干扰:
file: {a:0, b:1}
call 1: read the file
call 2: read the file
call 1: edit the object to insert {c:2}
call 2: edit the object to insert {d:3}
call 1: stringify and write {a:0, b:1, c:2}
call 2 stringify and write {a:0, b:1, d:3}
file: {a:0, b:1, d:3}
我想做的是,当call1读取JSON时,它会阻止对该文件的任何其他读取调用,直到它完成。这将导致调用2读取{a:0, b:1, c:2}
,并将{d:3}
添加到其中,从而得到所需的{a:0, b:1, c:2, d:3}
结果
所以我的问题是:
我如何打开一个文件,这样我就可以读取它,处理数据,然后覆盖它,同时阻止其他异步调用打开文件
阻止这一个文件长达100毫秒的性能影响并不坏,因为这个文件只适用于那一个公会,我预计在高峰期每个公会每分钟只会看到几个请求,很少会同时看到两个请求。我只是想安全起见,不要像示例中{c:2}
那样意外删除任何数据。
我可以在JS中创建一个全局队列来防止双重文件写入,但我更希望有一种操作系统级别的方法来临时阻止这个文件。如果这是特定于操作系统的,我将使用Linux。
这是我现在拥有的:
const fsPromises = require('node:fs/promises');
const path = require('node:path')
const dataPath = path.join(__dirname, 'data');
async function setGuildData(guildID, newData) {
let fileHandle = await fsPromises.open(path.join(dataPath, `${guildID}.json`), "w+"); // open the file. note 1
let fileData; // make a variable for the file data
try {
fileData = JSON.parse(await fileHandle.readFile()); // try to read and parse the file.
} catch (err) {
fileData = {}; // if that doesn't work, assume the file was not made yet and return an empty object
// yes I tried logging the error, see note 1
}
await fileHandle.write(JSON.stringify(Object.assign(fileData, newData)), 0); // use Object.assign to overwrite some properties or create new ones, and write this to the file
await fileHandle.close(); // close the filehandle.
}
注1:"w+"
模式似乎在打开时清除文件。我想要一种模式,允许我先读取然后覆盖文件,同时保持文件打开,以防止其他异步调用干扰。
对于Linux,实际上没有任何方法可以阻止这样的文件,但您可以使用Promises来创建原子保护。这是一个队列系统,您可以使用它对任何文件编辑请求进行排队。
下面是一个例子,如果不使用makeAtomic,答案将是1,而不是2。
const sleep = ms => new Promise(r => setTimeout(r, ms));
function makeAtomic() {
let prom = Promise.resolve(null);
function run(cb) {
const oProm = prom;
prom = (async () => {
try {
await oProm;
} finally {
return cb();
}
})();
return prom;
};
return run;
}
const atom = makeAtomic();
let v = 0;
async function addOne() {
return atom(async c => {
let ov = v;
await sleep(200);
v = ov + 1;
});
}
async function test() {
await Promise.all([addOne(), addOne()]);
console.log(v);
}
test().catch(e => console.error(e));
注意:你基本上可以与代码的其他部分共享原子承诺,如果序列化很重要,它不一定是同一个代码,如果你想同时执行多个操作,那么它很方便,当然你可以有多个makeAtomics,这样做可以使并行操作非常简单,当然也可以避免异步编码中容易出现的竞争条件。