跳转到内容
123xiao | 无名键客

《Node.js 中基于 Worker Threads 与消息队列的高并发任务处理实战-156》

字数: 0 阅读时长: 1 分钟

背景与问题

很多团队第一次做 Node.js 高并发任务处理时,都会先写出这样一版:

  • HTTP 请求进来
  • 主线程直接做 CPU 密集计算,或者同步做图片处理、报表生成、压缩、加密
  • 请求量一上来,接口延迟飙升,甚至整个进程“假死”

这不是 Node.js 不行,而是用错了位置

Node.js 的主线程适合做:

  • 网络 I/O
  • 事件调度
  • 轻量业务编排

但如果你把以下任务直接塞进主线程,就很容易出问题:

  • 大量 JSON 解析/序列化
  • 文件压缩
  • 图片转码
  • 批量加密/哈希
  • 规则计算、风控评分、报表聚合

我自己第一次踩这个坑,是在一个“批量生成缩略图”的服务里。接口本身逻辑不复杂,但一旦用户同时提交几百个任务,CPU 直接打满,主线程 event loop 延迟明显变高,连健康检查都开始超时。

这类问题本质上有两个维度:

  1. 单进程内部如何并行处理 CPU 密集任务
  2. 系统层面如何通过消息队列削峰、解耦、失败重试

所以,一个比较稳妥的架构通常是:

  • 主服务:负责接收请求、校验参数、投递消息
  • 消息队列:负责缓存任务、削峰、重试、异步解耦
  • Worker Threads 池:负责真正执行 CPU 密集型任务
  • 结果存储/状态回写:记录任务进度、结果和失败原因

这篇文章我会从“架构落地”的角度,带你搭一套可运行的 Node.js 实战示例。


方案全景:为什么是 Worker Threads + 消息队列

先说结论:

  • Worker Threads 解决的是“Node.js 进程内 CPU 并行”
  • 消息队列 解决的是“任务异步化、削峰、重试、解耦”
  • 两者结合,才适合高并发任务场景

只用 Worker Threads 不够

如果只有 Worker Threads,没有队列:

  • 突发 10 万个任务时,服务端还是得瞬时接住
  • 任务生命周期不好管理
  • 重试、积压、回溯、死信处理都比较麻烦

只用消息队列也不够

如果只有队列,消费端仍然在单线程 Node.js 主线程里跑 CPU 任务:

  • 消费者会很快成为瓶颈
  • event loop 被阻塞
  • 吞吐不稳定

推荐架构

flowchart LR
    A[客户端/上游系统] --> B[API 服务]
    B --> C[消息队列]
    C --> D[Consumer 进程]
    D --> E[Worker Thread Pool]
    E --> F[任务结果存储]
    E --> G[日志/指标系统]
    B --> H[任务状态查询接口]
    H --> F

这个架构的优点很明确:

  • API 快速返回,避免同步阻塞
  • 队列承接流量尖峰
  • Worker 池充分利用多核 CPU
  • 任务失败可重试
  • 状态可追踪,便于运维和排查

核心原理

这一部分我们不讲太虚的概念,直接讲跟落地最相关的三个点。

1. Worker Threads:把 CPU 任务移出主线程

Node.js 的 worker_threads 模块可以在同一进程中创建多个线程,每个线程有独立的事件循环和 JavaScript 执行环境。

适合处理:

  • CPU 密集型计算
  • 数据转换
  • 批量压缩/加密
  • 规则引擎计算

主线程与 Worker 之间通过以下方式通信:

  • postMessage
  • parentPort.on('message')
  • workerData
  • Transferable objects
  • SharedArrayBuffer

最常见的做法是:主线程维护一个 Worker 池,按需分发任务。

2. 消息队列:削峰与解耦

队列的作用不是“让系统更快”,而是“让系统更稳”。

它解决的问题包括:

  • 流量突刺时先排队,避免系统被打爆
  • API 层与执行层解耦
  • 消费失败可重试
  • 支持延迟任务、死信队列、优先级等能力

在生产环境里你可以选择:

  • RabbitMQ:消费确认、路由灵活
  • Kafka:吞吐高,适合日志/流式数据
  • Redis Stream / BullMQ:开发效率高,Node.js 生态友好

本文示例为了可运行和便于理解,会用一个内存消息队列模拟器来展示流程。实际生产只需把这层替换成 RabbitMQ、BullMQ 或 Kafka 消费逻辑即可。

