引言:题库服务器部署的核心挑战

在当今数字化教育和在线测评环境中,将题库部署在服务器上已经成为标准做法。然而,这种部署方式面临着三大核心挑战:数据安全、访问流畅性和突发故障处理。题库作为教育机构的核心资产,不仅包含知识产权,还涉及用户隐私数据,一旦泄露或损坏,后果不堪设想。同时,用户期望在任何时间、任何地点都能快速访问题库,这要求系统具备高可用性和高性能。最后,硬件故障、网络攻击、软件bug等突发情况随时可能发生,如何快速恢复服务并最小化数据损失是必须解决的问题。

本文将从这三个维度出发,提供一套完整的解决方案,涵盖技术架构、实施策略和最佳实践,帮助您构建一个安全、高效、可靠的题库服务系统。

一、数据安全:构建多层防御体系

1.1 数据加密:保护静态和传输中的数据

数据加密是保护题库安全的第一道防线。我们需要在两个层面实施加密:静态数据加密(Data at Rest)和传输中数据加密(Data in Transit)。

静态数据加密

静态数据加密保护存储在磁盘上的数据。对于题库数据库,我们应该:

  • 使用透明数据加密(TDE)技术
  • 对敏感字段(如题目答案、用户信息)进行列级加密
  • 定期轮换加密密钥

MySQL数据库TDE配置示例:

-- 启用MySQL的TDE功能(需要InnoDB引擎)
-- 1. 安装加密插件(MySQL企业版或Percona版本)
INSTALL PLUGIN keyring_file SONAME 'keyring_file.so';

-- 2. 配置my.cnf
/*
[mysqld]
early-plugin-load=keyring_file.so
keyring_file_data=/var/lib/mysql-keyring/keyring
*/

-- 3. 创建加密表空间
ALTER TABLE questions ENCRYPTION='Y';
ALTER TABLE users ENCRYPTION='Y';

-- 4. 验证加密状态
SELECT TABLE_NAME, CREATE_OPTIONS 
FROM INFORMATION_SCHEMA.TABLES 
WHERE TABLE_SCHEMA = 'question_bank' 
AND CREATE_OPTIONS LIKE '%ENCRYPTION="Y"%';

传输中数据加密

确保所有数据传输都通过加密通道进行:

  • 强制使用HTTPS/TLS 1.2+协议
  • 数据库连接使用SSL/TLS加密
  • API通信使用JWT或OAuth 2.0认证

Node.js中配置HTTPS和数据库SSL连接:

const https = require('https');
const fs = require('fs');
const mysql = require('mysql2');

// 1. 配置HTTPS服务器
const options = {
  key: fs.readFileSync('/path/to/private.key'),
  cert: fs.readFileSync('/path/to/certificate.crt'),
  minVersion: 'TLSv1.2', // 强制使用TLS 1.2+
  ciphers: [
    'ECDHE-RSA-AES128-GCM-SHA256',
    'ECDHE-RSA-AES256-GCM-SHA384',
    'AES128-GCM-SHA256',
    'AES256-GCM-SHA384'
  ].join(':')
};

// 2. 创建HTTPS服务器
const server = https.createServer(options, (req, res) => {
  // 处理请求
});

// 3. 数据库连接使用SSL
const pool = mysql.createPool({
  host: 'db.example.com',
  user: 'app_user',
  database: 'question_bank',
  password: 'secure_password',
  ssl: {
    ca: fs.readFileSync('/path/to/ca.pem'),
    rejectUnauthorized: true
  },
  connectionLimit: 10
});

1.2 访问控制:最小权限原则

严格的访问控制是防止未授权访问的关键。我们采用RBAC(基于角色的访问控制)模型,结合多因素认证(MFA)。

RBAC模型设计

-- 创建角色表
CREATE TABLE roles (
  id INT PRIMARY KEY AUTO_INCREMENT,
  name VARCHAR(50) NOT NULL UNIQUE,
  description VARCHAR(255)
);

-- 创建权限表
CREATE TABLE permissions (
  id INT PRIMARY KEY AUTO_INCREMENT,
  name VARCHAR(100) NOT NULL UNIQUE,
  description VARCHAR(255)
);

-- 角色-权限关联表
CREATE TABLE role_permissions (
  role_id INT,
  permission_id INT,
  PRIMARY KEY (role_id, permission_id),
  FOREIGN KEY (role_id) REFERENCES roles(id),
  FOREIGN KEY (permission_id) REFERENCES permissions(id)
);

-- 用户-角色关联表
CREATE TABLE user_roles (
  user_id INT,
  role_id INT,
  PRIMARY KEY (user_id, role_id),
  FOREIGN KEY (user_id) REFERENCES users(id),
  FOREIGN KEY (role_id) REFERENCES roles(id)
);

-- 插入基础角色和权限
INSERT INTO roles (name, description) VALUES 
('admin', '系统管理员'),
('teacher', '教师'),
('student', '学生');

INSERT INTO permissions (name, description) VALUES 
('question:read', '读取题目'),
('question:write', '创建/修改题目'),
('question:delete', '删除题目'),
('user:manage', '管理用户');

