完成 APP 建立流程和使用分析、增加主機備機的備援機制、管理者後臺增加資料庫監控
This commit is contained in:
424
lib/database-failover.js
Normal file
424
lib/database-failover.js
Normal file
@@ -0,0 +1,424 @@
|
||||
"use strict";
|
||||
// =====================================================
|
||||
// 資料庫備援連線服務
|
||||
// =====================================================
|
||||
var __importDefault = (this && this.__importDefault) || function (mod) {
|
||||
return (mod && mod.__esModule) ? mod : { "default": mod };
|
||||
};
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.dbFailover = exports.DatabaseFailover = void 0;
|
||||
const promise_1 = __importDefault(require("mysql2/promise"));
|
||||
// 主機資料庫配置
|
||||
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,
|
||||
};
|
||||
// 備機資料庫配置
|
||||
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,
|
||||
};
|
||||
// 備援資料庫管理類
|
||||
class DatabaseFailover {
|
||||
constructor() {
|
||||
this.masterPool = null;
|
||||
this.slavePool = null;
|
||||
this.healthCheckInterval = null;
|
||||
this.status = {
|
||||
isEnabled: process.env.DB_FAILOVER_ENABLED === 'true',
|
||||
currentDatabase: 'master',
|
||||
masterHealthy: false,
|
||||
slaveHealthy: false,
|
||||
lastHealthCheck: 0,
|
||||
consecutiveFailures: 0,
|
||||
};
|
||||
// 同步初始化連接池
|
||||
this.initializePoolsSync();
|
||||
this.startHealthCheck();
|
||||
}
|
||||
static getInstance() {
|
||||
if (!DatabaseFailover.instance) {
|
||||
DatabaseFailover.instance = new DatabaseFailover();
|
||||
}
|
||||
return DatabaseFailover.instance;
|
||||
}
|
||||
// 同步初始化連接池
|
||||
initializePoolsSync() {
|
||||
try {
|
||||
console.log('🚀 開始同步初始化資料庫備援系統...');
|
||||
// 直接創建連接池,不等待測試
|
||||
this.masterPool = promise_1.default.createPool(masterConfig);
|
||||
console.log('✅ 主機資料庫連接池創建成功');
|
||||
this.slavePool = promise_1.default.createPool(slaveConfig);
|
||||
console.log('✅ 備機資料庫連接池創建成功');
|
||||
// 設置默認使用主機
|
||||
this.status.currentDatabase = 'master';
|
||||
this.status.masterHealthy = true; // 假設主機健康,後續健康檢查會驗證
|
||||
console.log('🎯 當前使用資料庫: 主機');
|
||||
// 異步測試連接
|
||||
this.testConnectionsAsync();
|
||||
}
|
||||
catch (error) {
|
||||
console.error('❌ 資料庫連接池同步初始化失敗:', error);
|
||||
}
|
||||
}
|
||||
// 異步測試連接
|
||||
async testConnectionsAsync() {
|
||||
try {
|
||||
await this.testConnections();
|
||||
}
|
||||
catch (error) {
|
||||
console.error('❌ 異步連接測試失敗:', error);
|
||||
}
|
||||
}
|
||||
// 初始化連接池
|
||||
async initializePools() {
|
||||
try {
|
||||
console.log('🚀 開始初始化資料庫備援系統...');
|
||||
// 先測試主機連接
|
||||
console.log('📡 測試主機資料庫連接...');
|
||||
const masterHealthy = await this.testMasterConnection();
|
||||
if (masterHealthy) {
|
||||
console.log('✅ 主機資料庫連接正常,使用主機');
|
||||
this.status.currentDatabase = 'master';
|
||||
this.status.masterHealthy = true;
|
||||
// 初始化主機連接池
|
||||
this.masterPool = promise_1.default.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 = promise_1.default.createPool(slaveConfig);
|
||||
console.log('✅ 備機資料庫連接池初始化成功');
|
||||
}
|
||||
else {
|
||||
console.log('❌ 主機和備機都無法連接,嘗試初始化主機連接池');
|
||||
this.masterPool = promise_1.default.createPool(masterConfig);
|
||||
this.status.currentDatabase = 'master';
|
||||
}
|
||||
}
|
||||
// 初始化另一個連接池(用於健康檢查)
|
||||
if (this.status.currentDatabase === 'master' && !this.slavePool) {
|
||||
this.slavePool = promise_1.default.createPool(slaveConfig);
|
||||
console.log('✅ 備機資料庫連接池初始化成功(用於健康檢查)');
|
||||
}
|
||||
else if (this.status.currentDatabase === 'slave' && !this.masterPool) {
|
||||
this.masterPool = promise_1.default.createPool(masterConfig);
|
||||
console.log('✅ 主機資料庫連接池初始化成功(用於健康檢查)');
|
||||
}
|
||||
console.log(`🎯 當前使用資料庫: ${this.status.currentDatabase === 'master' ? '主機' : '備機'}`);
|
||||
}
|
||||
catch (error) {
|
||||
console.error('❌ 資料庫連接池初始化失敗:', error);
|
||||
}
|
||||
}
|
||||
// 測試主機連接
|
||||
async testMasterConnection() {
|
||||
try {
|
||||
const connection = await promise_1.default.createConnection(masterConfig);
|
||||
await connection.ping();
|
||||
await connection.end();
|
||||
return true;
|
||||
}
|
||||
catch (error) {
|
||||
console.error('主機資料庫連接失敗:', error.message);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
// 測試備機連接
|
||||
async testSlaveConnection() {
|
||||
try {
|
||||
const connection = await promise_1.default.createConnection(slaveConfig);
|
||||
await connection.ping();
|
||||
await connection.end();
|
||||
return true;
|
||||
}
|
||||
catch (error) {
|
||||
console.error('備機資料庫連接失敗:', error.message);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
// 測試資料庫連接
|
||||
async testConnections() {
|
||||
// 測試主機
|
||||
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);
|
||||
}
|
||||
}
|
||||
// 開始健康檢查
|
||||
startHealthCheck() {
|
||||
if (!this.status.isEnabled)
|
||||
return;
|
||||
const interval = parseInt(process.env.DB_HEALTH_CHECK_INTERVAL || '30000');
|
||||
this.healthCheckInterval = setInterval(async () => {
|
||||
await this.performHealthCheck();
|
||||
}, interval);
|
||||
}
|
||||
// 執行健康檢查
|
||||
async performHealthCheck() {
|
||||
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();
|
||||
}
|
||||
// 決定當前使用的資料庫
|
||||
determineCurrentDatabase() {
|
||||
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}`);
|
||||
}
|
||||
}
|
||||
// 獲取當前連接池
|
||||
getCurrentPool() {
|
||||
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;
|
||||
}
|
||||
// 獲取連接
|
||||
async getConnection() {
|
||||
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) {
|
||||
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('資料庫連接失敗,已達到最大重試次數');
|
||||
}
|
||||
// 執行查詢
|
||||
async query(sql, params) {
|
||||
const connection = await this.getConnection();
|
||||
try {
|
||||
const [rows] = await connection.execute(sql, params);
|
||||
return rows;
|
||||
}
|
||||
finally {
|
||||
connection.release();
|
||||
}
|
||||
}
|
||||
// 執行單一查詢
|
||||
async queryOne(sql, params) {
|
||||
try {
|
||||
const results = await this.query(sql, params);
|
||||
return results.length > 0 ? results[0] : null;
|
||||
}
|
||||
catch (error) {
|
||||
console.error('資料庫單一查詢錯誤:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
// 執行插入
|
||||
async insert(sql, params) {
|
||||
const connection = await this.getConnection();
|
||||
try {
|
||||
const [result] = await connection.execute(sql, params);
|
||||
return result;
|
||||
}
|
||||
finally {
|
||||
connection.release();
|
||||
}
|
||||
}
|
||||
// 執行更新
|
||||
async update(sql, params) {
|
||||
const connection = await this.getConnection();
|
||||
try {
|
||||
const [result] = await connection.execute(sql, params);
|
||||
return result;
|
||||
}
|
||||
finally {
|
||||
connection.release();
|
||||
}
|
||||
}
|
||||
// 執行刪除
|
||||
async delete(sql, params) {
|
||||
const connection = await this.getConnection();
|
||||
try {
|
||||
const [result] = await connection.execute(sql, params);
|
||||
return result;
|
||||
}
|
||||
finally {
|
||||
connection.release();
|
||||
}
|
||||
}
|
||||
// 開始事務
|
||||
async beginTransaction() {
|
||||
const connection = await this.getConnection();
|
||||
await connection.beginTransaction();
|
||||
return connection;
|
||||
}
|
||||
// 提交事務
|
||||
async commit(connection) {
|
||||
await connection.commit();
|
||||
connection.release();
|
||||
}
|
||||
// 回滾事務
|
||||
async rollback(connection) {
|
||||
await connection.rollback();
|
||||
connection.release();
|
||||
}
|
||||
// 獲取備援狀態
|
||||
getStatus() {
|
||||
return { ...this.status };
|
||||
}
|
||||
// 強制切換到指定資料庫
|
||||
async switchToDatabase(database) {
|
||||
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;
|
||||
}
|
||||
// 關閉所有連接池
|
||||
async close() {
|
||||
if (this.healthCheckInterval) {
|
||||
clearInterval(this.healthCheckInterval);
|
||||
}
|
||||
if (this.masterPool) {
|
||||
await this.masterPool.end();
|
||||
}
|
||||
if (this.slavePool) {
|
||||
await this.slavePool.end();
|
||||
}
|
||||
}
|
||||
}
|
||||
exports.DatabaseFailover = DatabaseFailover;
|
||||
// 導出單例實例
|
||||
exports.dbFailover = DatabaseFailover.getInstance();
|
Reference in New Issue
Block a user