数据库连接管理

要点

  • 数据库连接是昂贵资源,必须使用连接池复用
  • 连接池大小需要根据服务器配置和数据库配置调整
  • 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 个连接

过多连接的问题:

  • 数据库上下文切换开销大
  • 内存占用高
  • 性能下降

过少连接的问题:

  • 请求排队等待连接
  • 响应变慢
  • 超时错误

调整方法:

  1. 监控数据库的活跃连接数
  2. 监控应用的连接等待时间
  3. 根据负载调整连接池大小

4. Edge 环境连接

Edge 环境(Cloudflare Workers、Deno Deploy、Vercel Edge)不能直接连接传统数据库,因为:

  1. 没有持久的 TCP 连接
  2. 冷启动频繁,连接池无法保持
  3. 地理分布广,连接延迟高

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 app

5. 连接泄漏

连接泄漏是指连接被借用后没有归还,导致池里的可用连接越来越少,最终耗尽。

5.1 常见原因

  1. 异常未捕获:查询抛出异常,连接没有归还
  2. 长时间运行的查询:查询执行时间过长,连接被占用
  3. 手动获取连接未释放:使用 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. 生产环境检查清单

  1. 连接池配置maxidleTimeoutMillisconnectionTimeoutMillis
  2. 超时配置statement_timeoutidle_in_transaction_session_timeout
  3. 错误处理:所有数据库操作都有 try-catch
  4. 优雅关闭:SIGTERM/SIGINT 时断开连接
  5. 监控:连接池使用情况、慢查询、错误率
  6. 备份:定期备份数据库
  7. 迁移:使用迁移工具管理 schema 变更

总结

数据库连接管理是生产环境的必备配置。连接池复用连接、Edge 环境需要特殊方案、连接泄漏需要预防和监控。

这一节涉及到的几个实践:

  1. 连接池:复用连接,避免频繁创建和销毁
  2. 连接池大小:根据 CPU 核心数、负载调整
  3. Edge 环境:Serverless 数据库、远程连接、D1
  4. 连接泄漏:使用 ORM、捕获异常、监控连接数
  5. 优雅关闭:SIGTERM/SIGINT 时断开连接
  6. 重试逻辑:指数退避重试失败的操作
  7. 超时配置:防止慢查询占用连接过久

良好的连接管理可以提高应用稳定性和性能。

下一篇看事务处理——事务 ACID、Prisma/Drizzle 事务 API、隔离级别。