// ===================================================== // 資料庫備援連線服務 // ===================================================== import mysql from 'mysql2/promise'; // 資料庫配置介面 interface DatabaseConfig { host: string; port: number; user: string; password: string; database: string; charset: string; timezone: string; acquireTimeout: number; timeout: number; reconnect: boolean; connectionLimit: number; queueLimit: number; retryDelay: number; maxRetries: number; idleTimeout: number; maxIdle: number; ssl: boolean; } // 主機資料庫配置 const masterConfig = { host: process.env.DB_HOST || 'mysql.theaken.com', port: parseInt(process.env.DB_PORT || '33306'), user: process.env.DB_USER || 'AI_Platform', password: process.env.DB_PASSWORD || 'Aa123456', database: process.env.DB_NAME || 'db_AI_Platform', charset: 'utf8mb4', timezone: '+08:00', connectionLimit: 10, queueLimit: 0, idleTimeout: 300000, ssl: false as any, }; // 備機資料庫配置 const slaveConfig = { host: process.env.SLAVE_DB_HOST || '122.100.99.161', port: parseInt(process.env.SLAVE_DB_PORT || '43306'), user: process.env.SLAVE_DB_USER || 'A999', password: process.env.SLAVE_DB_PASSWORD || '1023', database: process.env.SLAVE_DB_NAME || 'db_AI_Platform', // 修正為 AI 平台資料庫 charset: 'utf8mb4', timezone: '+08:00', connectionLimit: 10, queueLimit: 0, idleTimeout: 300000, ssl: false as any, }; // 備援狀態 interface FailoverStatus { isEnabled: boolean; currentDatabase: 'master' | 'slave'; masterHealthy: boolean; slaveHealthy: boolean; lastHealthCheck: number; consecutiveFailures: number; } // 備援資料庫管理類 export class DatabaseFailover { private static instance: DatabaseFailover; private masterPool: mysql.Pool | null = null; private slavePool: mysql.Pool | null = null; private status: FailoverStatus; private healthCheckInterval: NodeJS.Timeout | null = null; private constructor() { this.status = { isEnabled: process.env.DB_FAILOVER_ENABLED === 'true', currentDatabase: 'master', masterHealthy: false, slaveHealthy: false, lastHealthCheck: Date.now(), // 使用當前時間作為初始值 consecutiveFailures: 0, }; // 同步初始化連接池 this.initializePoolsSync(); this.startHealthCheck(); } public static getInstance(): DatabaseFailover { if (!DatabaseFailover.instance) { DatabaseFailover.instance = new DatabaseFailover(); } return DatabaseFailover.instance; } // 同步初始化連接池 private initializePoolsSync(): void { try { console.log('🚀 開始同步初始化資料庫備援系統...'); // 直接創建連接池,不等待測試 this.masterPool = mysql.createPool(masterConfig); console.log('✅ 主機資料庫連接池創建成功'); this.slavePool = mysql.createPool(slaveConfig); console.log('✅ 備機資料庫連接池創建成功'); // 設置默認使用主機 this.status.currentDatabase = 'master'; this.status.masterHealthy = true; // 假設主機健康,後續健康檢查會驗證 console.log('🎯 當前使用資料庫: 主機'); // 異步測試連接 this.testConnectionsAsync(); } catch (error) { console.error('❌ 資料庫連接池同步初始化失敗:', error); } } // 異步測試連接 private async testConnectionsAsync(): Promise { try { await this.testConnections(); } catch (error) { console.error('❌ 異步連接測試失敗:', error); } } // 初始化連接池 private async initializePools(): Promise { try { console.log('🚀 開始初始化資料庫備援系統...'); // 先測試主機連接 console.log('📡 測試主機資料庫連接...'); const masterHealthy = await this.testMasterConnection(); if (masterHealthy) { console.log('✅ 主機資料庫連接正常,使用主機'); this.status.currentDatabase = 'master'; this.status.masterHealthy = true; // 初始化主機連接池 this.masterPool = mysql.createPool(masterConfig); console.log('✅ 主機資料庫連接池初始化成功'); } else { console.log('❌ 主機資料庫連接失敗,測試備機...'); // 測試備機連接 const slaveHealthy = await this.testSlaveConnection(); if (slaveHealthy) { console.log('✅ 備機資料庫連接正常,切換到備機'); this.status.currentDatabase = 'slave'; this.status.slaveHealthy = true; // 初始化備機連接池 this.slavePool = mysql.createPool(slaveConfig); console.log('✅ 備機資料庫連接池初始化成功'); } else { console.log('❌ 主機和備機都無法連接,嘗試初始化主機連接池'); this.masterPool = mysql.createPool(masterConfig); this.status.currentDatabase = 'master'; } } // 初始化另一個連接池(用於健康檢查) if (this.status.currentDatabase === 'master' && !this.slavePool) { this.slavePool = mysql.createPool(slaveConfig); console.log('✅ 備機資料庫連接池初始化成功(用於健康檢查)'); } else if (this.status.currentDatabase === 'slave' && !this.masterPool) { this.masterPool = mysql.createPool(masterConfig); console.log('✅ 主機資料庫連接池初始化成功(用於健康檢查)'); } console.log(`🎯 當前使用資料庫: ${this.status.currentDatabase === 'master' ? '主機' : '備機'}`); } catch (error) { console.error('❌ 資料庫連接池初始化失敗:', error); } } // 測試主機連接 private async testMasterConnection(): Promise { try { const connection = await mysql.createConnection(masterConfig); await connection.ping(); await connection.end(); return true; } catch (error: any) { console.error('主機資料庫連接失敗:', error.message); return false; } } // 測試備機連接 private async testSlaveConnection(): Promise { try { const connection = await mysql.createConnection(slaveConfig); await connection.ping(); await connection.end(); return true; } catch (error: any) { console.error('備機資料庫連接失敗:', error.message); return false; } } // 測試資料庫連接 private async testConnections(): Promise { // 測試主機 try { if (this.masterPool) { const connection = await this.masterPool.getConnection(); await connection.ping(); connection.release(); this.status.masterHealthy = true; console.log('主機資料庫連接正常'); } } catch (error) { this.status.masterHealthy = false; console.error('主機資料庫連接失敗:', error); } // 測試備機 try { if (this.slavePool) { const connection = await this.slavePool.getConnection(); await connection.ping(); connection.release(); this.status.slaveHealthy = true; console.log('備機資料庫連接正常'); } } catch (error) { this.status.slaveHealthy = false; console.error('備機資料庫連接失敗:', error); } } // 開始健康檢查 private startHealthCheck(): void { if (!this.status.isEnabled) return; const interval = parseInt(process.env.DB_HEALTH_CHECK_INTERVAL || '30000'); this.healthCheckInterval = setInterval(async () => { await this.performHealthCheck(); }, interval); } // 執行健康檢查 private async performHealthCheck(): Promise { const now = Date.now(); this.status.lastHealthCheck = now; // 檢查主機 if (this.masterPool) { try { const connection = await this.masterPool.getConnection(); await connection.ping(); connection.release(); this.status.masterHealthy = true; } catch (error) { this.status.masterHealthy = false; console.error('主機資料庫健康檢查失敗:', error); } } // 檢查備機 if (this.slavePool) { try { const connection = await this.slavePool.getConnection(); await connection.ping(); connection.release(); this.status.slaveHealthy = true; } catch (error) { this.status.slaveHealthy = false; console.error('備機資料庫健康檢查失敗:', error); } } // 決定當前使用的資料庫 this.determineCurrentDatabase(); } // 決定當前使用的資料庫 private determineCurrentDatabase(): void { const previousDatabase = this.status.currentDatabase; if (this.status.masterHealthy) { if (this.status.currentDatabase !== 'master') { console.log('🔄 主機資料庫恢復,切換回主機'); this.status.currentDatabase = 'master'; this.status.consecutiveFailures = 0; } } else if (this.status.slaveHealthy) { if (this.status.currentDatabase !== 'slave') { console.log('🔄 主機資料庫故障,切換到備機'); this.status.currentDatabase = 'slave'; this.status.consecutiveFailures++; } } else { this.status.consecutiveFailures++; console.error('❌ 主機和備機資料庫都無法連接'); } // 記錄狀態變化 if (previousDatabase !== this.status.currentDatabase) { console.log(`📊 資料庫狀態變化: ${previousDatabase} → ${this.status.currentDatabase}`); } } // 獲取當前連接池 private getCurrentPool(): mysql.Pool | null { if (this.status.currentDatabase === 'master') { if (this.masterPool) { return this.masterPool; } else if (this.slavePool) { // 主機不可用,嘗試使用備機 console.log('⚠️ 主機連接池不可用,嘗試使用備機'); this.status.currentDatabase = 'slave'; return this.slavePool; } } else if (this.status.currentDatabase === 'slave') { if (this.slavePool) { return this.slavePool; } else if (this.masterPool) { // 備機不可用,嘗試使用主機 console.log('⚠️ 備機連接池不可用,嘗試使用主機'); this.status.currentDatabase = 'master'; return this.masterPool; } } console.error('❌ 沒有可用的資料庫連接池'); return null; } // 獲取連接 public async getConnection(): Promise { const pool = this.getCurrentPool(); if (!pool) { throw new Error('沒有可用的資料庫連接'); } let retries = 0; const maxRetries = parseInt(process.env.DB_RETRY_ATTEMPTS || '3'); const retryDelay = parseInt(process.env.DB_RETRY_DELAY || '2000'); while (retries < maxRetries) { try { return await pool.getConnection(); } catch (error: any) { console.error(`資料庫連接失敗 (嘗試 ${retries + 1}/${maxRetries}):`, error.message); if (error.code === 'ECONNRESET' || error.code === 'PROTOCOL_CONNECTION_LOST') { // 觸發健康檢查 await this.performHealthCheck(); retries++; if (retries < maxRetries) { console.log(`等待 ${retryDelay}ms 後重試...`); await new Promise(resolve => setTimeout(resolve, retryDelay)); continue; } } throw error; } } throw new Error('資料庫連接失敗,已達到最大重試次數'); } // 執行查詢 public async query(sql: string, params?: any[]): Promise { const connection = await this.getConnection(); try { const [rows] = await connection.execute(sql, params); return rows as T[]; } finally { connection.release(); } } // 執行單一查詢 public async queryOne(sql: string, params?: any[]): Promise { try { const results = await this.query(sql, params); return results.length > 0 ? results[0] : null; } catch (error) { console.error('資料庫單一查詢錯誤:', error); throw error; } } // 執行插入 public async insert(sql: string, params?: any[]): Promise { const connection = await this.getConnection(); try { const [result] = await connection.execute(sql, params); return result as mysql.ResultSetHeader; } finally { connection.release(); } } // 執行更新 public async update(sql: string, params?: any[]): Promise { const connection = await this.getConnection(); try { const [result] = await connection.execute(sql, params); return result as mysql.ResultSetHeader; } finally { connection.release(); } } // 執行刪除 public async delete(sql: string, params?: any[]): Promise { const connection = await this.getConnection(); try { const [result] = await connection.execute(sql, params); return result as mysql.ResultSetHeader; } finally { connection.release(); } } // 開始事務 public async beginTransaction(): Promise { const connection = await this.getConnection(); await connection.beginTransaction(); return connection; } // 提交事務 public async commit(connection: mysql.PoolConnection): Promise { await connection.commit(); connection.release(); } // 回滾事務 public async rollback(connection: mysql.PoolConnection): Promise { await connection.rollback(); connection.release(); } // 獲取備援狀態 public getStatus(): FailoverStatus { return { ...this.status }; } // 獲取主機連接池 public getMasterPool(): mysql.Pool | null { return this.masterPool; } // 獲取備機連接池 public getSlavePool(): mysql.Pool | null { return this.slavePool; } // 強制切換到指定資料庫 public async switchToDatabase(database: 'master' | 'slave'): Promise { if (database === 'master' && this.status.masterHealthy) { this.status.currentDatabase = 'master'; return true; } else if (database === 'slave' && this.status.slaveHealthy) { this.status.currentDatabase = 'slave'; return true; } return false; } // 關閉所有連接池 public async close(): Promise { if (this.healthCheckInterval) { clearInterval(this.healthCheckInterval); } if (this.masterPool) { await this.masterPool.end(); } if (this.slavePool) { await this.slavePool.end(); } } } // 導出單例實例 export const dbFailover = DatabaseFailover.getInstance(); // 導出類型 export type { PoolConnection } from 'mysql2/promise';