
本文介绍如何在 node.js 环境中使用 typescript 或 javascript 编写具备自动重试能力的 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 最佳实践的数据库重试方案,既解决启动期连接韧性问题,也为运行时查询容错提供灵活基础。