在Node后端开发中,接口该写同步还是异步,讨论的往往不是语法风格,而是事件循环下主线程会不会被占住。Node主线程一旦被同步逻辑卡住,其他请求就要一起等。同样一段代码放在单机脚本里可能没事,放到线上接口并进入并发场景,就可能出现接口卡顿、RT变高、前面的请求未处理完后请求已排队。因此分界线不是“哪种写法更现代”,而是这段逻辑会不会把当前执行线占死。
先看文件读取例子。同步版本:
- const fs = require('node:fs');
- app.get('/profile', (req, res) => {
- const text = fs.readFileSync('./profile.json', 'utf8');
- res.send(JSON.parse(text));
- });
复制代码
功能本身没问题,但只要接口有人调用,readFileSync就会占住主线程。文件没读完,这次请求不往下走,其他请求也很难舒服进入。异步版本:
- const fs = require('node:fs/promises');
- app.get('/profile', async (req, res) => {
- const text = await fs.readFile('./profile.json', 'utf8');
- res.send(JSON.parse(text));
- });
复制代码
关键不是多了async几个字符,而是文件读取不会一直堵主线程。判断同步异步,应盯住阻塞点,而不是写法外观。
同步不是不能用。服务启动阶段读取本地配置、加载静态字典、初始化模板,通常只做一次,发生在服务真正接请求之前,用同步很正常:
- const fs = require('node:fs');
- const config = JSON.parse(
- fs.readFileSync('./config.json', 'utf8')
- );
复制代码
这种地方硬绕成异步,收益未必大,代码反而更散。单机脚本、数据迁移脚本、批量处理文件的小工具、构建脚本、内部工具也适合同步优先,因为目标是把事情按顺序做完,不关心还能不能同时服务其他请求。轻量的纯内存逻辑,如字段整理、对象映射、少量判断,本来就该同步:
- function toUserCard(user) {
- return {
- id: user.id,
- nickname: user.nickname,
- isVip: user.level >= 3,
- };
- }
复制代码
这种函数没有I/O和网络等待,硬写成异步没有实际意义。边界要清楚:纯内存逻辑不等于永远安全。请求链路里的大循环、大JSON处理、大量排序聚合,就算不访问数据库,也会拖住主线程。此时问题不是同步能不能用,而是这段计算本来就不该这么放。
正式线上服务里,下面这些地方应优先异步:数据库查库、写库、事务;Redis、MQ、第三方HTTP调用;文件读写;用户接口里的耗时操作。只要要等网络或外部资源结果,就应走异步。数据库示例:
- app.get('/orders', async (req, res) => {
- const data = await orderService.list(req.user.id);
- res.json(data);
- });
复制代码
这里若坚持同步写法,本质是拿接口吞吐换一时省事。文件I/O对Node同样是I/O,放在请求主路径上就有能力拖慢主线程。用户接口中的耗时操作,可以先问一句:这段逻辑是不是在用户请求过程中执行,而且会不会花时间;答案是会,就优先往异步、非阻塞靠。
容易带偏人的不是同步,而是“异步绝对正确”。误区一是接口代码必须全异步。接口应避免阻塞,但不等于每一层逻辑都必须异步;参数清洗、结果拼装、轻量校验本来就是同步动作,套一层async只是把短路绕远。误区二是同步写法一定更差。在一次性脚本、启动阶段、低请求量本地任务里,同步不一定差,有时更省心。Node要警惕的是同步阻塞出现在并发请求链路里。误区三是async/await写满就代表工程更规范。没有异步源却假装异步,会增加调用方和维护负担:
- async function buildUserView(user) {
- return {
- id: user.id,
- nickname: user.nickname,
- };
- }
复制代码
这不是错,只是没必要。
实际项目里做判断,可先过四个问题:1.有没有等待外部资源,数据库、Redis、文件、HTTP只要涉及等待外部返回,优先异步;2.它在不在请求主路径上,在用户接口执行链路里就要对阻塞更敏感;3.它一旦慢下来,会不会连带拖住别的请求,不能只看自己能不能跑完,还要看跑的时候会不会把别人堵住;4.它属于启动期还是运行期,启动期任务可放宽,运行期任务要保守。很多争论是把这两个阶段混成了一件事。
场景速查可这样记:服务启动时读取本地配置,同步即可;单机脚本、构建脚本、内部工具,同步优先;轻量对象映射、字段整理、简单计算,保持同步;数据库CRUD,必须异步;Redis、MQ、第三方接口调用,必须异步;文件上传、下载、读写,必须异步;面向用户的线上接口主路径,优先非阻塞设计。这不是死规定,但能挡掉大部分误判。
结论可以收成一句:Node里没有“同步永远落后”,也没有“异步天然正确”。同步适合一次性、短链路、轻逻辑;异步适合I/O、等待态、并发请求场景。真正要盯住的是这段代码会不会影响整条请求链路的流动性。先判断这段代码会不会堵住别人,再决定它该不该写成同步。 |