本文 AI 產出,尚未審核
ExpressJS (TypeScript) – 效能優化:資料庫連線池(Connection Pool)
簡介
在 Node.js 與 Express 搭配 TypeScript 開發 Web API 時,資料庫的存取往往是系統效能的瓶頸。每一次請求若都直接建立與關閉資料庫連線,除了耗費大量 CPU 與 I/O 時間,還會導致 連線數過多、資源耗盡,最終讓服務不穩定甚至崩潰。
為了解決這個問題,連線池(Connection Pool) 應運而生。它透過事先建立固定數量的連線,讓每一次的查詢都能從池中快速取得可用連線,使用完畢後再歸還,從而大幅降低建立/銷毀連線的開銷,提升整體吞吐量與響應速度。
本篇文章將從概念、實作、常見陷阱與最佳實踐,帶你一步一步在 Express + TypeScript 專案中正確使用資料庫連線池,讓你的 API 在高併發情境下依然保持穩定與快速。
核心概念
1. 什麼是連線池?
- 連線池 是一組已經建立好的資料庫連線集合,程式在需要執行 SQL 時,從池中「借」出一條連線,執行完畢後再「還」回去。
- 主要目標是 重用 連線,減少每次請求的
connect → query → disconnect三段式流程。 - 連線池同時也會負責 健康檢查(例如 ping、idle timeout)以及 自動擴縮(根據設定的最小/最大連線數動態調整)。
2. 為什麼在 Express 中一定要用?
| 沒有使用連線池 | 使用連線池 |
|---|---|
| 每個請求都建立新連線 → 延遲 高、CPU 消耗大 | 直接取用現有連線 → 毫秒級 回應 |
| 連線數量不受控 → 可能超過 DB 上限,導致 Too many connections | 連線數受 max 限制,避免 DB 被打垮 |
| 連線資源釋放不當 → 產生 leak,長時間運行後資源枯竭 | 連線自動回收、檢測,降低 leak 風險 |
3. 常見的連線池套件
| 資料庫 | 主流套件 | TypeScript 支援 |
|---|---|---|
| PostgreSQL | pg(內建 pool) |
✅(型別定義已內建) |
| MySQL / MariaDB | mysql2(內建 pool) |
✅(型別定義已內建) |
| SQLite(較少使用) | better-sqlite3(不支援 pool) |
❌ |
| 多種 DB | generic-pool(自訂 pool) |
✅(需要自行宣告型別) |
| ORM | TypeORM、Prisma、Sequelize |
✅(內建 pool) |
以下將以 PostgreSQL 與 MySQL 為例,示範在 Express + TypeScript 中如何建立與使用連線池。
程式碼範例
1️⃣ 基礎的 PostgreSQL 連線池(pg)
// src/db/postgresPool.ts
import { Pool, PoolClient } from 'pg';
import { config } from 'dotenv';
config(); // 載入 .env
// 建立一個全域的 Pool 實例
export const pgPool = new Pool({
host: process.env.PG_HOST,
port: Number(process.env.PG_PORT),
user: process.env.PG_USER,
password: process.env.PG_PASSWORD,
database: process.env.PG_DATABASE,
// 連線池設定
max: 20, // 最多同時 20 條連線
idleTimeoutMillis: 30000, // 空閒 30 秒自動關閉
connectionTimeoutMillis: 2000, // 2 秒內無法取得連線即拋錯
});
// 取得連線的便利函式(自動回收)
export async function query<T>(text: string, params?: any[]): Promise<T[]> {
const client: PoolClient = await pgPool.connect();
try {
const res = await client.query<T>(text, params);
return res.rows;
} finally {
client.release(); // **務必釋放連線**,否則會造成 leak
}
}
重點:
client.release()必須放在finally區塊,確保不管查詢成功或失敗,都會把連線還回池中。
2️⃣ 在 Express Middleware 中使用連線池
// src/middleware/dbMiddleware.ts
import { Request, Response, NextFunction } from 'express';
import { pgPool } from '../db/postgresPool';
// 為每個請求掛載一個可重用的 client
export async function dbMiddleware(req: Request, res: Response, next: NextFunction) {
const client = await pgPool.connect();
// 把 client 存在 req 中,供後續路由使用
(req as any).dbClient = client;
// 回傳時自動釋放
res.on('finish', () => {
client.release();
});
next();
}
// src/routes/user.ts
import { Router, Request, Response } from 'express';
const router = Router();
router.get('/users/:id', async (req: Request, res: Response) => {
const client = (req as any).dbClient;
const { id } = req.params;
const result = await client.query('SELECT * FROM users WHERE id = $1', [id]);
res.json(result.rows[0]);
});
export default router;
技巧:使用
res.on('finish')監聽回應結束,保證即使發生例外,也會釋放連線。
3️⃣ MySQL 連線池(mysql2)
// src/db/mysqlPool.ts
import mysql, { Pool, PoolConnection } from 'mysql2/promise';
import { config } from 'dotenv';
config();
export const mysqlPool: Pool = mysql.createPool({
host: process.env.MYSQL_HOST,
port: Number(process.env.MYSQL_PORT),
user: process.env.MYSQL_USER,
password: process.env.MYSQL_PASSWORD,
database: process.env.MYSQL_DATABASE,
waitForConnections: true,
connectionLimit: 15, // 最大 15 條連線
queueLimit: 0, // 無限制排隊
// 其他設定可參考官方文件
});
// 直接在服務層使用
export async function query<T>(sql: string, params?: any[]): Promise<T[]> {
const connection: PoolConnection = await mysqlPool.getConnection();
try {
const [rows] = await connection.execute<T>(sql, params);
return rows as T[];
} finally {
connection.release(); // **釋放連線** 很重要
}
}
4️⃣ 使用 generic-pool 建立自訂連線池(適用不同 DB)
// src/db/customPool.ts
import { createPool, Pool as GenericPool } from 'generic-pool';
import { Client } from 'pg';
// 建立 factory
const factory = {
create: async () => {
const client = new Client({
host: process.env.PG_HOST,
port: Number(process.env.PG_PORT),
user: process.env.PG_USER,
password: process.env.PG_PASSWORD,
database: process.env.PG_DATABASE,
});
await client.connect();
return client;
},
destroy: async (client: Client) => {
await client.end();
},
};
// 建立 Generic Pool
export const pgGenericPool: GenericPool<Client> = createPool(factory, {
max: 10,
min: 2,
idleTimeoutMillis: 30000,
});
// 使用方式
export async function genericQuery<T>(sql: string, params?: any[]): Promise<T[]> {
const client = await pgGenericPool.acquire();
try {
const res = await client.query<T>(sql, params);
return res.rows;
} finally {
await pgGenericPool.release(client);
}
}
適用情境:當你需要同時管理多種不同類型的連線(例如 Redis + PostgreSQL)時,
generic-pool提供一致的 API。
5️⃣ 與 TypeORM 結合的連線池設定
// src/ormconfig.ts
import { DataSource } from 'typeorm';
import { User } from './entity/User';
export const AppDataSource = new DataSource({
type: 'postgres',
host: process.env.PG_HOST,
port: Number(process.env.PG_PORT),
username: process.env.PG_USER,
password: process.env.PG_PASSWORD,
database: process.env.PG_DATABASE,
entities: [User],
synchronize: false,
// TypeORM 內建 pool,直接設定 max
extra: {
max: 25, // 最大連線數
idleTimeoutMillis: 30000,
},
});
// src/routes/user.ts (使用 TypeORM)
import { Router } from 'express';
import { AppDataSource } from '../ormconfig';
import { User } from '../entity/User';
const router = Router();
router.get('/users/:id', async (req, res) => {
const repo = AppDataSource.getRepository(User);
const user = await repo.findOneBy({ id: Number(req.params.id) });
res.json(user);
});
export default router;
常見陷阱與最佳實踐
| 陷阱 | 可能產生的問題 | 解決方式 / 最佳實踐 |
|---|---|---|
| 忘記 release / release() 放錯位置 | 連線永遠不會回收,最終導致 Connection leak,服務崩潰 | 必須在 finally 或 res.on('finish') 中釋放,即使拋錯也不例外 |
| max 設定過高 | DB 本身的連線上限被超過,產生 Too many connections | 依照 DB 規格與實際併發量設定,通常 max = CPU cores * 2~4 為佳 |
| idleTimeout 設定過低 | 連線頻繁被關閉再重新建立,失去 pool 的好處 | 設定 30~60 秒的 idleTimeout,可減少不必要的建立/關閉 |
| 在全域變數中直接使用 client | 多個請求共用同一條連線,產生 race condition | 每次請求必須 獨立取得 連線,使用 pool 的 acquire / connect |
| 沒有處理連線失效(ping) | 連線在 idle 時被 DB 端關閉,導致查詢失敗 | 在 pool 設定 validation 或自行在 acquire 前 client.query('SELECT 1') |
| 在測試環境使用相同 pool 設定 | 測試時產生過多連線,干擾本機 DB | 使用 測試專用資料庫 或把 max 設為 1~2,確保測試快速且不影響正式環境 |
其他最佳實踐
- 統一管理 pool:將 pool 實例放在單一模組(如
db/postgresPool.ts),讓整個專案都共用同一個 pool,避免重複建立。 - 使用 TypeScript 型別:
pg、mysql2均已提供官方型別,盡量使用PoolClient、PoolConnection之類的型別,讓編譯期即可捕捉錯誤。 - 監控指標:透過
pool.totalCount、pool.idleCount、pool.waitingCount(pg)或相應屬性,搭配 Prometheus / Grafana 監控連線使用情況,提前發現瓶頸。 - 分層抽象:將資料庫操作封裝在 Repository / Service 層,讓 controller 僅負責請求/回應,保持程式碼乾淨且易於測試。
- Graceful Shutdown:在
process.on('SIGTERM')時,呼叫pool.end()讓所有連線安全關閉,避免資料遺失或未完成的交易。
// Graceful shutdown 範例
import { pgPool } from './db/postgresPool';
process.on('SIGTERM', async () => {
console.log('🛑 Received SIGTERM, closing DB pool...');
await pgPool.end(); // 等待所有連線回收
process.exit(0);
});
實際應用場景
| 場景 | 為何需要連線池 | 建議的 pool 設定 |
|---|---|---|
| 高併發的 REST API(如電商訂單系統) | 每秒可能有上千筆查詢,建立連線成本太高 | max = 30~50,idleTimeout = 30000 |
| 微服務間的資料同步(使用 PostgreSQL) | 服務 A 需要頻繁讀取服務 B 的資料表 | 使用 generic-pool 讓兩種 DB 共用同一個抽象層 |
| 批次任務(Cron) | 每天凌晨跑大量寫入,連線持續時間較長 | max = 10,connectionTimeout = 5000,確保不會卡住主流程 |
| 多租戶 SaaS 平台 | 每個租戶都有獨立的資料庫,連線池要分租戶管理 | 為每個租戶建立 獨立的 pool,或使用 pg‑pool 的 key 功能做分流 |
| 使用 ORM(TypeORM / Prisma) | ORM 已內建 pool,但仍需根據實際流量調整 | 在 extra.max 中設定上限,並搭配 poolTimeout 防止死鎖 |
總結
- 連線池是提升 Express + TypeScript 應用效能的關鍵,它能減少建立/關閉資料庫連線的開銷,避免連線過多導致 DB 被打垮。
- 透過 pg、mysql2、generic-pool 或 ORM 內建的 pool 功能,我們可以輕鬆在程式碼中取得、使用、釋放連線。
- 永遠在 finally 或回應結束時釋放連線,設定合適的
max、idleTimeout,並加入健康檢查與監控,才能避免連線 leak 與資源枯竭。 - 在實務開發中,根據 併發量、資料庫上限、業務需求 調整 pool 參數,並於 Graceful Shutdown 時正確關閉 pool,讓服務在升級或維護時保持平滑。
掌握了這些概念與實作技巧,你的 Express API 將在高壓測試下依然保持 快速、穩定,為使用者提供更好的體驗。祝開發順利,程式碼乾淨! 🎉