-- 分配权限
INSERT INTO role_permissions (role_id, permission_id) 
SELECT r.id, p.id FROM roles r, permissions p 
WHERE (r.name = 'admin' AND p.name IN ('question:read', 'question:write', 'question:delete', 'user:manage'))
   OR (r.name = 'teacher' AND p.name IN ('question:read', 'question:write'))
   OR (r.name = 'student' AND p.name = 'question:read');

多因素认证实现

// 使用speakeasy库实现TOTP(基于时间的一次性密码)
const speakeasy = require('speakeasy');
const QRCode = require('qrcode');

// 1. 为用户生成MFA密钥
function generateMFASetup(userId) {
  const secret = speakeasy.generateSecret({
    name: `题库系统(${userId})`
  });
  
  // 保存加密后的密钥到数据库
  const encryptedSecret = encrypt(secret.base32); // 使用AES加密
  saveToDatabase(userId, encryptedSecret);
  
  // 生成二维码供用户扫描
  QRCode.toDataURL(secret.otpauth_url, (err, data_url) => {
    console.log('扫描二维码:', data_url);
  });
  
  return { secret: secret.base32 };
}

// 2. 验证MFA代码
function verifyMFA(userId, token) {
  const encryptedSecret = getFromDatabase(userId);
  const secret = decrypt(encryptedSecret);
  
  return speakeasy.totp.verify({
    secret: secret,
    encoding: 'base32',
    token: token,
    window: 1 // 允许1个时间窗口的偏差
  });
}

// 3. Express中间件:保护路由
function requireMFA(req, res, next) {
  if (req.session.mfaVerified) {
    return next();
  }
  
  const token = req.headers['x-mfa-token'];
  if (!token) {
    return res.status(403).json({ error: 'MFA token required' });
  }
  
  if (verifyMFA(req.user.id, token)) {
    req.session.mfaVerified = true;
    next();
  } else {
    res.status(401).json({ error: 'Invalid MFA token' });
  }
}

// 4. 保护敏感路由
app.put('/api/questions/:id', requireMFA, (req, res) => {
  // 只有通过MFA验证的用户才能修改题目
});

1.3 审计与监控:记录所有操作

完整的审计日志是事后追溯和合规性要求的关键。我们需要记录:

  • 谁(用户ID)在什么时间(时间戳)访问了什么资源(API端点)
  • 操作类型(查询、创建、修改、删除)
  • 操作结果(成功/失败)
  • 客户端信息(IP地址、User-Agent)

审计日志表设计:

CREATE TABLE audit_logs (
  id BIGINT PRIMARY KEY AUTO_INCREMENT,
  user_id INT,
  action VARCHAR(50) NOT NULL,
  resource_type VARCHAR(50),
  resource_id VARCHAR(100),
  timestamp DATETIME DEFAULT CURRENT_TIMESTAMP,
  ip_address VARCHAR(45),
  user_agent TEXT,
  result ENUM('success', 'failure') NOT NULL,
  details JSON,
  INDEX idx_user_timestamp (user_id, timestamp),
  INDEX idx_timestamp (timestamp)
);

自动记录审计日志的中间件:

const auditLog = (action, resourceType) => {
  return async (req, res, next) => {
    const start = Date.now();
    
    // 监听响应完成事件
    res.on('finish', async () => {
      const duration = Date.now() - start;
      const logEntry = {
        user_id: req.user?.id || null,
        action: action,
        resource_type: resourceType,
        resource_id: req.params.id || req.body.id,
        ip_address: req.ip,
        user_agent: req.get('User-Agent'),
        result: res.statusCode < 400 ? 'success' : 'failure',
        details: {
          method: req.method,
          path: req.path,
          status_code: res.statusCode,
          duration_ms: duration,
          request_body: req.body,
          query_params: req.query
        }
      };
      
      // 异步写入日志,不阻塞主请求
      await db.query('INSERT INTO audit_logs SET ?', logEntry);
    });
    
    next();
  };
};

// 使用示例
app.post('/api/questions', 
  authenticate, 
  auditLog('create', 'question'), 
  async (req, res) => {
    // 创建题目逻辑
  }
);

1.4 数据备份与恢复:最后的防线

即使有最好的防护措施,数据丢失的风险仍然存在。因此,必须实施3-2-1备份策略:

  • 3份数据副本
  • 2种不同的存储介质
  • 1份异地备份

自动化备份脚本(Bash + MySQL):

#!/bin/bash
# backup_question_bank.sh

# 配置
DB_HOST="db.example.com"
DB_USER="backup_user"
DB_PASS="secure_backup_password"
DB_NAME="question_bank"
BACKUP_DIR="/backup/mysql"
S3_BUCKET="s3://question-bank-backups"
RETENTION_DAYS=30

# 创建备份目录
mkdir -p $BACKUP_DIR

# 生成时间戳
TIMESTAMP=$(date +"%Y%m%d_%H%M%S")
BACKUP_FILE="$BACKUP_DIR/${DB_NAME}_$TIMESTAMP.sql.gz"

