本文 AI 產出,尚未審核

ExpressJS (TypeScript) – 效能優化:資料庫連線池(Connection Pool)

簡介

Node.jsExpress 搭配 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 TypeORMPrismaSequelize ✅(內建 pool)

以下將以 PostgreSQLMySQL 為例,示範在 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,服務崩潰 必須在 finallyres.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 或自行在 acquireclient.query('SELECT 1')
在測試環境使用相同 pool 設定 測試時產生過多連線,干擾本機 DB 使用 測試專用資料庫 或把 max 設為 1~2,確保測試快速且不影響正式環境

其他最佳實踐

  1. 統一管理 pool:將 pool 實例放在單一模組(如 db/postgresPool.ts),讓整個專案都共用同一個 pool,避免重複建立。
  2. 使用 TypeScript 型別pgmysql2 均已提供官方型別,盡量使用 PoolClientPoolConnection 之類的型別,讓編譯期即可捕捉錯誤。
  3. 監控指標:透過 pool.totalCountpool.idleCountpool.waitingCount(pg)或相應屬性,搭配 Prometheus / Grafana 監控連線使用情況,提前發現瓶頸。
  4. 分層抽象:將資料庫操作封裝在 Repository / Service 層,讓 controller 僅負責請求/回應,保持程式碼乾淨且易於測試。
  5. 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~50idleTimeout = 30000
微服務間的資料同步(使用 PostgreSQL) 服務 A 需要頻繁讀取服務 B 的資料表 使用 generic-pool 讓兩種 DB 共用同一個抽象層
批次任務(Cron) 每天凌晨跑大量寫入,連線持續時間較長 max = 10connectionTimeout = 5000,確保不會卡住主流程
多租戶 SaaS 平台 每個租戶都有獨立的資料庫,連線池要分租戶管理 為每個租戶建立 獨立的 pool,或使用 pg‑poolkey 功能做分流
使用 ORM(TypeORM / Prisma) ORM 已內建 pool,但仍需根據實際流量調整 extra.max 中設定上限,並搭配 poolTimeout 防止死鎖

總結

  • 連線池是提升 Express + TypeScript 應用效能的關鍵,它能減少建立/關閉資料庫連線的開銷,避免連線過多導致 DB 被打垮。
  • 透過 pgmysql2generic-poolORM 內建的 pool 功能,我們可以輕鬆在程式碼中取得、使用、釋放連線。
  • 永遠在 finally 或回應結束時釋放連線,設定合適的 maxidleTimeout,並加入健康檢查與監控,才能避免連線 leak 與資源枯竭。
  • 在實務開發中,根據 併發量、資料庫上限、業務需求 調整 pool 參數,並於 Graceful Shutdown 時正確關閉 pool,讓服務在升級或維護時保持平滑。

掌握了這些概念與實作技巧,你的 Express API 將在高壓測試下依然保持 快速、穩定,為使用者提供更好的體驗。祝開發順利,程式碼乾淨! 🎉