3. 背压(Backpressure):高并发系统里最容易被忽视的点

很多人以为“线程越多越快”,其实不对。

如果:

  • 队列消费速度 > Worker 池处理速度
  • 或者 Worker 池开太多导致 CPU 抢占严重

那么结果往往是:

  • 上下文切换增多
  • 平均耗时变长
  • 内存膨胀
  • 整体吞吐反而下降

所以必须控制:

  • 队列拉取速率
  • Worker 池大小
  • 单任务最大执行时间
  • 最大重试次数

下面这张时序图能更直观看清整个链路。

sequenceDiagram
    participant Client as 客户端
    participant API as API 服务
    participant MQ as 消息队列
    participant Consumer as Consumer
    participant Pool as Worker 池
    participant Store as 状态存储

    Client->>API: 提交任务
    API->>Store: 写入 PENDING
    API->>MQ: 投递消息
    API-->>Client: 返回 taskId

    MQ->>Consumer: 拉取任务
    Consumer->>Pool: 分配给空闲 Worker
    Pool->>Pool: 执行 CPU 计算
    Pool->>Store: 更新 SUCCESS/FAILED
    Consumer->>MQ: ack / retry

方案对比与取舍分析

在 architecture 类型文章里,单说“怎么做”还不够,还得讲“为什么不是别的方案”。

方案一:主线程直接执行

优点

  • 简单
  • 开发成本低

缺点

  • CPU 任务会阻塞 event loop
  • 请求量一高就雪崩
  • 无法优雅削峰

适用场景

  • 小工具、低并发后台任务
  • 非核心链路

方案二:Cluster 多进程 + 进程内执行

优点

  • 能利用多核
  • 部署简单

缺点

  • 任务调度和共享状态复杂
  • 每个进程仍可能被 CPU 任务阻塞
  • 任务重试和异步解耦仍需额外设计

适用场景

  • HTTP 服务横向扩展
  • I/O 为主的业务

方案三:消息队列 + 独立 Node.js Consumer + Worker Threads 池

优点

  • 架构清晰
  • 可扩展性好
  • CPU 任务隔离
  • 易于加入重试、监控、限流

缺点

  • 开发复杂度更高
  • 需要队列和状态存储
  • 调优成本增加

适用场景

  • 中高并发异步任务系统
  • 图片、报表、风控、ETL、批处理类业务

方案四:直接上多语言异构计算服务

比如:

  • Node.js 做 API 和调度
  • Python / Go / Rust 做计算引擎

优点

  • 针对特定任务性能更强
  • 便于语言分工

缺点

  • 运维和 RPC 成本更高
  • 调试链路更长

适用场景

  • 算法密集
  • 团队有成熟多语言基础设施

如果你现在是中级阶段,最推荐的演进路径就是:

  1. 先把 CPU 任务从主线程剥离
  2. 再引入消息队列
  3. 再加监控、重试、限流、死信

不要一开始就把系统做成“分布式宇宙飞船”。


容量估算:上线前至少要算这几笔账

架构不是画图,最终还得落到容量。

假设:

  • 单个任务平均耗时:200ms
  • 单个 Worker 同时只能跑 1 个 CPU 任务
  • 机器 CPU 核数:8
  • Worker 池大小:6(通常不要打满所有核,给主线程和系统留余量)

那么理论吞吐约为:

TPS ≈ Worker 数 / 单任务耗时秒数
TPS ≈ 6 / 0.2 = 30 tasks/s

如果高峰期入口任务速率是 90 tasks/s,那么积压速率大概是:

90 - 30 = 60 tasks/s

10 秒内可能堆积:

60 * 10 = 600 tasks

这时你就能反推:

  • 队列长度是否可接受
  • 是否需要更多 Consumer 实例
  • 是否需要任务拆分
  • 是否应该降低单任务粒度

经验上,容量评估至少看这几个指标:

  • 入队速率
  • 消费速率
  • 平均处理耗时
  • P95/P99 处理耗时
  • 重试率
  • 死信率
  • Worker 池利用率
  • event loop 延迟

实战代码(可运行)

下面做一个完整示例:

  • api.js:模拟任务提交
  • queue.js:内存消息队列
  • worker-pool.js:Worker 线程池
  • task-worker.js:实际执行 CPU 任务
  • consumer.js:消费队列并调度 Worker
  • store.js:内存状态存储
  • index.js:启动演示

