26年bunjs, elysia+pg一把梭, redis都省了

Node 启动慢,Express 路由散。TypeORM 写个 CRUD 三个文件。Java Spring 注解比代码还多。PHP 老项目维护,像在考古。想要启动快,类型好。还得跑得动高并发。数据库得原生,别给我加太多魔法。一套就够:

bash 复制代码
import { Elysia } from 'elysia'
import { Pool } from 'pg'

const pool = new Pool({
  connectionString: 'postgres://app:app@localhost/app'
})

new Elysia()
  .get('/users', async () => {
    const { rows } = await pool.query('select id, name from users')
    return rows
  })
  .listen(3000)

bun run app.ts 一下。10 行代码,一个接口搞定。启动 30 毫秒,类型端到端。SQL 直接写,没 ORM 那一层。这就是 Bun + Elysia + pg 的解法。

Bun 是答案。原生 TS,启动 30 毫秒。自带 test、bundler、package manager。一个二进制,替代一堆工具。

Elysia 是 Bun 上的 Web 框架。端到端类型推导。编辑器直接补全。比 Express 快 5 到 21 倍。比 NestJS 轻十倍。不挑数据库,你想用啥就用啥。

pg 是 Node 圈最稳的 PG 驱动。没有 ORM 那一层魔法。SQL 写啥就跑啥。出问题了,一眼能看清。

为啥不接 ORM?电商业务表多,关系复杂。ORM 给你自动 join,效率反而低。直接 SQL,心里踏实。

上代码。

一、最小可用服务

bash 复制代码
import { Elysia } from 'elysia'
import { Pool } from 'pg'

const pool = new Pool({
  host: '127.0.0.1',
  port: 5432,
  user: 'app',
  password: 'app',
  database: 'app'
})

const app = new Elysia()
  .get('/', () => '老铁你好')
  .get('/users', async () => {
    const { rows } = await pool.query(
      'select id, name from users order by id desc'
    )
    return rows
  })
  .listen(3000)

console.log(`跑在 ${app.server!.url}`)

bun run app.ts 一下就起来。不用等 Tomcat。不用配 Nginx + PHP-FPM。不用 Spring 那一坨 XML。一个文件,就是一个服务。

二、POST 接口 + 参数校验

手写 SQL 太累?拼字符串又怕注入?Elysia 自带 validation。

bash 复制代码
import { Elysia, t } from 'elysia'

const app = new Elysia()
  .post('/users', async ({ body }) => {
    const { rows } = await pool.query(
      'insert into users(name) values($1) returning id, name',
      [body.name]
    )
    return rows[0]
  }, {
    body: t.Object({
      name: t.String({ minLength: 1, maxLength: 50 })
    })
  })
  .listen(3000)

body 进来就是类型好的。不合法直接 422。if 判断全省了。类型就是文档,省心。

三、项目大了,路由怎么拆

老项目最怕啥?一个文件 3000 行。找个接口按 Ctrl+F 半天。

Elysia 用 use 拆模块。

bash 复制代码
// src/modules/user.ts
import { Elysia } from 'elysia'

export const userModule = new Elysia({ prefix: '/user' })
  .get('/', () => 'user list')
  .get('/:id', ({ params }) => `user id: ${params.id}`)
  .post('/', ({ body }) => body)

// src/index.ts
import { Elysia } from 'elysia'
import { userModule } from './modules/user'

const app = new Elysia()
  .use(userModule)
  .listen(3000)

prefix 一加,路径自动拼。模块之间不打架。比 Spring 的 @RequestMapping 还省事。

四、连接池怎么管

Bun 起多实例。PG 连接要复用。pg.Pool 默认就是连接池。但参数得调一下。

bash 复制代码
const pool = new Pool({
  host: process.env.PG_HOST,
  port: Number(process.env.PG_PORT ?? 5432),
  user: process.env.PG_USER,
  password: process.env.PG_PASSWORD,
  database: process.env.PG_DB,
  max: 20,
  idleTimeoutMillis: 30000
})

max 控并发。idleTimeoutMillis 释放空闲。K8s 里 pod 频繁重启。连接不能漏。Bun 退出 hook 会清。

五、事务怎么写

电商下单,最怕啥?钱扣了,货没发。事务没写好,赔钱又挨骂。

PG 原生 BEGIN / COMMIT

bash 复制代码
import { Elysia, t } from 'elysia'

