You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Node.js文件计数器并发递增问题及解决方案问询

问题解答

首先直接给结论:完全有可能出现两个请求都读到10,最终文件写入11的情况,这就是典型的竞态条件(Race Condition)。

为什么会发生这种情况?

因为fs.readFile和fs.writeFile都是异步非阻塞操作。当两个请求几乎同时到达时,它们的读取操作会先被触发,都从文件里读到初始值10;接着两个请求各自把10加1得到11,然后先后执行写入操作——最后一次写入会覆盖前一次的结果,所以文件最终是11,而不是预期的12。

举个时间线例子:

  • 请求A:发起readFile,读到10
  • 请求B:几乎同时发起readFile,也读到10
  • 请求A:计算10+1=11,发起writeFile写入11
  • 请求B:计算10+1=11,发起writeFile写入11
  • 文件最终值是11,丢失了一次增量

怎么解决这个问题?

下面是几种实用的解决方案,你可以根据自己的场景选择:

1. 改用同步文件操作(适合低并发场景)

把异步的readFile/writeFile换成同步的readFileSync/writeFileSync,这样每个请求的"读-加1-写"流程会变成原子操作——同一时间只有一个请求能执行这个完整流程,避免竞态。

代码示例:

const fs = require('fs');
const path = require('path');
const express = require('express');
const app = express();

const counterFile = path.join(__dirname, 'counter.txt');

app.get('/increment', (req, res) => {
  try {
    // 同步读取当前值
    const currentValue = parseInt(fs.readFileSync(counterFile, 'utf8'), 10);
    const newValue = currentValue + 1;
    // 同步写入新值
    fs.writeFileSync(counterFile, newValue.toString());
    res.send(`当前计数器值:${newValue}`);
  } catch (err) {
    console.error('计数器操作失败:', err);
    res.status(500).send('操作失败');
  }
});

app.listen(3000, () => console.log('服务启动在3000端口'));

⚠️ 注意:同步操作会阻塞Node.js的事件循环,如果你的服务并发量很高,这种方法会导致请求排队,性能下降,所以只适合低流量场景。

2. 使用内存计数器+定时持久化(适合高并发,可接受短暂数据丢失)

把计数器存在内存变量里,每次请求直接对内存变量做增量操作,然后定期把内存中的值写入文件(比如每几秒一次),或者在进程退出时写入。这种方式性能最好,但要注意:如果进程意外崩溃,最近一次持久化之后的增量会丢失。

代码示例:

const fs = require('fs');
const path = require('path');
const express = require('express');
const app = express();

const counterFile = path.join(__dirname, 'counter.txt');
// 初始化内存计数器,从文件读取初始值
let counter = parseInt(fs.readFileSync(counterFile, 'utf8'), 10) || 0;

// 每5秒持久化一次到文件
setInterval(() => {
  fs.writeFileSync(counterFile, counter.toString());
}, 5000);

// 进程退出时强制持久化
process.on('exit', () => {
  fs.writeFileSync(counterFile, counter.toString());
});

app.get('/increment', (req, res) => {
  counter++;
  res.send(`当前计数器值:${counter}`);
});

app.listen(3000, () => console.log('服务启动在3000端口'));

3. 使用数据库的原子操作(高并发场景首选)

用数据库代替文件存储计数器,因为数据库本身提供了原子性的增量操作,能完美解决竞态问题。比如Redis的INCR命令,或者SQL数据库的UPDATE ... SET counter = counter + 1语句。

以Redis为例的代码示例:

const redis = require('redis');
const express = require('express');
const app = express();
const fs = require('fs');
const path = require('path');
const counterFile = path.join(__dirname, 'counter.txt');

// 连接Redis
const redisClient = redis.createClient({
  // 配置你的Redis地址、密码等
});

(async () => {
  await redisClient.connect();
  // 初始化计数器(如果不存在则设为0)
  const current = await redisClient.get('counter');
  if (!current) {
    await redisClient.set('counter', '0');
  }
})();

app.get('/increment', async (req, res) => {
  try {
    // Redis的INCR命令是原子性的,直接返回增量后的值
    const newValue = await redisClient.incr('counter');
    res.send(`当前计数器值:${newValue}`);
    // 可选:定期把Redis的值同步到文件做持久化
    if (newValue % 100 === 0) {
      fs.writeFileSync(counterFile, newValue.toString());
    }
  } catch (err) {
    console.error('Redis操作失败:', err);
    res.status(500).send('操作失败');
  }
});

app.listen(3000, () => console.log('服务启动在3000端口'));

4. 使用应用层互斥锁(轻量级,无需额外依赖)

通过第三方库实现应用层的互斥锁,确保同一时间只有一个请求执行读写文件的流程。比如用async-mutex库:

代码示例:

const fs = require('fs').promises;
const path = require('path');
const express = require('express');
const { Mutex } = require('async-mutex');
const app = express();

const counterFile = path.join(__dirname, 'counter.txt');
// 创建一个互斥锁
const mutex = new Mutex();

app.get('/increment', async (req, res) => {
  let release;
  try {
    // 获取锁,确保同一时间只有一个请求进入代码块
    release = await mutex.acquire();
    // 读取当前值
    const currentValue = parseInt(await fs.readFile(counterFile, 'utf8'), 10);
    const newValue = currentValue + 1;
    // 写入新值
    await fs.writeFile(counterFile, newValue.toString());
    res.send(`当前计数器值:${newValue}`);
  } catch (err) {
    console.error('计数器操作失败:', err);
    res.status(500).send('操作失败');
  } finally {
    // 释放锁
    if (release) release();
  }
});

app.listen(3000, () => console.log('服务启动在3000端口'));

总结

  • 如果是低并发场景,用同步文件操作最简单;
  • 高并发场景优先选Redis或数据库的原子操作;
  • 想要轻量且性能不错,选内存计数器+定时持久化(注意数据丢失风险);
  • 不想用数据库又要保证数据安全,用应用层互斥锁。

内容的提问来源于stack exchange,提问作者user2039981

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.14 08:09:14