目录结构

high-concurrency-demo/
├── api.js
├── consumer.js
├── index.js
├── queue.js
├── store.js
├── task-worker.js
└── worker-pool.js

1)状态存储:store.js

// store.js
const tasks = new Map();

function createTask(taskId, payload) {
  tasks.set(taskId, {
    taskId,
    payload,
    status: 'PENDING',
    result: null,
    error: null,
    retryCount: 0,
    createdAt: Date.now(),
    updatedAt: Date.now(),
  });
}

function updateTask(taskId, patch) {
  const old = tasks.get(taskId);
  if (!old) return;
  tasks.set(taskId, {
    ...old,
    ...patch,
    updatedAt: Date.now(),
  });
}

function getTask(taskId) {
  return tasks.get(taskId);
}

function getAllTasks() {
  return Array.from(tasks.values());
}

module.exports = {
  createTask,
  updateTask,
  getTask,
  getAllTasks,
};

2)内存消息队列:queue.js

// queue.js
const EventEmitter = require('events');

class InMemoryQueue extends EventEmitter {
  constructor() {
    super();
    this.queue = [];
  }

  publish(message) {
    this.queue.push(message);
    this.emit('message');
  }

  consume(handler) {
    const tryConsume = async () => {
      while (this.queue.length > 0) {
        const msg = this.queue.shift();
        try {
          await handler(msg);
        } catch (err) {
          console.error('[queue] consume error:', err.message);
        }
      }
    };

    this.on('message', tryConsume);
    setImmediate(tryConsume);
  }

  size() {
    return this.queue.length;
  }
}

module.exports = new InMemoryQueue();

3)Worker 线程:task-worker.js

这里用一个“故意吃 CPU”的计算任务来模拟真实场景,比如大规模数值计算。

// task-worker.js
const { parentPort } = require('worker_threads');

function heavyCalculation(n) {
  let count = 0;
  for (let i = 2; i <= n; i++) {
    let isPrime = true;
    for (let j = 2; j * j <= i; j++) {
      if (i % j === 0) {
        isPrime = false;
        break;
      }
    }
    if (isPrime) count++;
  }
  return count;
}

parentPort.on('message', (task) => {
  try {
    const { taskId, payload } = task;
    const start = Date.now();
    const result = heavyCalculation(payload.n);
    const duration = Date.now() - start;

    parentPort.postMessage({
      taskId,
      success: true,
      result: {
        primeCount: result,
        duration,
      },
    });
  } catch (error) {
    parentPort.postMessage({
      taskId: task.taskId,
      success: false,
      error: error.message,
    });
  }
});

4)Worker 池:worker-pool.js

这个实现支持:

  • 固定大小线程池
  • 空闲 Worker 复用
  • 任务排队
  • Promise 风格调用
// worker-pool.js
const path = require('path');
const { Worker } = require('worker_threads');

class WorkerPool {
  constructor(size, workerFile) {
    this.size = size;
    this.workerFile = workerFile;
    this.workers = [];
    this.idleWorkers = [];
    this.taskQueue = [];
    this.callbacks = new Map();

    for (let i = 0; i < size; i++) {
      this.addWorker();
    }
  }

  addWorker() {
    const worker = new Worker(path.resolve(__dirname, this.workerFile));
    worker.currentTaskId = null;

    worker.on('message', (message) => {
      const { taskId, success, result, error } = message;
      const cb = this.callbacks.get(taskId);

      if (cb) {
        this.callbacks.delete(taskId);
        worker.currentTaskId = null;
        this.idleWorkers.push(worker);

        if (success) cb.resolve(result);
        else cb.reject(new Error(error));

        this.schedule();
      }
    });

    worker.on('error', (err) => {
      console.error('[worker] error:', err.message);

      if (worker.currentTaskId) {
        const cb = this.callbacks.get(worker.currentTaskId);
        if (cb) {
          this.callbacks.delete(worker.currentTaskId);
          cb.reject(err);
        }
      }

      this.workers = this.workers.filter((w) => w !== worker);
      this.idleWorkers = this.idleWorkers.filter((w) => w !== worker);

      this.addWorker();
      this.schedule();
    });

    worker.on('exit', (code) => {
      if (code !== 0) {
        console.warn(`[worker] exited with code ${code}`);
      }
    });

    this.workers.push(worker);
    this.idleWorkers.push(worker);
  }