const app = new Elysia()
  .post('/order', async ({ body }) => {
    const client = await pool.connect()
    try {
      await client.query('BEGIN')
      await client.query(
        'update accounts set money = money - $1 where id = $2',
        [body.amount, body.from]
      )
      await client.query(
        'update accounts set money = money + $1 where id = $2',
        [body.amount, body.to]
      )
      await client.query('COMMIT')
      return { ok: true }
    } catch (e) {
      await client.query('ROLLBACK')
      throw e
    } finally {
      client.release()
    }
  }, {
    body: t.Object({
      from: t.Number(),
      to: t.Number(),
      amount: t.Number({ minimum: 1 })
    })
  })
  .listen(3000)

简单粗暴。没 ORM 那一层封装。出问题一眼能看清。比 MySQL 的事务稳。

六、监控怎么办

运维大哥天天问:QPS 多少?延迟多少?错误率涨没涨?

Elysia 自带生命周期钩子。

bash 复制代码
import { Elysia } from 'elysia'

const app = new Elysia()
  .onRequest(({ request }) => {
    console.log(`-> ${request.method} ${request.url}`)
  })
  .onAfterHandle(({ set }) => {
    console.log(`<- ${set.status}`)
  })
  .onError(({ code, error }) => {
    console.error('出事儿了:', error)
  })
  .get('/', () => 'ok')

要接 Prometheus 也简单。@elysiajs/prometheus 一行 .use() 就行。不用自己写中间件。

七、几个避坑点

写了一年,踩了不少坑。说几个常见的。

1. 千万别用字符串拼接 SQL

bash 复制代码
// 错 ❌
await pool.query(
  `select * from users where name = '${name}'`
)

// 对 ✅
await pool.query(
  'select * from users where name = $1',
  [name]
)

$1 是参数化。注入风险直接没了。

2. 时间用 timestamptz,别用 timestamp

bash 复制代码
create table orders (
  id serial primary key,
  user_id int not null,
  amount numeric(10, 2) not null,
  created_at timestamptz not null default now()
);

timestamp 没时区。跨时区业务,必踩坑。timestamptz 才靠谱。

3. JSON 字段用 jsonb

bash 复制代码
create table products (
  id serial primary key,
  name text not null,
  attrs jsonb not null default '{}'::jsonb
);

jsonb 能建索引。json 不行。商品属性、配置项,用 jsonb

4. 记得建索引

bash 复制代码
create index idx_orders_user_id on orders(user_id);
create index idx_orders_created_at on orders(created_at desc);

没索引的 PG,慢得跟 MySQL 一样。索引建对了,快得飞起。

八、啥时候 pg 能替 redis

不少项目,redis 就是为了缓存。session 也是。限流也是。排队也是。pub/sub 也是。

但你真用了吗?装 redis 麻烦。运维要搭集群。内存贵。吃资源。

PG 自带这些功能。不用装第二个服务。省钱。省事。

下面几种场景,PG 完全能替。

1. 简单 KV 缓存

不用 redis 的 hash,不用 redis 的 list。就 KV 一把梭。

bash 复制代码
create table if not exists cache_kv (
  key text primary key,
  value jsonb not null,
  expire_at timestamptz
);

写入:

bash 复制代码
await pool.query(
  `insert into cache_kv(key, value, expire_at)
   values($1, $2, $3)
   on conflict (key) do update set
     value = excluded.value,
     expire_at = excluded.expire_at`,
  [key, JSON.stringify(value), new Date(Date.now() + 60_000)]
)

读取(自动过滤过期):

bash 复制代码
const r = await pool.query(
  `select value from cache_kv
   where key = $1
     and (expire_at is null or expire_at > now())`,
  [key]
)
const value = r.rows[0] ? JSON.parse(r.rows[0].value) : null

60 秒过期,够用。电商商品列表、配置项,都这么干。

2. Session 存储

redis 存 session 太常见。PG 也能存。还多一份备份。

bash 复制代码
create table if not exists sessions (
  id text primary key,
  user_id int not null,
  data jsonb not null,
  expire_at timestamptz not null
);

写入:

bash 复制代码
await pool.query(
  `insert into sessions(id, user_id, data, expire_at)
   values($1, $2, $3, $4)
   on conflict (id) do update set
     data = excluded.data,
     expire_at = excluded.expire_at`,
  [token, userId, JSON.stringify(data), new Date(Date.now() + 86_400_000)]
)

读 session:

bash 复制代码
const { rows } = await pool.query(
  `select user_id, data from sessions
   where id = $1 and expire_at > now()`,
  [token]
)

定时清理:

bash 复制代码
await pool.query(`delete from sessions where expire_at < now()`)

3. 分布式锁

秒杀、抢单,最怕并发。PG 自带 advisory lock。一行 SQL 一个锁。

bash 复制代码
// 加锁
await pool.query('select pg_advisory_lock($1)', [lockKey])

