如何处理多个异步操作同时删除同一文件的并发冲突?
你不需要强制使用同步版本的文件操作API,当前问题的核心是异步并发执行时没有做互斥控制,而非异步API本身的缺陷。
首先纠正一个常见误区:就算你把所有文件操作都换成同步版本,也解决不了当前的并发问题。同步API只是把单个函数内的文件操作变成阻塞连续执行的,但两次并发调用依然会先后通过size>1024的判断,第一个调用执行完清空current.log的操作后,第二个调用还是会按逻辑清空old.log、复制已经为空的current.log,数据丢失的问题完全不会得到缓解,反而会因为同步API阻塞事件循环,大幅降低你的API服务并发处理能力。
可行解决方案
1. 优先优化日志轮转逻辑
你现有实现本身存在逻辑缺陷:先清空old.log再复制current.log的操作存在时间窗口,一旦复制过程中出现程序异常,会直接导致old.log被清空、current.log数据还未写入,双向丢数据。
更稳妥的实现方式是利用操作系统同分区文件重命名为原子操作的特性,调整执行顺序:
- 直接将满容的current.log重命名为old.log,该操作不会中断,要么成功要么失败,不会出现中间状态
- 新建空的current.log供后续日志写入
- 如需保留多份历史日志,可以给旧日志加上时间戳后缀归档
2. 增加进程内互斥锁
对于单Node进程部署的场景,只需要加一个简单的状态锁,即可避免并发执行日志轮转的问题,参考实现代码如下:
const fs = require('fs/promises'); const currentLogFile = './current.log'; const oldLogFile = './old.log'; // 轮转状态锁 let isLogRotating = false; async function checkSizeCopyAndDelete() { // 已有轮转任务在执行,直接跳过本次调用 if (isLogRotating) return; try { // 每次执行前重新读取文件大小,避免判断时的时间差问题 const { size } = await fs.stat(currentLogFile); if (size <= 1024) return; // 加锁 isLogRotating = true; // 执行轮转:先重命名再新建空文件 await fs.rename(currentLogFile, oldLogFile); await fs.writeFile(currentLogFile, ''); } catch (err) { // 异常处理逻辑,可自行补充日志告警 console.error('日志轮转失败', err); } finally { // 执行完成/失败都释放锁 isLogRotating = false; } }
3. 多进程场景补充文件锁
如果你的服务是多进程部署,单进程的状态锁无法跨进程生效,可以引入文件级排他锁,执行轮转前先获取日志文件的排他锁,获取失败则跳过本次轮转,避免多进程同时操作文件。
4. 直接使用成熟的日志库
工业级场景下不需要自行实现日志轮转逻辑,可直接使用winston+winston-daily-rotate-file、log4js等成熟的日志库,内置的轮转逻辑已经兼容了并发处理、异常兜底、多进程适配等场景,可直接规避当前问题。
内容的提问来源于stack exchange,提问作者Youssef Mohamed