# 执行备份(使用mysqldump)
echo "开始备份数据库: $DB_NAME"
mysqldump -h $DB_HOST -u $DB_USER -p$DB_PASS \
  --single-transaction \
  --routines \
  --triggers \
  --events \
  $DB_NAME | gzip > $BACKUP_FILE

# 检查备份是否成功
if [ $? -eq 0 ]; then
  echo "备份成功: $BACKUP_FILE"
  
  # 上传到S3(需要配置AWS CLI)
  aws s3 cp $BACKUP_FILE $S3_BUCKET/
  
  # 本地保留最近3份
  ls -t $BACKUP_DIR/*.sql.gz | tail -n +4 | xargs rm -f
  
  # 清理S3旧备份
  aws s3 ls $S3_BUCKET/ | awk '{print $4}' | while read file; do
    # 检查文件日期,删除超过保留期的
    aws s3 rm $S3_BUCKET/$file
  done
  
else
  echo "备份失败!"
  # 发送告警通知
  curl -X POST https://hooks.slack.com/services/... \
    -d '{"text":"数据库备份失败!"}'
fi

恢复脚本示例:

#!/bin/bash
# restore_question_bank.sh

BACKUP_FILE=$1
DB_NAME="question_bank_restore"

if [ -z "$BACKUP_FILE" ]; then
  echo "用法: ./restore_question_bank.sh <backup_file.sql.gz>"
  exit 1
fi

echo "从 $BACKUP_FILE 恢复数据库..."

# 解压
gunzip -c $BACKUP_FILE > /tmp/restore.sql

# 恢复到临时数据库(避免覆盖生产数据)
mysql -h db.example.com -u root -p \
  -e "CREATE DATABASE IF NOT EXISTS $DB_NAME;"

mysql -h db.example.com -u root -p $DB_NAME < /tmp/restore.sql

echo "恢复完成。请验证数据后手动切换到生产环境。"

二、访问流畅:优化性能与可用性

2.1 数据库优化:从查询到索引

数据库是题库系统的核心,优化数据库性能是确保访问流畅的关键。

索引策略

-- 题目表结构示例
CREATE TABLE questions (
  id BIGINT PRIMARY KEY AUTO_INCREMENT,
  subject_id INT NOT NULL,          -- 学科ID
  grade_level INT NOT NULL,         -- 年级级别
  question_type ENUM('single', 'multiple', 'essay') NOT NULL,
  difficulty TINYINT NOT NULL,      -- 难度1-5
  content TEXT NOT NULL,            -- 题目内容
  options JSON,                     -- 选项(JSON格式)
  answer TEXT,                      -- 答案
  tags JSON,                        -- 标签数组
  status ENUM('draft', 'published', 'archived') DEFAULT 'draft',
  created_by INT NOT NULL,          -- 创建者
  created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
  updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
);

-- 创建复合索引(根据查询模式)
-- 1. 按学科、年级、难度查询(最常用)
CREATE INDEX idx_subject_grade_diff ON questions(subject_id, grade_level, difficulty);

-- 2. 按标签查询(使用JSON索引,MySQL 8.0+)
CREATE INDEX idx_tags ON questions((CAST(tags AS CHAR(32) ARRAY)));

-- 3. 按创建者和状态查询
CREATE INDEX idx_creator_status ON questions(created_by, status);

-- 4. 全文搜索索引(用于内容搜索)
CREATE FULLTEXT INDEX idx_content_search ON questions(content);

-- 查看查询计划
EXPLAIN SELECT * FROM questions 
WHERE subject_id = 101 
  AND grade_level = 8 
  AND difficulty BETWEEN 3 AND 5;

查询优化技巧

// 使用连接池减少连接开销
const pool = mysql.createPool({
  host: 'db.example.com',
  user: 'app_user',
  database: 'question_bank',
  password: 'secure_password',
  connectionLimit: 20,          // 根据并发量调整
  queueLimit: 100,              // 队列上限,防止连接耗尽
  acquireTimeout: 60000,        // 获取连接超时
  timeout: 60000                // 查询超时
});

// 使用预处理语句防止SQL注入并提升性能
async function getQuestionsByFilter(filters) {
  const { subjectId, gradeLevel, difficulty, limit = 20, offset = 0 } = filters;
  
  // 构建动态查询
  let whereClause = [];
  let params = [];
  
  if (subjectId) {
    whereClause.push('subject_id = ?');
    params.push(subjectId);
  }
  if (gradeLevel) {
    whereClause.push('grade_level = ?');
    params.push(gradeLevel);
  }
  if (difficulty) {
    whereClause.push('difficulty = ?');
    params.push(difficulty);
  }
  
  // 添加分页参数
  params.push(parseInt(limit), parseInt(offset));
  
  const sql = `
    SELECT id, content, difficulty, tags 
    FROM questions 
    ${whereClause.length ? 'WHERE ' + whereClause.join(' AND ') : ''}
    ORDER BY created_at DESC 
    LIMIT ? OFFSET ?
  `;
  
  // 使用连接池执行
  const [rows] = await pool.query(sql, params);
  return rows;
}

// 使用Redis缓存热点数据
const redis = require('redis');
const client = redis.createClient({ url: 'redis://cache.example.com:6379' });

async function getQuestionWithCache(questionId) {
  const cacheKey = `question:${questionId}`;
  
  // 尝试从缓存获取
  const cached = await client.get(cacheKey);
  if (cached) {
    return JSON.parse(cached);
  }
  
  // 缓存未命中,查询数据库
  const [rows] = await pool.query('SELECT * FROM questions WHERE id = ?', [questionId]);
  const question = rows[0];
  
  if (question) {
    // 写入缓存,设置TTL为5分钟
    await client.setex(cacheKey, 300, JSON.stringify(question));
  }
  
  return question;
}

2.2 缓存策略:减少数据库压力

缓存是提升访问流畅性的利器,但需要合理设计策略。

多级缓存架构

// 内存缓存(Node.js进程内)
const NodeCache = require('node-cache');
const localCache = new NodeCache({ stdTTL: 60 }); // 1分钟

// Redis集群配置
const redis = require('redis');
const redisClient = redis.createCluster({
  rootNodes: [
    { url: 'redis://node1:6379' },
    { url: 'redis://node2:6379' },
    { url: 'redis://node3:6379' }
  ]
});

// 缓存装饰器模式
function cacheable(ttl = 300) {
  return function(target, propertyKey, descriptor) {
    const originalMethod = descriptor.value;
    
    descriptor.value = async function(...args) {
      const cacheKey = `${propertyKey}:${JSON.stringify(args)}`;
      
      // 1. 检查本地缓存
      const local = localCache.get(cacheKey);
      if (local !== undefined) {
        return local;
      }
      
      // 2. 检查Redis缓存
      const redisData = await redisClient.get(cacheKey);
      if (redisData) {
        const result = JSON.parse(redisData);
        localCache.set(cacheKey, result, 60); // 回填本地缓存
        return result;
      }
      
      // 3. 执行原始方法
      const result = await originalMethod.apply(this, args);
      
      // 4. 写入缓存
      if (result !== null && result !== undefined) {
        await redisClient.setex(cacheKey, ttl, JSON.stringify(result));
        localCache.set(cacheKey, result, 60);
      }
      
      return result;
    };
    
    return descriptor;
  };
}

// 使用示例
class QuestionService {
  @cacheable(600) // 缓存10分钟
  async getQuestionById(id) {
    const [rows] = await pool.query('SELECT * FROM questions WHERE id = ?', [id]);
    return rows[0];
  }
}

缓存更新策略(Cache-Aside + Write-Through)

// 写操作时同步更新缓存
async function updateQuestion(questionId, updates) {
  // 1. 更新数据库
  const [result] = await pool.query(
    'UPDATE questions SET ? WHERE id = ?',
    [updates, questionId]
  );
  
  if (result.affectedRows > 0) {
    // 2. 使相关缓存失效
    const cacheKeys = [
      `question:${questionId}`,
      `questions:subject:${updates.subject_id}`,
      `questions:grade:${updates.grade_level}`
    ];
    
    // 删除缓存(不等待,异步)
    redisClient.del(cacheKeys).catch(err => {
      console.error('缓存删除失败:', err);
    });
    
    // 3. 可选:立即预热新缓存
    const [rows] = await pool.query('SELECT * FROM questions WHERE id = ?', [questionId]);
    if (rows[0]) {
      redisClient.setex(`question:${questionId}`, 600, JSON.stringify(rows[0]));
    }
  }
  
  return result;
}

2.3 负载均衡与CDN:分发与加速

对于高并发访问,需要使用负载均衡器和CDN来分发流量。

Nginx负载均衡配置

# /etc/nginx/nginx.conf

# 定义上游服务器组
upstream question_api {
    # 负载均衡策略:加权轮询
    server api1.example.com:3000 weight=3;
    server api2.example.com:3000 weight=3;
    server api3.example.com:3000 weight=2;
    
    # 健康检查
    check interval=3000 rise=2 fall=3 timeout=1000 type=http;
    check_http_send "GET /health HTTP/1.0\r\n\r\n";
    check_http_expect_alive http_2xx http_3xx;
    
    # 会话保持(同一IP分配到同一服务器)
    ip_hash;
}

# HTTPS服务器配置
server {
    listen 443 ssl http2;
    server_name api.questionbank.com;
    
    # SSL证书
    ssl_certificate /etc/ssl/certs/questionbank.crt;
    ssl_certificate_key /etc/ssl/private/questionbank.key;
    ssl_protocols TLSv1.2 TLSv1.3;
    ssl_ciphers HIGH:!aNULL:!MD5;
    
    # 限流:防止DDoS
    limit_req_zone $binary_remote_addr zone=api_limit:10m rate=10r/s;
    limit_req zone=api_limit burst=20 nodelay;
    
    # 限速:限制单个IP
    limit_conn_zone $binary_remote_addr zone=addr:10m;
    limit_conn addr 10;
    
    location /api/ {
        proxy_pass http://question_api;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
        
        # 超时设置
        proxy_connect_timeout 5s;
        proxy_send_timeout 60s;
        proxy_read_timeout 60s;
        
        # 缓冲设置
        proxy_buffering on;
        proxy_buffer_size 4k;
        proxy_buffers 8 4k;
        
        # 启用gzip压缩
        gzip on;
        gzip_types application/json text/plain;
    }
    
    # 健康检查端点
    location /health {
        access_log off;
        return 200 "healthy\n";
        add_header Content-Type text/plain;
    }
}

CDN配置(以Cloudflare为例)

// 边缘计算:在CDN边缘节点缓存静态资源
// Cloudflare Workers示例

addEventListener('fetch', event => {
  event.respondWith(handleRequest(event.request));
});

async function handleRequest(request) {
  const url = new URL(request.url);
  
  // 静态资源缓存
  if (url.pathname.startsWith('/static/')) {
    // 设置CDN缓存策略
    const response = await fetch(request);
    const newResponse = new Response(response.body, response);
    newResponse.headers.set('Cache-Control', 'public, max-age=31536000'); // 1年
    return newResponse;
  }
  
  // API响应缓存(边缘缓存)
  if (url.pathname.startsWith('/api/questions/')) {
    const cache = caches.default;
    const cacheKey = new Request(url.toString(), request);
    
    // 检查缓存
    let response = await cache.match(cacheKey);
    if (response) {
      return response;
    }
    
    // 回源请求
    response = await fetch(request);
    
    // 缓存响应(仅GET请求,状态码200)
    if (request.method === 'GET' && response.status === 200) {
      const cacheResponse = response.clone();
      cacheResponse.headers.set('Cache-Control', 'public, max-age=300'); // 5分钟
      event.waitUntil(cache.put(cacheKey, cacheResponse));
    }
    
    return response;
  }
  
  return fetch(request);
}

2.4 异步处理与消息队列:解耦耗时操作

对于发送邮件、生成报表等耗时操作,使用消息队列异步处理,避免阻塞主请求。

RabbitMQ配置与使用

const amqp = require('amqplib');

// 1. 连接RabbitMQ
async function getChannel() {
  const connection = await amqp.connect('amqp://user:pass@rabbitmq.example.com');
  const channel = await connection.createChannel();
  
  // 声明队列
  await channel.assertQueue('question_export', {
    durable: true,      // 队列持久化
    deadLetterExchange: 'dlx', // 死信队列
    deadLetterRoutingKey: 'question_export_dlx'
  });
  
  return channel;
}

// 2. 生产者:发送导出任务
async function enqueueExportTask(userId, filters) {
  const channel = await getChannel();
  
  const task = {
    userId,
    filters,
    timestamp: Date.now(),
    retryCount: 0
  };
  
  channel.sendToQueue(
    'question_export',
    Buffer.from(JSON.stringify(task)),
    { persistent: true } // 消息持久化
  );
  
  console.log('导出任务已加入队列');
}

// 3. 消费者:处理导出任务
async function consumeExportTasks() {
  const channel = await getChannel();
  
  channel.consume('question_export', async (msg) => {
    try {
      const task = JSON.parse(msg.content.toString());
      
      // 执行导出逻辑
      await generateExportFile(task.userId, task.filters);
      
      // 发送通知
      await sendNotification(task.userId, '导出完成');
      
      // 确认消息
      channel.ack(msg);
    } catch (error) {
      console.error('处理失败:', error);
      
      // 检查重试次数
      if (task.retryCount < 3) {
        task.retryCount++;
        // 重新入队
        channel.sendToQueue('question_export', Buffer.from(JSON.stringify(task)));
        channel.ack(msg);
      } else {
        // 超过重试次数,发送到死信队列
        channel.reject(msg, false);
      }
    }
  });
}

// 4. 死信队列处理
async function setupDeadLetterQueue() {
  const channel = await getChannel();
  
  // 声明死信交换机
  await channel.assertExchange('dlx', 'direct');
  
  // 声明死信队列
  await channel.assertQueue('question_export_dlx', {
    durable: true
  });
  
  // 绑定
  await channel.bindQueue('question_export_dlx', 'dlx', 'question_export_dlx');
  
  // 监控死信
  channel.consume('question_export_dlx', (msg) => {
    console.error('永久失败的任务:', msg.content.toString());
    // 发送告警
    sendAlert('导出任务失败', msg.content.toString());
  });
}

三、突发故障处理:构建弹性系统

3.1 故障检测与健康检查

及时发现故障是快速恢复的前提。我们需要实现多层次的健康检查。

应用层健康检查

// Express健康检查端点
app.get('/health', async (req, res) => {
  const checks = {
    timestamp: new Date().toISOString(),
    uptime: process.uptime(),
    version: process.env.npm_package_version
  };
  
  // 检查数据库连接
  try {
    await pool.query('SELECT 1');
    checks.database = 'healthy';
  } catch (e) {
    checks.database = 'unhealthy';
  }
  
  // 检查Redis连接
  try {
    await redisClient.ping();
    checks.redis = 'healthy';
  } catch (e) {
    checks.redis = 'unhealthy';
  }
  
  // 检查外部API(如果有)
  try {
    const response = await fetch('https://api.partner.com/health');
    checks.partner_api = response.ok ? 'healthy' : 'unhealthy';
  } catch (e) {
    checks.partner_api = 'unhealthy';
  }
  
  // 综合状态
  const allHealthy = Object.values(checks).every(v => v === 'healthy');
  const status = allHealthy ? 200 : 503;
  
  res.status(status).json(checks);
});

// 深度健康检查(用于内部监控)
app.get('/health/deep', async (req, res) => {
  const checks = {};
  
  // 1. 数据库查询性能
  const start = Date.now();
  await pool.query('SELECT COUNT(*) as count FROM questions');
  checks.dbQueryTime = Date.now() - start;
  
  // 2. Redis读写测试
  const testKey = 'health:test';
  await redisClient.set(testKey, 'ok', 'EX', 10);
  const redisValue = await redisClient.get(testKey);
  checks.redisReadWrite = redisValue === 'ok';
  
  // 3. 磁盘空间检查
  const df = require('diskusage');
  const disk = df.checkSync('/');
  checks.diskSpace = disk.available > 1024 * 1024 * 1024; // >1GB
  
  // 4. 内存使用
  const used = process.memoryUsage();
  checks.memoryUsage = {
    rss: Math.round(used.rss / 1024 / 1024) + 'MB',
    heapUsed: Math.round(used.heapUsed / 1024 / 1024) + 'MB'
  };
  
  const allHealthy = checks.dbQueryTime < 1000 && 
                     checks.redisReadWrite && 
                     checks.diskSpace;
  
  res.status(allHealthy ? 200 : 503).json(checks);
});

外部监控(Prometheus + Grafana)

# prometheus.yml 配置
global:
  scrape_interval: 15s

scrape_configs:
  - job_name: 'question-bank-api'
    static_configs:
      - targets: ['api1.example.com:9090', 'api2.example.com:9090']
    metrics_path: '/metrics'
    scrape_interval: 5s

  - job_name: 'mysql'
    static_configs:
      - targets: ['db.example.com:9104']

  - job_name: 'redis'
    static_configs:
      - targets: ['cache.example.com:9121']
// 在应用中暴露Prometheus指标
const client = require('prom-client');

// 创建指标
const httpRequestsTotal = new client.Counter({
  name: 'http_requests_total',
  help: 'Total HTTP requests',
  labelNames: ['method', 'path', 'status']
});

const httpRequestDuration = new client.Histogram({
  name: 'http_request_duration_seconds',
  help: 'HTTP request duration',
  labelNames: ['method', 'path'],
  buckets: [0.1, 0.5, 1, 2, 5]
});

// Express中间件
app.use((req, res, next) => {
  const end = httpRequestDuration.startTimer();
  
  res.on('finish', () => {
    httpRequestsTotal.inc({
      method: req.method,
      path: req.route?.path || req.path,
      status: res.statusCode
    });
    end();
  });
  
  next();
});

// 暴露指标端点
app.get('/metrics', async (req, res) => {
  res.set('Content-Type', client.register.contentType);
  res.end(await client.register.metrics());
});

3.2 自动故障转移与恢复

数据库主从复制与故障转移

-- MySQL主从复制配置(主库)
-- my.cnf 配置
[mysqld]
server-id = 1
log_bin = mysql-bin
binlog_format = ROW
expire_logs_days = 7

-- 创建复制用户
CREATE USER 'repl'@'%' IDENTIFIED BY 'secure_repl_password';
GRANT REPLICATION SLAVE ON *.* TO 'repl'@'%';

-- 查看主库状态
SHOW MASTER STATUS;
-- 从库配置
[mysqld]
server-id = 2
relay_log = mysql-relay-bin
log_bin = mysql-bin
read_only = 1

-- 配置复制
CHANGE MASTER TO
  MASTER_HOST='db-primary.example.com',
  MASTER_USER='repl',
  MASTER_PASSWORD='secure_repl_password',
  MASTER_LOG_FILE='mysql-bin.000001',
  MASTER_LOG_POS=123456;

START SLAVE;
SHOW SLAVE STATUS\G

自动故障转移脚本

#!/usr/bin/env python3
# db_failover.py

import mysql.connector
import time
import sys

PRIMARY_HOST = 'db-primary.example.com'
REPLICA_HOST = 'db-replica.example.com'
MONITOR_INTERVAL = 10

def check_primary_health():
    """检查主库健康状态"""
    try:
        conn = mysql.connector.connect(
            host=PRIMARY_HOST,
            user='monitor',
            password='monitor_pass',
            connect_timeout=5
        )
        cursor = conn.cursor()
        cursor.execute("SELECT 1")
        cursor.close()
        conn.close()
        return True
    except Exception as e:
        print(f"主库连接失败: {e}")
        return False

def promote_replica():
    """提升从库为主库"""
    try:
        # 连接到从库
        conn = mysql.connector.connect(
            host=REPLICA_HOST,
            user='root',
            password='root_pass'
        )
        cursor = conn.cursor()
        
        # 停止从库复制
        cursor.execute("STOP SLAVE")
        
        # 重置复制信息
        cursor.execute("RESET SLAVE ALL")
        
        # 检查是否需要执行未完成的事务
        cursor.execute("SHOW SLAVE STATUS")
        slave_status = cursor.fetchone()
        
        # 提升为主库
        cursor.execute("SET GLOBAL read_only = 0")
        
        print("从库已提升为主库")
        cursor.close()
        conn.close()
        return True
        
    except Exception as e:
        print(f"提升失败: {e}")
        return False

def update_dns_or_vip():
    """更新DNS或VIP指向新主库"""
    # 这里可以调用云服务商API更新DNS记录
    # 或者使用keepalived切换VIP
    print("更新DNS/VIP指向新主库")
    pass

def main():
    print("开始监控数据库健康...")
    
    while True:
        if not check_primary_health():
            print("主库故障,开始故障转移...")
            
            # 等待几秒确保故障不是临时的
            time.sleep(30)
            
            if not check_primary_health():
                if promote_replica():
                    update_dns_or_vip()
                    print("故障转移完成")
                    sys.exit(0)
                else:
                    print("故障转移失败,需要人工干预")
                    sys.exit(1)
            else:
                print("主库恢复,无需转移")
        
        time.sleep(MONITOR_INTERVAL)

if __name__ == '__main__':
    main()

应用层重试与熔断

// 使用circuit-breaker模式
const CircuitBreaker = require('opossum');

// 为数据库操作创建熔断器
const dbBreaker = new CircuitBreaker(
  async (query, params) => {
    return await pool.query(query, params);
  },
  {
    timeout: 3000,          // 超时时间
    errorThresholdPercentage: 50, // 错误率阈值
    resetTimeout: 30000,    // 半开状态时间
    volumeThreshold: 10     // 最小请求数
  }
);

// 熔断器事件监听
dbBreaker.on('open', () => {
  console.error('数据库熔断器打开,服务降级');
  // 发送告警
  sendAlert('数据库熔断', '数据库服务不可用');
});

dbBreaker.on('halfOpen', () => {
  console.log('数据库熔断器半开,尝试恢复');
});

dbBreaker.on('close', () => {
  console.log('数据库熔断器关闭,服务恢复');
});

// 使用熔断器执行查询
async function getQuestionsSafe(filters) {
  try {
    const result = await dbBreaker.fire(
      'SELECT * FROM questions WHERE subject_id = ? AND grade_level = ?',
      [filters.subjectId, filters.gradeLevel]
    );
    return result;
  } catch (error) {
    if (dbBreaker.opened) {
      // 熔断状态,返回降级数据
      return getFallbackData();
    }
    throw error;
  }
}

// 降级数据(缓存的热门题目)
function getFallbackData() {
  return [
    { id: 1, content: '备用题目1', difficulty: 3 },
    { id: 2, content: '备用题目2', difficulty: 2 }
  ];
}

3.3 数据一致性与事务管理

在分布式系统中,确保数据一致性至关重要。

分布式事务(Saga模式)

// 使用Saga模式处理跨服务事务
// 例如:创建题目 + 更新统计 + 发送通知

class QuestionSaga {
  constructor() {
    this.steps = [];
  }

  // 步骤1:创建题目
  async createQuestion(questionData) {
    const step = {
      name: 'createQuestion',
      action: async () => {
        const [result] = await pool.query(
          'INSERT INTO questions SET ?',
          [questionData]
        );
        return result.insertId;
      },
      compensate: async (questionId) => {
        await pool.query('DELETE FROM questions WHERE id = ?', [questionId]);
      }
    };
    this.steps.push(step);
    return this;
  }

  // 步骤2:更新统计
  async updateStats(questionId, subjectId) {
    const step = {
      name: 'updateStats',
      action: async () => {
        await pool.query(
          'UPDATE subject_stats SET question_count = question_count + 1 WHERE subject_id = ?',
          [subjectId]
        );
      },
      compensate: async () => {
        await pool.query(
          'UPDATE subject_stats SET question_count = question_count - 1 WHERE subject_id = ?',
          [subjectId]
        );
      }
    };
    this.steps.push(step);
    return this;
  }

  // 步骤3:发送通知
  async sendNotification(userId, questionId) {
    const step = {
      name: 'sendNotification',
      action: async () => {
        // 调用通知服务
        await fetch('https://notification.example.com/send', {
          method: 'POST',
          headers: { 'Content-Type': 'application/json' },
          body: JSON.stringify({
            userId,
            message: `题目 ${questionId} 已创建`
          })
        });
      },
      compensate: async () => {
        // 通知无法撤销,但可以发送纠正消息
        console.log(`补偿:发送纠正通知给用户 ${userId}`);
      }
    };
    this.steps.push(step);
    return this;
  }

  // 执行Saga
  async execute() {
    const results = [];
    
    try {
      for (const step of this.steps) {
        const result = await step.action();
        results.push({ name: step.name, result, success: true });
      }
      return { success: true, results };
    } catch (error) {
      // 执行补偿
      console.error('Saga执行失败,开始补偿:', error);
      
      for (let i = results.length - 1; i >= 0; i--) {
        const step = this.steps[i];
        if (results[i].success) {
          try {
            await step.compensate(results[i].result);
            console.log(`补偿成功: ${step.name}`);
          } catch (compError) {
            console.error(`补偿失败: ${step.name}`, compError);
            // 记录到补偿日志表,人工处理
            await logCompensationFailure(step.name, results[i].result, compError);
          }
        }
      }
      
      return { success: false, error: error.message };
    }
  }
}

// 使用示例
const saga = new QuestionSaga()
  .createQuestion({ content: '测试题目', subject_id: 101 })
  .updateStats(/* 参数 */)
  .sendNotification(123, 456);