  runTask(task) {
    return new Promise((resolve, reject) => {
      this.taskQueue.push({ task, resolve, reject });
      this.schedule();
    });
  }

  schedule() {
    while (this.idleWorkers.length > 0 && this.taskQueue.length > 0) {
      const worker = this.idleWorkers.shift();
      const { task, resolve, reject } = this.taskQueue.shift();

      worker.currentTaskId = task.taskId;
      this.callbacks.set(task.taskId, { resolve, reject });
      worker.postMessage(task);
    }
  }

  stats() {
    return {
      poolSize: this.size,
      totalWorkers: this.workers.length,
      idleWorkers: this.idleWorkers.length,
      busyWorkers: this.workers.length - this.idleWorkers.length,
      pendingTasks: this.taskQueue.length,
    };
  }
}

module.exports = WorkerPool;

5)消费者:consumer.js

这里加入一个简单重试机制。

// consumer.js
const queue = require('./queue');
const store = require('./store');
const WorkerPool = require('./worker-pool');

const MAX_RETRY = 2;
const pool = new WorkerPool(4, './task-worker.js');

function startConsumer() {
  queue.consume(async (message) => {
    const { taskId, payload } = message;

    store.updateTask(taskId, { status: 'PROCESSING' });

    try {
      const result = await pool.runTask({ taskId, payload });
      store.updateTask(taskId, {
        status: 'SUCCESS',
        result,
        error: null,
      });
      console.log(`[consumer] task ${taskId} success`, result);
    } catch (err) {
      const task = store.getTask(taskId);
      const retryCount = (task?.retryCount || 0) + 1;

      if (retryCount <= MAX_RETRY) {
        store.updateTask(taskId, {
          status: 'RETRYING',
          retryCount,
          error: err.message,
        });

        console.warn(`[consumer] task ${taskId} retry ${retryCount}`);
        queue.publish({ taskId, payload });
      } else {
        store.updateTask(taskId, {
          status: 'FAILED',
          retryCount,
          error: err.message,
        });
        console.error(`[consumer] task ${taskId} failed:`, err.message);
      }
    }
  });

  setInterval(() => {
    console.log('[pool stats]', pool.stats(), 'queueSize=', queue.size());
  }, 2000);
}

module.exports = {
  startConsumer,
};

6)API 模拟:api.js

// api.js
const crypto = require('crypto');
const queue = require('./queue');
const store = require('./store');

function submitTask(payload) {
  const taskId = crypto.randomUUID();
  store.createTask(taskId, payload);
  queue.publish({ taskId, payload });
  return { taskId };
}

module.exports = {
  submitTask,
};

7)启动入口:index.js

// index.js
const { submitTask } = require('./api');
const { startConsumer } = require('./consumer');
const store = require('./store');

startConsumer();

// 模拟高并发提交任务
for (let i = 0; i < 20; i++) {
  const n = 30000 + Math.floor(Math.random() * 5000);
  const { taskId } = submitTask({ n });
  console.log(`[api] submitted taskId=${taskId}, n=${n}`);
}

// 定时输出任务结果
const timer = setInterval(() => {
  const tasks = store.getAllTasks();
  const summary = tasks.reduce(
    (acc, t) => {
      acc[t.status] = (acc[t.status] || 0) + 1;
      return acc;
    },
    {}
  );

  console.log('[summary]', summary);

  const done = tasks.every((t) => ['SUCCESS', 'FAILED'].includes(t.status));
  if (done && tasks.length > 0) {
    console.log('\n=== FINAL TASKS ===');
    console.table(
      tasks.map((t) => ({
        taskId: t.taskId.slice(0, 8),
        status: t.status,
        retryCount: t.retryCount,
        duration: t.result?.duration ?? '-',
        primeCount: t.result?.primeCount ?? '-',
        error: t.error ?? '-',
      }))
    );
    clearInterval(timer);
    process.exit(0);
  }
}, 1000);

运行方式

初始化并运行:

node index.js

如果你想更明显地观察 Worker 池作用,可以调整:

  • 任务数:20 -> 100
  • 池大小:4 -> 2 / 8
  • 计算规模:30000 -> 80000

运行后的架构行为解读

