TypeScript/JavaScript 中实现健壮的数据库连接重试逻辑

2次阅读

TypeScript/JavaScript 中实现健壮的数据库连接重试逻辑

本文介绍如何在 node.js 环境中使用 typescriptjavascript 编写具备自动重试能力的 mysql 数据库连接与查询逻辑,支持可配置最大重试次数与间隔,并确保连接失败时不崩溃、不提前返回,符合生产级容错要求。

在构建高可用后端服务时,数据库临时不可用(如维护重启、网络抖动、容器启动延迟)是常见场景。若连接逻辑缺乏重试机制,应用可能快速失败甚至退出。原问题中提供的递归 setTimeout + 异步回调方案存在明显缺陷:无法控制重试上限、无法统一错误处理、易导致调用爆炸或内存泄漏,且 query 方法未集成重试逻辑,违背“失败即重试”的设计目标

以下是一个专业、可维护、生产就绪的重试实现方案,核心改进包括:

  • 分离关注点:将通用重试逻辑抽象为独立的 retry 工具方法,支持任意 promise 返回函数;
  • 可控重试策略:支持最大重试次数(maxRetries)和固定延迟(delay),负值表示无限重试;
  • 连接阶段强保障:connect() 方法内部封装连接过程为 Promise,并通过 retry() 保证最终成功或明确超限失败;
  • 查询阶段按需重试:query() 本身不内置重试(避免无界重试掩盖 SQL 错误),但可轻松组合 retry() 实现语义化重试(见后文示例);
  • 类型安全与可读性:使用现代 async/await + Promise 风格,规避回调地狱,便于 typescript 类型推导。

✅ 推荐实现(TypeScript 兼容)

import mysql from 'mysql';  interface DbConfig {   host: string;   user: string;   password: string;   port: string | number;   database?: string; }  class Database {   connection: mysql.Connection | null = null;   config?: DbConfig;    // 通用重试工具:适用于任意返回 Promise 的操作   retry<T>(     fn: () => Promise<T>,     maxRetries: number,     delay: number   ): Promise<T> {     return new Promise((resolve, reject) => {       let attempts = 0;        const attempt = () => {         fn()           .then(result => resolve(result))           .catch(error => {             attempts++;             if (maxRetries < 0 || attempts <= maxRetries) {               console.warn(                 `[DB Retry] Attempt ${attempts}/${maxRetries === -1 ? '∞' : maxRetries} failed:`,                 error instanceof Error ? error.message : String(error)               );               setTimeout(attempt, delay);             } else {               reject(                 new Error(                   `Max retries (${maxRetries}) exceeded. Last error: ${                     error instanceof Error ? error.message : String(error)                   }`                 )               );             }           });       };        attempt();     });   }    // 建立带重试的数据库连接   async connect(config: DbConfig): Promise<void> {     const connectOnce = (): Promise<mysql.Connection> => {       return new Promise((resolve, reject) => {         const conn = mysql.createConnection(config);         conn.connect(err => {           if (err) reject(err);           else resolve(conn);         });       });     };      try {       const conn = await this.retry(         connectOnce,         parseInt(process.env.DB_MAX_RETRY || '5', 10),         parseInt(process.env.DB_RETRY_INTERVAL || '5000', 10)       );       this.connection = conn;       this.config = config;       console.log(`✅ Database connected: ${config.database || '(no DB name)'}`);     } catch (err) {       console.error('❌ Failed to establish database connection after retries:', err);       throw err; // 让调用方决定是否兜底(如降级、告警)     }   }    // 基础查询方法(无重试)——保持职责单一   async query(sql: string, values?: any[]): Promise<any[]> {     if (!this.connection) {       throw new Error('Database not connected. Call connect() first.');     }      return new Promise((resolve, reject) => {       this.connection!.query(sql, values, (err, results) => {         if (err) reject(err);         else resolve(results);       });     });   }    // 可选:封装带重试的查询(适用于幂等读操作)   async queryWithRetry(     sql: string,     values?: any[],     maxRetries = 3,     delay = 2000   ): Promise<any[]> {     return this.retry(       () => this.query(sql, values),       maxRetries,       delay     );   }    // 安全关闭连接   close(): void {     if (this.connection) {       this.connection.end();       this.connection = null;       console.log('? Database connection closed.');     }   } }  export default Database;

? 使用示例

import Database from './Database';  const db = new Database();  // 启动时自动重连(阻塞式初始化) async function initDatabase() {   const config = {     host: process.env.DB_HOST || 'localhost',     user: process.env.DB_USER,     password: process.env.DB_PASSWORD,     port: process.env.DB_PORT || 3306,     database: process.env.DB_NAME,   };    console.log('⏳ Initializing database connection...');   try {     await db.connect(config);     console.log('? Database ready.');      // 示例:带重试的健康检查查询     const result = await db.queryWithRetry('select 1 AS alive', 3, 1000);     console.log('Health check:', result);    } catch (error) {     console.error('? Critical: DB initialization failed.', error);     process.exit(1); // 或触发熔断/告警   } }  initDatabase();

⚠️ 关键注意事项

  • 环境变量校验:务必在启动前校验 DB_MAX_RETRY 和 DB_RETRY_INTERVAL,避免 NaN 导致无限重试;
  • 连接泄漏防护:每次 createConnection 失败后,旧连接若已部分建立需手动 .end() 或 .destroy()(本例中因连接未成功,无需显式销毁);
  • 幂等性原则:仅对 SELECT 等安全操作启用查询重试;INSERT/UPDATE 等非幂等操作重试可能导致数据异常,应结合事务与唯一约束保障;
  • 监控与告警:在 retry 的 console.warn 中加入指标上报(如 prometheus Counter),便于观测重试频次;
  • 替代方案建议:生产环境推荐使用 mysql2(支持 Promise API 和连接池)+ generic-pool,进一步提升稳定性和性能。

通过以上设计,你将获得一个清晰、可测试、可扩展且符合 Node.js 最佳实践的数据库重试方案,既解决启动期连接韧性问题,也为运行时查询容错提供灵活基础。

text=ZqhQzanResources