const result = await saga.execute();
if (!result.success) {
  // 处理失败
}

3.4 灾难恢复计划

灾难恢复脚本

#!/bin/bash
# disaster_recovery.sh

# 配置
BACKUP_S3_BUCKET="s3://question-bank-backups"
RECOVERY_DIR="/recovery"
DB_HOST="db-recovery.example.com"

# 步骤1:从S3拉取最新备份
echo "从S3拉取备份..."
aws s3 ls $BACKUP_S3_BUCKET/ | tail -1 | awk '{print $4}' > latest_backup.txt
LATEST_BACKUP=$(cat latest_backup.txt)
aws s3 cp $BACKUP_S3_BUCKET/$LATEST_BACKUP $RECOVERY_DIR/

# 步骤2:恢复数据库
echo "恢复数据库..."
gunzip -c $RECOVERY_DIR/$LATEST_BACKUP > $RECOVERY_DIR/restore.sql
mysql -h $DB_HOST -u root -p < $RECOVERY_DIR/restore.sql

# 步骤3:验证数据完整性
echo "验证数据..."
mysql -h $DB_HOST -u root -p -e "SELECT COUNT(*) FROM questions;" question_bank

# 步骤4:切换应用到恢复数据库
echo "更新应用配置..."
sed -i "s/db-primary.example.com/db-recovery.example.com/g" /app/config/database.js