try {
  // 业务逻辑
  await doOrder()
} finally {
  // 释放
  await pool.query('select pg_advisory_unlock($1)', [lockKey])
}

或者非阻塞版:

bash 复制代码
const r = await pool.query(
  'select pg_try_advisory_lock($1) as got',
  [lockKey]
)
if (!r.rows[0].got) {
  throw new Error('排队中')
}

比 redis 的 setnx 还简单。

4. Pub/Sub

实时通知、订单状态推送。不用装 redis。PG 的 LISTEN/NOTIFY 就够。

bash 复制代码
import { Client } from 'pg'

// 订阅
const sub = new Client({
  host: '127.0.0.1',
  port: 5432,
  user: 'app',
  password: 'app',
  database: 'app'
})
await sub.connect()
await sub.query('LISTEN order_event')

sub.on('notification', (msg) => {
  console.log('收到:', msg.payload)
})

发布:

bash 复制代码
await pool.query(
  `select pg_notify($1, $2)`,
  ['order_event', JSON.stringify({ orderId: 123, status: 'paid' })]
)

小项目、内部通知,够用。千万级消息流不行。那老老实实上 redis 或 kafka。

5. 任务队列

电商订单要异步发货。PG 也能排队。

bash 复制代码
create table jobs (
  id bigserial primary key,
  type text not null,
  payload jsonb not null,
  status text not null default 'pending',
  retry int not null default 0,
  created_at timestamptz not null default now()
);

worker 抢任务:

bash 复制代码
const { rows } = await pool.query(`
  update jobs
  set status = 'running', locked_at = now()
  where id = (
    select id from jobs
    where status = 'pending'
    order by id
    for update skip locked
    limit 1
  )
  returning *
`)

if (rows[0]) {
  await handle(rows[0])
  await pool.query(`update jobs set status = 'done' where id = $1`, [rows[0].id])
}

SKIP LOCKED 是关键。多个 worker 不抢同一个任务。比 redis 的 brpop 简单。失败还能重试,数据不丢。

6. 限流

防刷、防爆。PG 也能做。

bash 复制代码
create table if not exists rate_limit (
  key text primary key,
  count int not null default 0,
  window_start timestamptz not null default now()
);
bash 复制代码
const { rows } = await pool.query(`
  insert into rate_limit(key, count, window_start)
  values($1, 1, now())
  on conflict (key) do update set
    count = case
      when rate_limit.window_start < now() - interval '1 minute'
      then 1
      else rate_limit.count + 1
    end,
    window_start = case
      when rate_limit.window_start < now() - interval '1 minute'
      then now()
      else rate_limit.window_start
    end
  returning count
`, [ipKey])

if (rows[0].count > 100) {
  throw new Error('请求太快')
}

简单粗暴。QPS 万级以内够用。


啥时候别替

PG 不是万能的。下面场景老老实实上 redis:

  • • 百万级 QPS 缓存

  • • 复杂数据结构(sorted set、bitmap)

  • • 大 key 大 value

  • • 强内存计算

  • • 全内存热数据

PG 优势在关系和事务。纯缓存场景,redis 更快。

但中小项目,流量没上百万。业务还要事务。数据还要持久。

PG 一把梭。省一个服务。省一份钱。省一份运维。

总结一下

Bunjs 启动快,原生 TS。Elysia 类型好,性能强。pg 是 Node 圈最稳的 PG 驱动。这三样加一起。就是 2026 的后端答案。

写电商写 OA 写 CRM。统统能扛。启动 30 毫秒,并发扛得住。类型端到端,BUG 少一半。

AI 写代码现在确实多。但框架选错了,AI 越写越乱。选对了,AI 帮你提速三倍。这套组合,省心。

要整就整这套。别的真不行。

相关推荐
葫芦和十三9 小时前
图解 MongoDB 19|Oplog:复制的真正载体,不是文档是操作
后端·mongodb·agent
葫芦和十三10 小时前
图解 MongoDB 20|复制延迟与 catch up:Secondary 为什么跟不上
后端·mongodb·agent
lichenyang45314 小时前
Docker 学习笔记(一):为什么需要镜像、容器和仓库?
前端
kyriewen14 小时前
别再对着 TypeScript 报错发呆了:我把 10 个最常见的红色波浪线翻译成了人话
前端·javascript·typescript
IT_陈寒14 小时前
SpringBoot自动配置的坑,我的API突然就404了
前端·人工智能·后端
free3515 小时前
从 0 实现一个 Tiny JavaScript VM:项目架构拆解
javascript
ServBay15 小时前
为什么说 MCP 是 2026 年开发者必须掌握的黄金协议?
后端·mcp