Files
ai-showcase-platform/lib/database-failover.ts

500 lines
15 KiB
TypeScript

// =====================================================
// 資料庫備援連線服務
// =====================================================
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<void> {
try {
await this.testConnections();
} catch (error) {
console.error('❌ 異步連接測試失敗:', error);
}
}
// 初始化連接池
private async initializePools(): Promise<void> {
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<boolean> {
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<boolean> {
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<void> {
// 測試主機
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<void> {
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<mysql.PoolConnection> {
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<T = any>(sql: string, params?: any[]): Promise<T[]> {
const connection = await this.getConnection();
try {
const [rows] = await connection.execute(sql, params);
return rows as T[];
} finally {
connection.release();
}
}
// 執行單一查詢
public async queryOne<T = any>(sql: string, params?: any[]): Promise<T | null> {
try {
const results = await this.query<T>(sql, params);
return results.length > 0 ? results[0] : null;
} catch (error) {
console.error('資料庫單一查詢錯誤:', error);
throw error;
}
}
// 執行插入
public async insert(sql: string, params?: any[]): Promise<mysql.ResultSetHeader> {
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<mysql.ResultSetHeader> {
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<mysql.ResultSetHeader> {
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<mysql.PoolConnection> {
const connection = await this.getConnection();
await connection.beginTransaction();
return connection;
}
// 提交事務
public async commit(connection: mysql.PoolConnection): Promise<void> {
await connection.commit();
connection.release();
}
// 回滾事務
public async rollback(connection: mysql.PoolConnection): Promise<void> {
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<boolean> {
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<void> {
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';