# 步骤5:重启应用
echo "重启应用..."
systemctl restart question-bank-app

echo "灾难恢复完成"

灾难恢复演练计划

# 灾难恢复演练计划

## 演练频率
- 桌面推演:每季度一次
- 实际演练:每半年一次

## 演练场景
1. 主数据库完全宕机
2. 数据中心断电
3. 勒索软件攻击
4. 人为误删除数据

## 演练步骤
1. 模拟故障发生
2. 执行灾难恢复脚本
3. 验证数据完整性
4. 测试应用功能
5. 记录演练时间和问题
6. 优化恢复流程

## 成功标准
- RTO(恢复时间目标)< 1小时
- RPO(恢复点目标)< 15分钟
- 数据完整性100%

四、综合实施建议

4.1 分阶段实施路线图

第一阶段:基础安全与备份(1-2周)

  1. 配置数据库TDE加密
  2. 实施RBAC访问控制
  3. 设置自动化备份策略
  4. 配置HTTPS和数据库SSL

第二阶段:性能优化(2-3周)

  1. 分析查询日志,创建索引
  2. 部署Redis缓存
  3. 配置Nginx负载均衡
  4. 实现异步任务队列

第三阶段:高可用与监控(2-3周)

  1. 配置数据库主从复制
  2. 部署健康检查端点
  3. 集成Prometheus + Grafana
  4. 实现熔断器和重试机制

