5.2 Koa洋葱模型与数据库访问层


5.2 Koa 洋葱模型与数据库访问层

本节摘要:Koa 把中间件写成 async 函数,await next 让洋葱模型的「回程」第一次变得可编程;数据层则承接框架之下的稳定输出——连接池异步借还、慢查询监控、SQL 注入防线与 ORM 取舍。本节把框架选型与数据访问放在一节,因为它们面对的是同一个问题:如何让 I/O 在事件循环上跑得又稳又快。

动手目标

  1. 能写出 Koa 风格中间件并解释 await next 前后的执行时机
  2. 能对比 Express 与 Koa 的错误处理与异步支持差异
  3. 能解释连接池的借还模型与池参数的调法
  4. 能识别 N+1 查询并做参数化查询防注入
  5. 能在 ORM 与裸 SQL 之间做场景化取舍

一、Koa:为 await 而生的框架

Koa 出自 Express 原班人马,动机很直接:Express 诞生时 async/await 还不存在,错误处理靠 next(err) 接力;Koa 从第一行代码就假设你写 async 函数。中间件长这样:

const Koa = require('koa'); const app = new Koa(); app.use(async (ctx, next) => { const start = Date.now(); await next(); // 等待管道后半程全部完成 const ms = Date.now() - start; console.log(`${ctx.method} ${ctx.url} - ${ms}ms`); }); app.use(async ctx => { ctx.body = { hello: 'koa' }; }); app.listen(3000);

await next() 把 Express 里被遗忘的「回程」变成了显式可控的代码:计时中间件不需要监听 finish 事件,await 之后天然是响应完成的位置。更妙的是错误处理——整条管道被包在一个大 try/catch 里,任何中间件抛错(包括 await 的异步拒绝)都会落到最外层的 error 事件:

app.on('error', (err, ctx) => { console.error('服务器错误', ctx.path, err.message); });

对比一眼见分晓:

维度 Express Koa
中间件形态 回调 + next async + await next
异步错误 try/catch 后 next(e) 中间件内直接 throw
回程可编程 常被遗忘 await 后显式
内置能力 路由、静态等齐备 极简核心,全靠生态装配
心智模型 流水线 洋葱

Koa 的「极简」是哲学也是代价:路由、体解析都要另装。我的选型口径:重生态、快交付选 Express;想要干净的控制流、团队 async 熟练度高选 Koa。两者性能差异在绝大多数业务里可以忽略——瓶颈几乎总在数据库,这正好引出下半节。

二、连接池:数据库访问的第一课

每个查询前新建连接、查完拆掉?TCP 握手加认证动辄几十毫秒,高并发下数据库会被连接风暴打死。连接池的模型:启动时建好 N 条连接,请求来了「借」一条,用完「还」回去。因为借与还都发生在异步等待之后,池天然就是事件循环友好的设计。

const { Pool } = require('pg'); // 以 PostgreSQL 为例 const pool = new Pool({ max: 20, // 池上限:不是越大越好 idleTimeoutMillis: 30000, // 空闲连接 30 秒后回收 connectionTimeoutMillis: 5000, // 借不到连接最多等 5 秒,宁可报错别堆积 }); app.get('/api/users/:id', async (req, res, next) => { try { const { rows } = await pool.query( 'SELECT id, name FROM users WHERE id = $1', [req.params.id] ); res.json(rows[0] || null); } catch (e) { next(e); } });

池大小的调法有共识可循:池上限参考数据库能承受的并发连接数除以服务进程数。第 7 章会讲 cluster 开 4 个进程,那么单进程池上限就该是总配额的四分之一——四个进程各 20 条连接打满一个百连接上限的库,正好。

⚠️ 常见坑:连接借出不还。任何查询路径都要保证 release 被执行,用池的 query 快捷方法(自动借还)或 try/finally 手动管理,别用事务时忘了异常分支的释放。

三、慢查询:事件循环的隐形杀手

接口慢的两大来源,一个是第 2 章讲的主线程被同步计算卡住,另一个就是慢查询——它不卡事件循环(等待是异步的),但会把连接池占满,后续请求全堵在「等连接」上,症状是整体超时。三道防线:

// 1. 给每条查询加超时,别让一条慢 SQL 拖死一个池 const { rows } = await Promise.race([ pool.query(sql, params), new Promise((_, rej) => setTimeout(() => rej(new Error('查询超时')), 5000)), ]); // 2. 记录慢查询日志(耗时阈值按业务定) const t = Date.now(); const result = await pool.query(sql, params); if (Date.now() - t > 200) console.warn('慢查询', Date.now() - t, 'ms', sql.slice(0, 80)); // 3. 给高频查询字段建索引,并定期看执行计划

还有一类典型的「不知道自己在慢」——N+1 查询:列表接口查出 100 个用户,再逐个查每个用户的最新订单,101 条查询在事件循环上排队。解法是 join 或批量 IN 查询,一次取回。症状特征:页面数据量越大越慢,且慢得线性。

四、注入防线与 ORM 取舍

字符串拼接 SQL 是永远的红线。参数化查询(占位符 + 参数数组)让「输入」永远只当数据不当代码:

// 危险:用户输入直接拼进 SQL const bad = "SELECT * FROM users WHERE name = '" + input + "'"; // input 为 ' OR '1'='1 时,全表泄露 // 安全:占位符 const good = await pool.query('SELECT * FROM users WHERE name = $1', [input]);

要不要上 ORM(Sequelize、Prisma 一类)?我的取舍表:CRUD 为主、模型关系复杂、团队规模大 → ORM 的类型安全与迁移工具值得;报表查询多、性能敏感路径、SQL 已经过 DBA 优化 → 裸 SQL 加一层薄的 DAO 函数更直接。混用也是常态:ORM 管常规读写,复杂查询留逃生舱。判断标准只有一条——ORM 生成的 SQL 你要能看懂并解释,看不懂的抽象迟早变成事故。

💡 关键直觉:框架层的选择可以随团队口味换,数据层的纪律(池化、超时、参数化、慢日志)一条都不能省——事件循环不会替你兜住数据库的坑。

本节要点回顾

  • await next 是分水岭:Koa 让洋葱回程可编程,异步错误自动进最外层。
  • 选型看生态与控制流偏好:Express 齐全、Koa 干净,性能不是决策项。
  • 池是借还模型:上限按「库总配额 ÷ 进程数」倒推;宁可超时报错,不要排队堆积。
  • 慢查询三防线:查询超时、慢日志、索引与执行计划;N+1 是线性变慢的头号嫌疑。
  • 参数化是红线:拼接 SQL 没有任何借口;ORM 可以用,但要能解释它生成的 SQL。

作者与出处
原作者: 灏天文库
来源:灏天文库
整理: 灏天文库整理
由灏天文库平台收录,内容或由平台用户上传,仅供学习交流
发布者: 作者: 灏天文库 转发
评论区 (0)
U