这个示例虽然简化了队列实现,但关键行为已经具备:

  1. API 层只负责接任务,不做重计算
  2. 任务先进队列
  3. Consumer 拉取后丢给 Worker 池
  4. Worker 执行完成后回写状态
  5. 失败任务自动重试

你可以把它理解成生产架构的“骨架版”。

下面用状态图再看一遍任务生命周期。

stateDiagram-v2
    [*] --> PENDING
    PENDING --> PROCESSING
    PROCESSING --> SUCCESS
    PROCESSING --> RETRYING
    RETRYING --> PROCESSING
    RETRYING --> FAILED
    FAILED --> [*]
    SUCCESS --> [*]

常见坑与排查

这部分非常重要。很多系统不是设计错,而是细节坑太多。

1. 主线程还是卡顿,明明已经用了 Worker

常见原因

  • 主线程里还有大量同步逻辑
  • 大对象序列化/反序列化太重
  • 日志打印过多
  • 队列消费回调里做了额外 CPU 工作

排查建议

先看 event loop 延迟。

你可以简单加一段监控:

// loop-delay.js
let last = Date.now();

setInterval(() => {
  const now = Date.now();
  const delta = now - last - 1000;
  console.log('[event loop delay]', delta, 'ms');
  last = now;
}, 1000);

如果延迟明显变大,说明主线程仍有阻塞点。

2. Worker 池开太大,性能反而下降

这是很常见的误判。

原因

  • CPU 核数有限
  • 线程过多导致上下文切换增加
  • 缓存命中率下降
  • 系统调度开销变大

建议

经验上:

  • Worker 数先从 CPU 核数 - 1CPU 核数 * 0.5 ~ 1 开始压测
  • 不要凭感觉直接开到几十上百

3. 消息重复消费

在真实 MQ 中,这几乎一定会遇到。

原因

  • 消费者处理成功但 ack 失败
  • 网络抖动导致重复投递
  • Consumer 重启期间状态不一致

建议

业务上要做幂等

  • taskId 做去重键
  • 如果任务已成功,重复消息直接忽略
  • 数据落库时使用唯一约束

比如:

if (store.getTask(taskId)?.status === 'SUCCESS') {
  return;
}

生产里建议把幂等放到数据库或缓存层,不要只靠内存 Map。

4. 内存涨得很快

常见原因

  • 队列堆积过多
  • 任务结果太大
  • 回调 Map 没有及时清理
  • Worker 异常退出后资源未释放
  • 日志对象保留引用

排查手段

  • process.memoryUsage()
  • --inspect + Chrome DevTools
  • heap snapshot
  • 观察队列长度与 pending task 数量

5. Worker 异常退出后任务丢失

如果 Worker 在执行中崩溃,而主进程没有兜底,就会出现:

  • 任务没有结果
  • 状态一直卡在 PROCESSING

修复思路

  • 在 Worker error / exit 时识别当前任务
  • 将任务重新入队或标记失败
  • PROCESSING 状态设置超时扫描

例如:

  • 超过 5 分钟未更新的任务
  • 由补偿任务扫描后改成 PENDING 重投

6. 重试导致“雪崩式放大”

很多人一看失败就重试 5 次、10 次,这其实很危险。

典型问题

  • 原因若是系统性故障,重试只会加重压力
  • 队列会被重试任务挤爆
  • 下游依赖承受二次冲击

正确做法

  • 限制最大重试次数
  • 使用指数退避
  • 对不可重试错误直接失败
  • 配置死信队列

安全/性能最佳实践

这里我把真正值得长期坚持的建议列出来。

1. 使用线程池,不要每个任务新建 Worker

错误做法:

  • 一个任务创建一个 Worker
  • 任务结束销毁线程

这会带来明显开销:

  • 线程创建成本高
  • 内存抖动大
  • 吞吐不稳定

正确做法:

  • 固定大小 Worker 池
  • 任务复用线程

这也是本文示例采用的方式。

2. 控制消息体大小

消息队列不是文件存储系统。

尽量不要把超大对象直接塞进消息体里,建议:

  • 消息只放任务元信息
  • 大文件或大数据放对象存储/数据库
  • 消息里传引用地址、文件 key、记录 ID

这样可以显著降低:

  • 网络传输成本
  • 序列化开销
  • 消费失败概率

3. 任务要有超时与取消机制

真实生产里一定要考虑“长时间卡住”的任务。