第四阶段:自动化与演练(1-2周)

  1. 编写故障转移脚本
  2. 实现Saga事务模式
  3. 执行灾难恢复演练
  4. 完善文档和应急预案

4.2 成本与效益分析

方案 成本 效益 优先级
数据库加密 低 高(合规性) 高
自动化备份 低 极高(数据安全) 高
Redis缓存 中 高(性能提升) 高
主从复制 中 高(可用性) 中
负载均衡 中 中(扩展性) 中
多因素认证 低 高(安全性) 高
灾难恢复演练 低 极高(风险降低) 高

4.3 关键指标监控(KPI)

  • 可用性:> 99.9%
  • 平均响应时间:< 200ms
  • P95响应时间:< 500ms
  • 数据丢失:0(RPO=0)
  • 恢复时间:< 1小时(RTO)
  • 安全事件:0

4.4 团队职责分工

  • 开发团队:实现应用层安全、性能优化、健康检查
  • 运维团队:配置基础设施、备份策略、监控告警
  • 安全团队:安全审计、渗透测试、合规性检查
  • DBA团队:数据库优化、复制配置、备份验证

结论

将题库部署在服务器上并确保其安全、流畅和可靠,需要系统性的方法和持续的努力。通过实施多层安全防御(加密、访问控制、审计)、性能优化策略(索引、缓存、负载均衡)和弹性架构设计(故障检测、自动恢复、灾难恢复),您可以构建一个企业级的题库服务系统。

记住,没有一劳永逸的解决方案。技术架构需要随着业务增长和威胁演变而持续优化。定期进行安全审计、性能测试和灾难恢复演练,是保持系统健康的关键。投资于自动化和监控工具,虽然初期成本较高,但长期来看将大幅降低运维成本和风险。

最后,文档和培训同样重要。确保团队成员都了解系统的架构、应急预案和最佳实践。在真正的危机时刻,清晰的流程和训练有素的团队比任何技术工具都更可靠。