尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

Deno + YugabyteDB:构建可水平扩展API的现代技术栈实践

Deno + YugabyteDB:构建可水平扩展API的现代技术栈实践 先说结论Deno 不是又一个“可能会取代 Node”的框架它是一套按照现代标准重做的 JavaScript/TypeScript 运行时YugabyteDB 也不是又一个 PostgreSQL 的壳子它是把大家熟悉的 SQL 能力搬到分布式架构上、还保留了 Postgres 生态兼容性的数据库。这两样东西组合起来做 API在开发体验、类型安全、测试便利性以及后期水平扩展这几个维度上和传统 Node 单机数据库的组合有明显差别。这篇文章我会从零开始把一个“产品管理 API”在 Deno YugabyteDB 上完整搭一遍包括环境安装、表结构设计、CRUD 接口、事务、测试和部署中间把所有踩过的坑和选型判断依据都交代清楚。适合谁看如果你正在维护一个比较老旧的 Node.js Express API被依赖版本、类型混乱、测试基建缺失折腾得够呛或者业务数据已经涨到单机 PostgreSQL 有点吃力又不想迁移到 MongoDB 这类放弃事务的 NoSQL——那这套组合很值得研究。哪怕你只是对 Deno 好奇这篇文章也能当作一份可以直接照着敲的入门手册来用。1. 为什么是 Deno YugabyteDB 这对组合1.1 Deno 到底改进了什么很多人第一次听说 Deno是因为它是 Node.js 作者 Ryan Dahl 在 2018 年公开批评 Node 早期设计缺陷之后推出的新项目。但“作者重写一遍”并不是重点重点在于工程理念彻底换了。首先是安全模型。Deno 默认没有网络访问权限、没有文件系统访问权限、没有环境变量读取权限所有敏感操作都要在启动命令里显式声明。习惯了npm install之后就随便require的网络包第一次跑deno run server.ts大概率会看到一个权限错误。这个设计初看麻烦实际上是把 Node 时代那种“一个依赖就能读你整个磁盘”的隐患从根上堵住了。其次是模块系统。Deno 原生支持 ESM还能直接通过 URL 引用远程模块不需要 node_modules 那一大坨目录。配合deno.json里的 import 映射你可以像用别名一样管理依赖版本。Node 那边经过这么多年CommonJS 和 ESM 的割裂至今还在制造兼容问题Deno 从第一天就是干净的 ESM。再有就是内置工具链。格式化、Lint、测试、编译成单文件可执行程序全部内置。deno fmt、deno lint、deno test、deno compile一条命令搞定不再需要 ESLint、Prettier、Jest、esbuild 这些拼装组合每个工具还都要单独配置版本和插件。Deno 2 出来后又加上了 npm 包兼容层意味着你可以在 Deno 项目里直接import pg from npm:pg用 Node 生态的包。这一点非常关键——它让整个迁移路径平滑了很多不是让你把现有代码推倒重来而是可以渐进改造。1.2 YugabyteDB 在数据库选型中的位置YugabyteDB 属于分布式 SQL 数据库。它对外兼容 PostgreSQL 的线协议和大部分 SQL 语法也就是说你可以用现成的 Postgres 驱动连接它但在内部它把数据自动分片存储在多个节点上每个分片又有多个副本通过 Raft 共识算法保证副本之间强一致。这句话信息量很大。我们拆开理解传统 PostgreSQL 是“单机数据库”一台机器扛所有读写到了瓶颈只能纵向扩容换更大内存、更快 SSD、更多 CPU成本跟着指数涨。如果想横向扩容就得引入读写分离、分库分表而这些操作对业务代码几乎是侵入性的运维复杂度还不小。YugabyteDB 的思路是让 SQL 数据库天然具备横向扩展能力。表的数据按照主键自动做哈希分片分布在集群所有节点上应用层完全无感知。写入某个分片时数据会被同步复制到多个节点任何一个节点宕机其他副本立刻顶上。事务能力没有缩水跨分片的分布式事务依然保证 ACID这对很多金融、电商场景来说是刚需。那它和 CockroachDB 之类的同类产品有啥区别呢最大的差异在于对 PostgreSQL 生态的兼容深度。YugabyteDB 的 YSQL 接口从一开始就追求 Postgres 兼容常见的数据类型、SQL 语法、扩展机制都能直接对上。我从 PostgreSQL 迁移过来的经验是迁移的成本主要是改连接端口和检查个别不支持的扩展SQL 本身几乎不用动。1.3 这套组合适合什么场景具体讲Deno YugabyteDB 这套架构最适合三类场景。第一类是想要“新项目一步到位”的团队。反正没有历史包袱不如直接选择开发体验更好的运行时、天然分布式的数据库从一开始就把扩展问题纳入设计。第二类是现有 Node.js API 想要现代化的团队。API 逻辑不复杂但被 Node 的工程化问题拖累比如依赖冲突、测试框架不统一、部署脚本冗长。搬到 Deno 后工程化层面是降级还是升级一试便知。第三类是业务增长期、数据库已经出现瓶颈的团队。如果还在用单机 PostgreSQL且暂时不想引入中间件做分库分表可以直接把连接串指向 YugabyteDB数据通过逻辑导入工具迁移过去业务代码改动量很小。这也是我当初决定在真实项目里试这套组合的直接原因。当然它也有不适合的场景后面我会专门说。这里先记住一个核心判断这套组合的优势在于“开发体验现代化”和“存储层可扩展”这两件事被同时解决了而不是某一方面单点突出。2. 动手前的准备装环境、起库、搭项目骨架2.1 安装 Deno在 macOS 上直接一行命令brew install denoLinux 或者 WSL 环境用官方安装脚本curl -fsSL https://deno.land/install.sh | sh装完验证一下版本deno --version现在正常看到的都是 Deno 2.x 版本。Deno 2 是 2024 年 9 月发布的npm 兼容层就是在这一代里正式稳定下来的。如果你用的是 1.x 老版本建议先升级后面的命令和 API 都以 2.x 为准。安装本身没什么坑唯一要注意的是环境变量curl | sh方式安装后它默认会把DENO_INSTALL目录下的可执行文件路径写进~/.bashrc或~/.zshrc新开终端才能直接用deno命令。2.2 启动一个本地 YugabyteDB开发环境不需要搭集群用它的本地单节点模式就够。macOS 上安装brew install yugabyteLinux 上从官网下载 tarball解压后直接使用bin目录下的命令。启动非常直白yugabyted start这个命令会起一个包含 YSQL、YCQL 两个 API 接口的本地节点。我们只关心 YSQL也就是 PostgreSQL 兼容接口。启动完成后可以用下面命令确认状态yugabyted status这里必须记住一个关键差异YugabyteDB 的 YSQL 默认端口是 5433不是 PostgreSQL 默认的 5432。这个细节我后面会反复强调因为百分之九十的第一次连接失败都出在端口上。本地连接测试psql -h 127.0.0.1 -p 5433 -U yugabyte -d yugabyte默认用户和数据库都是yugabyte密码默认也是yugabyte。如果本地没有安装psql客户端也可以直接用后面代码里的驱动来验证连接不一定非要装 psql。2.3 创建项目结构和配置现在把项目骨架搭起来。我先列一个最终的目录结构后面代码都会按这个结构落位my-api/ ├── server.ts # 应用入口创建 Oak 应用 ├── db.ts # 数据库连接与查询封装 ├── routes/ │ └── products.ts # 产品相关路由 ├── middleware/ │ └── errorHandler.ts # 统一错误处理中间件 ├── tests/ │ └── api.test.ts # 接口集成测试 ├── deno.json # 项目配置和依赖映射 ├── .env # 环境变量不提交到仓库 └── Dockerfile # 容器化部署文件先写deno.jsonDeno 的配置文件同时承担了依赖声明、任务定义和编译选项三件事{ tasks: { dev: deno run --watch --allow-net --allow-env --env-file server.ts, start: deno run --allow-net --allow-env --env-file server.ts, test: deno test --allow-net --allow-env --env-file tests/ }, imports: { oak: https://deno.land/x/oakv13.2.5/mod.ts, pg: https://deno.land/x/postgresv0.19.3/mod.ts }, compilerOptions: { strict: true } }imports里的内容就是 Deno 的依赖管理方式。你把远程模块地址映射成本地短名代码里直接写import { Application } from oak就行。版本都锁死在 URL 里彻底杜绝了 Node 世界里常见的“同一个包装了多个不一致版本”的尴尬。.env文件放数据库连接信息DATABASE_URLpostgresql://yugabyte:yugabyte127.0.0.1:5433/yugabyte PORT8000启动命令里带了--env-fileDeno 会自动读取这个文件并加载进Deno.env。注意如果你用的 Deno 版本比较老可能要用--env-file.env这种带路径的写法具体以当前版本deno run --help里的说明为准。3. 从数据库连接到第一个接口3.1 别被端口 5433 坑了写db.ts之前得先把驱动选清楚。YugabyteDB 兼容 PostgreSQL 线协议所以直接用 Deno 社区的postgres模块就可以不用装任何数据库厂商专属驱动。这也是这个方案最顺手的地方生态是现成的。// db.ts import { Client } from pg; const connectionString Deno.env.get(DATABASE_URL) ?? postgresql://yugabyte:yugabyte127.0.0.1:5433/yugabyte; export const client new Client(connectionString); export async function initDb() { await client.connect(); console.log(数据库连接成功); }这里我直接用了pg这个来自imports映射的模块名。Deno 的 postgres 驱动 API 和 Node 生态的pg包有些相似但类型定义要干净得多。第一次跑这个文件你会立刻碰到 Deno 的权限拦截deno run --allow-net db.ts如果没加--allow-net它会直接报错网络访问被拒绝。这个报错不是为了恶心你而是 Deno 在提醒你这个程序真的需要网络能力请确认你信任它。我之前在团队里推广 Deno 的时候同事最不适应的就是这一步但一周之后大家都承认这种“显式授权”比 Node 里静默开放的模型安全得多。3.2 设计产品表接下来建表。在 YugabyteDB 的 YSQL 里执行以下 SQLCREATE EXTENSION IF NOT EXISTS pgcrypto; CREATE TABLE IF NOT EXISTS products ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), name TEXT NOT NULL, sku TEXT NOT NULL UNIQUE, price NUMERIC(12, 2) NOT NULL CHECK (price 0), stock INT NOT NULL DEFAULT 0 CHECK (stock 0), created_at TIMESTAMPTZ NOT NULL DEFAULT now() );两个细节说一下。gen_random_uuid()在较新的 YugabyteDB 版本里内置但如果你的版本比较旧就得先CREATE EXTENSION pgcrypto。加上那句扩展语句不会有什么副作用所以我的习惯是永远写上。NUMERIC(12, 2)是刻意为之的价格类型。金额计算用浮点数一定会出问题0.1 加 0.2 在二进制浮点里都不精确更别谈账目类数据只能用定点数。这点和 JavaScript 里推荐用整数分存储是同一个道理。在 YugabyteDB 里这张表的数据会自动按照id主键做哈希分片。等这张表数据量上来、节点扩展到三五台机器时应用层完全不用改代码写入和查询会自动分散到各节点。3.3 用 Oak 搭出路由骨架Oak 是 Deno 社区最主流的 Web 框架API 风格受 Koa 启发中间件模型非常顺手。我们不追求花哨先用它把健康检查和路由骨架搭起来。// server.ts import { Application, Router, Status } from oak; import { initDb } from ./db.ts; import { productsRouter } from ./routes/products.ts; import { errorHandler } from ./middleware/errorHandler.ts; export function createApp() { const app new Application(); app.use(errorHandler); app.use(productsRouter.routes()); app.use(productsRouter.allowedMethods()); const router new Router(); router.get(/api/health, (ctx) { ctx.response.body { status: ok, time: new Date().toISOString() }; }); app.use(router.routes()); app.use(router.allowedMethods()); return app; } if (import.meta.main) { await initDb(); const app createApp(); const port Number(Deno.env.get(PORT) ?? 8000); console.log(API 服务已启动: http://localhost:${port}); await app.listen({ port }); }import.meta.main这个判断是关键当文件作为入口直接运行时才会执行启动逻辑当测试文件导入createApp时不会真的监听到端口方便后面做集成测试。为了这一点把“创建应用”和“启动服务”拆开是值得的。3.4 统一错误处理错误处理中间件是整个 API 的兜底网。Oak 里中间件的写法跟 Koa 一脉相承next()之前可以做拦截next()之后的异常会被捕获。// middleware/errorHandler.ts import { Application, Status } from oak; export const errorHandler: Application[use][0] async (ctx, next) { try { await next(); } catch (err) { console.error(未捕获异常:, err); ctx.response.status Status.InternalServerError; ctx.response.body { error: 服务器内部错误 }; } };没有这个中间件的时候任何一个路由里抛出的异常都会把连接挂死甚至导致进程崩溃。有了它之后所有异常都被统一转成 JSON 响应同时日志里保留完整错误堆栈。等线上环境接入监控系统时只要在这个中间件里加上报逻辑即可。为什么这里返回的是通用错误信息而不是具体错误因为直接把数据库异常细节返回给客户端等于帮攻击者做信息收集。日志里详细记录响应里只说“服务器内部错误”这是 API 设计的基本素养。4. 完整 CRUD 与事务场景实操4.1 分页查询加模糊搜索先说查询接口。产品列表一般需要支持关键词搜索、分页和排序我直接用一个接口把它们都兼容进去// routes/products.ts import { Router, Status } from oak; import { client } from ../db.ts; export const productsRouter new Router(); productsRouter.get(/api/products, async (ctx) { const params ctx.request.url.searchParams; const q params.get(q)?.trim() ?? ; const limit Math.min(Number(params.get(limit) ?? 20), 100); const offset Math.max(Number(params.get(offset) ?? 0), 0); const where q ? WHERE name ILIKE $1 : ; const conditions: unknown[] q ? [%${q}%] : []; const sql SELECT id, name, sku, price, stock, created_at FROM products ${where} ORDER BY created_at DESC LIMIT $${conditions.length 1} OFFSET $${conditions.length 2} ; const result await client.queryObject(sql, ...conditions, limit, offset); ctx.response.body { data: result.rows, pagination: { limit, offset, count: result.rows.length }, }; });几个处理细节值得讲。limit做了上限截断最大 100。如果不限制客户端传入limit999999就能把整张表拉走这既是性能事故也是安全漏洞。offset则用Math.max防止负数。ILIKE是 PostgreSQL 系数据库特有的不区分大小写模糊匹配中文搜索也天然支持。但要注意前导通配符%q%会让索引失效数据量大了以后这个查询会变慢。这个问题的解法在数据库侧——用pg_trgm扩展建 GIN 索引YugabyteDB 对这类扩展的支持在不断完善生产环境建议认真评估。queryObject是我推荐的方法返回的行记录是对象而不是数组字段名就是列名省去手工映射。驱动内部会自动处理参数占位符和值的绑定不用自己拼接字符串也就不会踩 SQL 注入的坑。4.2 创建产品与唯一约束处理再来看写入接口这部分坑最多。productsRouter.post(/api/products, async (ctx) { const body await ctx.request.body.json(); const name typeof body?.name string ? body.name.trim() : ; const sku typeof body?.sku string ? body.sku.trim() : ; const price Number(body?.price); const stock Number(body?.stock ?? 0); if (!name || !sku || !Number.isFinite(price) || price 0 || !Number.isInteger(stock) || stock 0) { ctx.response.status Status.BadRequest; ctx.response.body { error: name、sku 必填price 必须为非负数字stock 必须为非负整数 }; return; } try { const result await client.queryObject( INSERT INTO products (name, sku, price, stock) VALUES ($1, $2, $3, $4) RETURNING id, name, sku, price, stock, created_at, name, sku, price, stock, ); ctx.response.status Status.Created; ctx.response.body { data: result.rows[0] }; } catch (err) { if (err instanceof Error err.message.includes(23505)) { ctx.response.status Status.Conflict; ctx.response.body { error: SKU 已存在请更换后重试 }; return; } throw err; } });这里的输入校验是手动写的因为我不想为了演示引入太多抽象。真实项目里可以上 zod 之类的校验库或者 Deno 标准的Typed API方案但手动校验至少把“必填”“类型”“范围”三层逻辑讲清楚了。Number(body?.price)这一步要小心如果用户传的是abcNumber()得到NaNNumber.isFinite能拦住如果传的是nullNumber(null)是 0所以前面body?.price用了可选链取到undefined时Number(undefined)是NaN也能拦住。SKU 冲突的处理是这段代码最值得说的。表上定义了sku TEXT NOT NULL UNIQUE并发条件下两个请求同时插入相同 SKU必然有一个触发唯一约束冲突。PostgreSQL 系的错误码23505就是这个冲突代码里直接通过错误信息判断错误码返回 409 Conflict而不是让异常落到兜底中间件变成 500。这里我还想强调一下“数据库约束 应用层校验”双保险的思路。应用层校验解决的是用户体验数据库约束解决的是数据正确性——哪怕应用层漏判了数据库也会拦下脏数据。别信“应用层已经校验过了”这种话多一层数据库约束永远值得。4.3 库存扣减事务怎么写事务是这套架构最见功底的部分。我们用“购买产品”这个场景先锁行、检查库存、扣减库存、提交事务。任何一步失败都要回滚。productsRouter.post(/api/products/:id/purchase, async (ctx) { const id ctx.params.id; const body await ctx.request.body.json(); const quantity Number(body?.quantity); if (!Number.isInteger(quantity) || quantity 0) { ctx.response.status Status.BadRequest; ctx.response.body { error: quantity 必须是正整数 }; return; } await client.queryArray(BEGIN); try { const locked await client.queryObject( SELECT id, stock FROM products WHERE id $1 FOR UPDATE, id, ); if (locked.rows.length 0) { await client.queryArray(ROLLBACK); ctx.response.status Status.NotFound; ctx.response.body { error: 产品不存在 }; return; } const currentStock Number(locked.rows[0].stock); if (currentStock quantity) { await client.queryArray(ROLLBACK); ctx.response.status Status.Conflict; ctx.response.body { error: 库存不足当前库存 ${currentStock} }; return; } const updated await client.queryObject( UPDATE products SET stock stock - $1 WHERE id $2 RETURNING id, name, stock, quantity, id, ); await client.queryArray(COMMIT); ctx.response.body { data: updated.rows[0] }; } catch (err) { await client.queryArray(ROLLBACK); throw err; } });SELECT ... FOR UPDATE的意思是把这行数据锁住直到事务结束。没有这一步两个并发请求同时读到库存为 10、同时扣 8最后都以为自己扣成功了实际库存只剩 2超卖就发生了。YugabyteDB 在这件事上做得很扎实虽然数据分布在多个节点但跨节点的分布式事务依然保证 ACID事务协调器会确保所有参与分片要么全部提交、要么全部回滚。对业务代码来说你写的还是BEGIN、COMMIT、ROLLBACK这套熟悉的 SQL认知负担没有任何增加。有一点要注意分布式事务因为涉及多节点协调跨分片的写事务延迟会比单机数据库高一些。如果某个业务的核心诉求是“极致低延迟”那就要重新评估但如果是“在可接受延迟范围内获得无限扩展能力”这个代价是值得的。5. 测试、运行与部署5.1 用 deno test 做接口测试Deno 内置了测试运行器不需要装 Jest 或 Mocha。我们可以做真正的接口级集成测试启动应用、发 HTTP 请求、断言响应。// tests/api.test.ts import { assertEquals } from https://deno.land/std0.224.0/assert/mod.ts; import { createApp } from ../server.ts; import { client } from ../db.ts; const TEST_PORT 18000; Deno.test({ name: GET /api/health 返回健康状态, async fn() { await client.connect(); const app createApp(); const controller new AbortController(); const listenPromise app.listen({ port: TEST_PORT, signal: controller.signal }); await new Promise((resolve) setTimeout(resolve, 200)); try { const res await fetch(http://localhost:${TEST_PORT}/api/health); assertEquals(res.status, 200); const body await res.json(); assertEquals(body.status, ok); } finally { controller.abort(); await listenPromise; await client.end(); } }, });跑测试的命令已经写在deno.json的 tasks 里了deno task test这个测试写法的好处是它跑的是真实路由、真实数据库连接、真实的 HTTP 栈比单元测试更接近线上行为。坏处是依赖数据库必须处于运行状态所以 CI 里要加一步“先拉起 YugabyteDB 再跑测试”。你可以把启动数据库的命令写进 CI 脚本。第一次跑测试大概率会遇到权限问题因为测试进程发起网络请求、读取环境变量所以 tasks 里带了--allow-net --allow-env。这个细节很容易被忽略排查方法我放在最后一节统一讲。5.2 生产环境启动与编译开发时用--watch模式改代码自动重启deno task dev生产环境直接用deno task start就够了。如果想要更彻底的可移植性可以用deno compile把整个项目编译成单个可执行文件deno compile --allow-net --allow-env --env-file -o api-server server.ts这个命令会产出一个不含任何外部依赖的原生可执行文件连 Deno 运行时本身都打进去了。部署到服务器上直接运行./api-server就行不需要目标机器安装 Deno这对容器镜像瘦身和快速分发非常有价值。编译出来的文件我实测过启动速度比脚本模式快不少因为省去了依赖解析和代码编译阶段。注意--env-file这个标志在deno compile里的支持情况随版本变化如果你用的版本不支持就把环境变量直接注入到宿主环境代码本身不受影响。5.3 容器化与发布容器化部署也很直接FROM denoland/deno:alpine-2.0.2 WORKDIR /app COPY deno.json . COPY server.ts db.ts ./ COPY routes/ routes/ COPY middleware/ middleware/ RUN deno cache server.ts EXPOSE 8000 CMD [run, --allow-net, --allow-env, --env-file, server.ts]deno cache这步是预拉取依赖并生成缓存目的跟 Node 构建镜像时的npm ci一样都是为了利用 Docker 分层缓存只要源码没变依赖层就不会重建构建速度快一大截。数据库层面开发环境用yugabyted本地单节点生产环境建议用多节点集群或者托管服务。好在 API 代码完全不关心数据库部署形态——因为走的是 PostgreSQL 协议。当你需要从单机数据库迁移到分布式数据库时代码几乎不用动这可比某些“云原生数据库”动辄要求你改 ORM 或者换掉整套驱动要舒服得多。6. 常见问题与排查技巧6.1 权限模型带来的报错Deno 的权限错误是新手遇到最多的拦路虎。典型报错信息类似error: Uncaught (in promise) PermissionDenied: network access to 127.0.0.1:5433, run again with the --allow-net flag解决办法不是真的让你把--allow-net傻傻加上就好。我的建议是这样的第一步先用--allow-all临时跑通验证逻辑第二步根据报错信息逐个收窄权限第三步把最终确认的最小权限集合固化到deno.json的 tasks 里。这样既保证了开发速度又不至于在线上放开所有权限。还要注意--allow-env本身是个粗粒度授权读任何环境变量都允许。如果你的应用有敏感的环境变量Deno 支持--allow-envDATABASE_URL,PORT这种细化写法建议生产环境按需收紧。6.2 连接池怎么用我演示用的Client是单连接适合开发环境。生产环境并发一上来单连接会成为瓶颈必须换成连接池。import { Pool } from pg; const pool new Pool(connectionString, 10); export async function query(text: string, ...args: unknown[]) { return pool.queryObject(text, ...args); }postgres驱动的Pool会维护一批空闲连接按需分配、用完归还。并发请求多的时候这个机制能把数据库连接数控制在一个合理水位不会无限增长把 YugabyteDB 的进程压垮。连接池大小怎么定一个粗略的公式是连接数 核数 x 2 磁盘数。这也就是 PostgreSQL 官方文档里给的经验值。太小了并发一高就排队太大了反而因为上下文切换和锁竞争拖慢数据库。我在项目里通常从 10 开始压测后逐步调整。还有一个坑连接池一旦初始化进程退出时必须优雅关闭否则连接会持续占用直到数据库侧超时踢掉。所以在server.ts里可以监听退出信号调用pool.end()。6.3 数据类型与时区的坑这部分是我最想吐槽的也是最值得收藏的。第一个坑NUMERIC类型在驱动里返回的不是 number而是字符串。比如price查出来是19.90而不是19.9。这其实是驱动在保护精度——NUMERIC(12,2)最高可能超过 JavaScript 安全整数范围。但如果你不处理前端拿到字符串一定会懵。解决方法是后端在序列化前统一转成 number或者在前端约定用字符串展示金额很多金融系统确实这么干。第二个坑时区。TIMESTAMPTZ类型存的是 UTC 时间驱动返回的是 JavaScript 的Date对象序列化后是 ISO 8601 格式。如果你的 API 调用方在别的时区展示时必须自己做本地化后端不要手动拼接时间字符串否则各个时区的用户看到的时间全是错的。第三个坑queryArray和queryObject的返回值结构不一样。前者result.rows是二维数组后者是对象数组。混用时很容易写错索引或者字段名我建议一个项目里统一用queryObject。第四个坑ILIKE 模糊搜索的索引失效。%关键词%这种写法在 PostgreSQL 家族里索引基本用不上表数据量大以后是全表扫描。优化方向是使用pg_trgm扩展或者改用专门的全文检索方案。YugabyteDB 的全文检索能力和单机 PostgreSQL 略有差距如果搜索是核心功能建议架构层就规划好搜索引擎。还有一个跟平台相关的小坑YugabyteDB 的默认端口 5433 和默认用户名yugabyte经常被当成标准 PostgreSQL 配置来写连接时才发现端口拒了。我的建议是把DATABASE_URL写进.env所有代码和文档统一引用这个变量永远别硬编码端口。6.4 什么情况不建议这套组合说了这么多好处也得把丑话说在前面。如果你确定业务就是个小规模内部工具单机 PostgreSQL 完全够用那直接上 YugabyteDB 属于过度设计运维复杂度并不低。如果你们的部署环境极其特殊、必须用某些 Node 专属依赖Deno 的 npm 兼容层虽然已经不错但偶尔还是有边界情况。还有如果团队里没有人熟悉 PostgreSQL 生态学习成本也得算进决策。架构选型没有银弹。我推荐这套组合是因为它在“开发体验”和“扩展能力”上做到了两全但真正生产落地前一定要拿自己的业务场景压测验证别因为一篇博客就拍板。写在最后这套组合我实际用了大概半年最大感受是Deno 的工程化体验改善是体验级别的不是纸面参数。内置测试、格式化、Lint 和编译让一个小团队不用再拼凑七八个工具就能把工程质量做到位。而 YugabyteDB 最让我安心的地方不是它有多炫技而是我写的还是熟悉的 PostgreSQL 语法事务依然完整但数据层突然有了横向扩展的余地。最后再分享一个我自己的习惯拿到新技术不要一上来就规划大型重构先用最小业务场景把全链路跑通——就像这篇文章里的产品管理 API从建表到事务到测试到部署一天之内全部打通。这条路走顺了再决定要不要让核心业务迁移过来。技术选型和做饭很像试菜永远比看菜谱靠谱。
返回列表