建议给每个任务设置:

  • 执行超时
  • 最大重试次数
  • 手动取消能力

如果 Worker 内部任务不可中断,至少在主流程里做到:

  • 超时后标记失败
  • 不再接受其结果回写
  • 重新投递或人工介入

4. 做好幂等设计

高并发异步系统的第一原则之一就是:

默认消息可能重复。

所以要确保:

  • 同一 taskId 重复执行不会造成脏数据
  • 结果写入是覆盖式或原子式
  • 外部副作用操作有幂等保护

5. 给队列和线程池加监控

最低限度你应该监控:

  • 队列积压长度
  • 消费速率
  • 成功率 / 失败率 / 重试率
  • Worker busy 比例
  • 单任务耗时 P95 / P99
  • event loop delay
  • 进程 CPU / 内存占用

没有这些指标,系统出问题时基本靠猜。

6. 区分可重试错误与不可重试错误

比如:

可重试

  • 临时网络超时
  • 短暂资源不足
  • 下游服务 5xx

不可重试

  • 参数非法
  • 数据格式错误
  • 任务目标不存在
  • 权限不足

如果不区分,系统只会不断重试无意义任务。

7. 用真实队列时要理解 ack 语义

不同消息系统语义差异很大:

  • RabbitMQ:ack/nack、prefetch 很关键
  • Kafka:offset 提交时机会影响重复消费
  • BullMQ:锁续期、重试、延迟队列机制要搞清楚

我建议上线前一定做两类演练:

  • Consumer 在处理到一半时崩溃
  • 队列连接闪断后恢复

这两个场景最容易暴露“看起来能跑,实际上不可靠”的问题。

8. 不要忽略数据安全与隔离

如果任务来自用户输入,还要额外注意:

  • 参数校验
  • 路径遍历防护
  • 文件处理沙箱化
  • 限制资源配额
  • 防止恶意提交超大任务

比如图片/压缩包处理这类场景,很容易被构造输入拖垮系统。


一个更贴近生产的演进建议

如果你打算把本文方案真正搬进项目,我建议按这个节奏推进:

第一步:先把同步改异步

  • 请求提交后立即返回 taskId
  • 提供状态查询接口
  • 不要求前端一直等结果

第二步:接入真实消息队列

优先考虑团队熟悉的技术栈:

  • Redis 生态熟:BullMQ
  • 重路由与可靠消费:RabbitMQ
  • 超大吞吐日志/流式:Kafka

第三步:引入持久化状态存储

不要长期依赖内存 Map。

至少落到:

  • MySQL / PostgreSQL:任务状态表
  • Redis:短状态缓存
  • 对象存储:大结果文件

第四步:补齐可靠性能力

包括:

  • 重试策略
  • 超时控制
  • 死信队列
  • 幂等键
  • 任务补偿扫描

第五步:压测和调优

重点调:

  • Worker 池大小
  • 消费并发度
  • 队列 prefetch
  • 单任务粒度
  • 消息体大小

总结

在 Node.js 里做高并发任务处理,核心不是“把代码写得更快”,而是把职责拆对:

  • 主线程处理请求与编排
  • 消息队列负责削峰、解耦、重试
  • Worker Threads负责 CPU 密集型执行
  • 状态存储负责可观测与可恢复

如果你只记住一句话,我希望是这句:

Node.js 不怕高并发,怕的是把 CPU 重活堆在主线程,还没有任何削峰和背压设计。

落地时的可执行建议如下:

  1. 先识别 CPU 密集任务,不要盲目全量改造
  2. 先上 Worker 池,再接消息队列
  3. 为任务设计状态机,至少有 PENDING / PROCESSING / SUCCESS / FAILED
  4. 一定做幂等与重试上限
  5. 压测决定线程数,不要靠感觉
  6. 监控队列积压、任务耗时和 event loop delay

最后补一个边界条件:如果你的任务极度计算密集,或者涉及复杂科学计算、视频编解码、AI 推理,那么 Node.js 更适合做调度层,而不是最终计算层。此时可以保留“消息队列 + 状态管理”架构,把执行层换成更擅长计算的服务。

对于大多数中型业务系统来说,Node.js + 消息队列 + Worker Threads 已经是一套足够实用、成本可控、扩展性也不错的高并发任务处理方案。


分享到:

下一篇
《Web逆向实战:中级开发者如何定位并复现前端签名算法实现接口自动化调用》