数据库连接管理
要点
- 数据库连接是昂贵资源,必须使用连接池复用
- 连接池大小需要根据服务器配置和数据库配置调整
- Edge 环境(Workers、Deno Deploy)不能直接连接传统数据库,需要远程连接或 Serverless 方案
- 连接泄漏会导致数据库连接耗尽,需要预防和监控
- 优雅关闭时应该断开数据库连接
- 生产环境需要监控连接使用情况
1. 为什么需要连接池
数据库连接涉及 TCP 握手、认证、初始化等步骤,创建成本高。如果每个请求都创建新连接,性能会很差。
连接池维护一组预先建立的连接,请求从池里借用连接,用完后归还。这样避免了频繁创建和销毁连接。
2. 配置连接池
2.1 pg (PostgreSQL)
import { Pool } from 'pg'
const pool = new Pool({
connectionString: process.env.DATABASE_URL,
max: 20, // 最大连接数
idleTimeoutMillis: 30000, // 空闲连接超时(30 秒)
connectionTimeoutMillis: 2000, // 获取连接超时(2 秒)
})2.2 mysql2 (MySQL)
import { createPool } from 'mysql2/promise'
const pool = await createPool({
uri: process.env.DATABASE_URL,
connectionLimit: 20,
idleTimeout: 30000,
connectTimeout: 2000,
})2.3 better-sqlite3 (SQLite)
SQLite 是嵌入式数据库,不需要连接池。
3. 连接池大小
连接池大小(max / connectionLimit)需要根据实际情况调整:
计算公式:
连接数 = CPU 核心数 × 2 + 磁盘数
一般范围:
- 小型应用:5-10 个连接
- 中型应用:10-20 个连接
- 大型应用:20-50 个连接
过多连接的问题:
- 数据库上下文切换开销大
- 内存占用高
- 性能下降
过少连接的问题:
- 请求排队等待连接
- 响应变慢
- 超时错误
调整方法:
- 监控数据库的活跃连接数
- 监控应用的连接等待时间
- 根据负载调整连接池大小
4. Edge 环境连接
Edge 环境(Cloudflare Workers、Deno Deploy、Vercel Edge)不能直接连接传统数据库,因为:
- 没有持久的 TCP 连接
- 冷启动频繁,连接池无法保持
- 地理分布广,连接延迟高
4.1 Serverless 数据库
专门为 Serverless 设计的数据库:
- Neon:Serverless PostgreSQL
- PlanetScale:Serverless MySQL
- Turso:Serverless SQLite(基于 libSQL)
- Cloudflare D1:Workers 原生 SQLite
// Neon (PostgreSQL)
import { neon } from '@neondatabase/serverless'
const sql = neon(process.env.DATABASE_URL!)
const users = await sql`SELECT * FROM users`
// Turso (SQLite)
import { createClient } from '@libsql/client'
const client = createClient({
url: process.env.TURSO_URL!,
authToken: process.env.TURSO_TOKEN,
})
const result = await client.execute('SELECT * FROM users')4.2 远程连接
通过 HTTP 或 gRPC 代理连接传统数据库:
- Prisma Data Proxy:Prisma 提供的代理,支持 Edge
- Xata:PostgreSQL + HTTP API
- Supabase:PostgreSQL + REST API
// Prisma Data Proxy
import { PrismaClient } from '@prisma/client'
const prisma = new PrismaClient({
datasources: {
db: {
url: process.env.DATABASE_URL_PROXY, // 代理 URL
},
},
})4.3 Cloudflare D1
D1 是 Cloudflare Workers 原生的 SQLite 服务:
// wrangler.toml
[[d1_databases]]
binding = "DB"
database_name = "hono_app"
database_id = "xxx"// src/index.ts
import { Hono } from 'hono'
import { drizzle } from 'drizzle-orm/d1'
export interface Env {
DB: D1Database
}
const app = new Hono<{ Bindings: Env }>()
app.get('/users', async (c) => {
const db = drizzle(c.env.DB)
const users = await db.select().from(usersTable)
return c.json(users)
})
export default app5. 连接泄漏
连接泄漏是指连接被借用后没有归还,导致池里的可用连接越来越少,最终耗尽。
5.1 常见原因
- 异常未捕获:查询抛出异常,连接没有归还
- 长时间运行的查询:查询执行时间过长,连接被占用
- 手动获取连接未释放:使用
pool.connect()但没有调用client.release()
5.2 预防
使用 ORM 或池的查询方法:
// ❌ 手动获取连接,容易忘记释放
const client = await pool.connect()
try {
await client.query('SELECT * FROM users')
} finally {
client.release()
}
// ✅ 使用池的查询方法,自动释放
await pool.query('SELECT * FROM users')捕获异常:
app.get('/users', async (c) => {
try {
const users = await db.select().from(users)
return c.json(users)
} catch (err) {
console.error('Database error:', err)
return c.json({ error: 'Database error' }, 500)
}
})5.3 监控
监控连接池的使用情况:
// pg
console.log('Total connections:', pool.totalCount)
console.log('Idle connections:', pool.idleCount)
console.log('Waiting requests:', pool.waitingCount)如果 waitingCount 持续增加,说明连接不够用或有泄漏。
6. 优雅关闭
应用关闭时应该断开数据库连接,避免连接被强制关闭:
import { Pool } from 'pg'
const pool = new Pool({ connectionString: process.env.DATABASE_URL })
// Node.js
process.on('SIGTERM', async () => {
console.log('SIGTERM received, closing database connections...')
await pool.end()
process.exit(0)
})
process.on('SIGINT', async () => {
console.log('SIGINT received, closing database connections...')
await pool.end()
process.exit(0)
})// Hono
import { Hono } from 'hono'
const app = new Hono()
// 优雅关闭
app.use('*', async (c, next) => {
await next()
// 请求完成后不做任何事
})
// 在应用外层处理
const server = serve({ fetch: app.fetch, port: 8787 })
process.on('SIGTERM', async () => {
server.stop()
await pool.end()
})7. 重试逻辑
数据库操作可能因为网络波动、连接超时等原因失败。添加重试逻辑可以提高稳定性:
async function withRetry<T>(
fn: () => Promise<T>,
maxRetries = 3,
baseDelay = 100,
): Promise<T> {
for (let i = 0; i < maxRetries; i++) {
try {
return await fn()
} catch (err) {
if (i === maxRetries - 1) throw err
const delay = baseDelay * Math.pow(2, i)
await new Promise((resolve) => setTimeout(resolve, delay))
}
}
throw new Error('Unreachable')
}
// 使用
const users = await withRetry(() => db.select().from(users))8. 超时配置
查询超时可以防止慢查询占用连接过久:
const pool = new Pool({
connectionString: process.env.DATABASE_URL,
statement_timeout: 5000, // 查询超时 5 秒
idle_in_transaction_session_timeout: 10000, // 事务空闲超时 10 秒
})9. 生产环境检查清单
- 连接池配置:
max、idleTimeoutMillis、connectionTimeoutMillis - 超时配置:
statement_timeout、idle_in_transaction_session_timeout - 错误处理:所有数据库操作都有 try-catch
- 优雅关闭:SIGTERM/SIGINT 时断开连接
- 监控:连接池使用情况、慢查询、错误率
- 备份:定期备份数据库
- 迁移:使用迁移工具管理 schema 变更
总结
数据库连接管理是生产环境的必备配置。连接池复用连接、Edge 环境需要特殊方案、连接泄漏需要预防和监控。
这一节涉及到的几个实践:
- 连接池:复用连接,避免频繁创建和销毁
- 连接池大小:根据 CPU 核心数、负载调整
- Edge 环境:Serverless 数据库、远程连接、D1
- 连接泄漏:使用 ORM、捕获异常、监控连接数
- 优雅关闭:SIGTERM/SIGINT 时断开连接
- 重试逻辑:指数退避重试失败的操作
- 超时配置:防止慢查询占用连接过久
良好的连接管理可以提高应用稳定性和性能。
下一篇看事务处理——事务 ACID、Prisma/Drizzle 事务 API、隔离级别。