# Redis改造实施计划(缓存+队列完整版) > **文档版本?* V2.0 > **更新日期?* 2025-12-12 > **目标完成时间?* 2025-12-18?天) > **负责人:** 技术团? > **风险等级?* 🟡 中等(有降级方案? > **重要变更?* Redis队列?可?调整?必须" --- ## ⚠️ **重要说明(V2.0更新?* 经过深入分析,Redis队列**不是可选项**,而是**核心功能的必须项**? 1. **ASL文献筛?*?000篇文献需?小时,不用Redis队列失败?> 95% 2. **DC Tool B病历提取**?000份病历需?-3小时,同样问?3. **SAE实例特?*?5分钟无流量自动缩容,长任务必然失? **因此本计划调整为:缓?队列一起实施(7天完成)** --- ## 📋 目录 1. [改造背景与目标](#1-改造背景与目标) 2. [当前系统状态分析](#2-当前系统状态分? 3. [Redis配置信息](#3-redis配置信息) 4. [改造详细步骤](#4-改造详细步?(✨ **已更新:包含队列**?5. [测试方案](#5-测试方案) 6. [风险评估与缓解](#6-风险评估与缓? 7. [上线计划](#7-上线计划) 8. [回滚方案](#8-回滚方案) 9. [监控与运维](#9-监控与运? --- ## 1. 改造背景与目标 ### 1.1 为什么要改造? #### **当前问题**?1. ?**违反云原生规?*:系统使用内存缓存,违反自己制定的云原生开发规?2. ?**LLM成本失控**:缓存不持久化,导致重复调用DeepSeek/Qwen API 3. ?**长任务不可靠**?0-60分钟的文献筛选任务,SAE实例重启后丢?4. ?**多实例不同步**:SAE扩容后,各实例缓存不共享 5. ?**Serverless不适配**:内存状态在Serverless环境下不可靠 #### **改造目?*?- ?**符合架构规范**:使用分布式缓存(Redis?- ?**降低API成本**:LLM结果缓存持久化,避免重复调用 - ?**任务持久?*:长时间任务不因实例重启而丢?- ?**支持多实?*:缓存在多实例间共享 - ?**平滑过渡**:保留降级方案,确保系统稳定 --- ## 2. 当前系统状态分? ### 2.1 已使用缓存的位置 #### **位置1:HealthCheckService.ts** ```typescript // 文件:backend/src/modules/dc/tool-b/services/HealthCheckService.ts // ?7行:读取缓存 const cached = await cache.get(cacheKey); // ?45行:写入缓存 await cache.set(cacheKey, result, 86400); // 24小时 ``` **用?*:Excel健康检查结果缓? **重要?*:?中等(避免重复解析Excel? **数据?*:~5KB/? --- #### **位置2:LLM12FieldsService.ts** ```typescript // 文件:backend/src/modules/asl/common/llm/LLM12FieldsService.ts // ?16行:读取缓存 const cached = await cache.get(cacheKey); // ?30行:写入缓存 await cache.set(cacheKey, JSON.stringify(result), 3600); // 1小时 ``` **用?*:LLM 12字段提取结果缓存 **重要?*:?高(直接影响API成本? **数据?*:~50KB/? **成本影响**?``` 单次提取成本:~¥0.43/?如果缓存失效,重复调用: - 10?= ¥4.3 - 100?= ¥43 - 1000?= ¥430 ``` --- ### 2.2 长时间异步任? #### **ASL模块:文献筛选任?* ```typescript // 文件:backend/src/modules/asl/services/screeningService.ts // ?3-65?processLiteraturesInBackground(task.id, projectId, literatures); ``` **问题**?- 199篇文献需?33-66分钟 - 当前使用内存队列(MemoryQueue?- SAE实例重启/缩容时任务丢? **影响**?- 用户体验极差(任务突然消失) - 已处理结果丢失,浪费API费用 - 无法追溯任务状? --- ### 2.3 当前架构配置 ```env # backend/.env CACHE_TYPE=memory # ?需要改?redis QUEUE_TYPE=memory # ?需要改?redis ``` --- ## 3. Redis配置信息 ### 3.1 阿里云Redis购买信息 | 配置?| ?| 说明 | |--------|---|------| | **产品** | Redis 开源版 | 完整Redis功能 | | **付费方式** | 包年包月 | 首次购买?折优?| | **部署模式** | 云原生(高可用) | 主从自动切换 | | **系列** | 标准?| 满足需?| | **地域** | 华北2(北京) | 与SAE同地?| | **实例类型** | 高可?| ?99.95%可用?| | **大版?* | Redis 7.0 | 最新稳定版 | | **架构类型** | 不启用集群(单节点) | 满足当前规模 | | **分片规格** | 256 MB | 初期足够 | | **分片数量** | 1 | 单分?| | **读写分离** | 关闭 | 简化配?| ### 3.2 预估成本 ``` 基础价格:?2/年(单机版) 高可用版:?80/年(估算?首次购买:?08/年(6折后? 对比收益?- 节省LLM API费用?¥500/?- 提升用户满意度:无价 - ROI?400% ``` ### 3.3 连接信息(购买后获取? ```env # 阿里云控制台 ?Redis实例 ?连接信息 REDIS_HOST=r-xxxxxxxxxxxx.redis.rds.aliyuncs.com REDIS_PORT=6379 REDIS_PASSWORD=your_secure_password_here REDIS_DB=0 # 或使用连接字符串 REDIS_URL=redis://:your_password@r-xxxxxxxxxxxx.redis.rds.aliyuncs.com:6379/0 ``` --- ## 4. 改造详细步? ### 4.1 Phase 1:本地开发环境准?? #### **步骤1.1:安装依?* ```bash cd backend npm install ioredis --save npm install @types/ioredis --save-dev ``` #### **步骤1.2:配置本地Redis** ```bash # 确认Docker Redis正在运行 docker ps | findstr redis # 如果没有运行,启动它 docker start ai-clinical-redis # 测试连接 docker exec -it ai-clinical-redis redis-cli ping # 应该返回:PONG ``` #### **步骤1.3:更新本?env** ```env # backend/.env DATABASE_URL=postgresql://postgres:postgres123@localhost:5432/ai_clinical_research # ==================== Redis配置 ==================== # 启用Redis缓存 CACHE_TYPE=redis REDIS_HOST=localhost REDIS_PORT=6379 REDIS_DB=0 # REDIS_PASSWORD= # 本地无密? # 队列暂时用内存(分阶段启用) QUEUE_TYPE=memory # ==================== JWT ==================== JWT_SECRET=your-secret-key-change-in-production JWT_EXPIRES_IN=7d # ==================== LLM API ==================== DEEPSEEK_API_KEY=sk-7f8cc37a79fa4799860b38fc7ba2e150 DASHSCOPE_API_KEY=sk-75b4ff29a14a49e79667a331034f3298 # ==================== Dify ==================== DIFY_API_URL=http://localhost/v1 DIFY_API_KEY=dataset-mfvdiKvQ2l3NvxWm7RoYMN3c # ==================== Server ==================== PORT=3001 NODE_ENV=development # ==================== CloseAI配置 ==================== CLOSEAI_API_KEY=sk-cu0iepbXYGGx2jc7BqP6ogtSWmP6fk918qV3RUdtGC3Ed1po CLOSEAI_OPENAI_BASE_URL=https://api.openai-proxy.org/v1 CLOSEAI_CLAUDE_BASE_URL=https://api.openai-proxy.org/anthropic # ==================== 存储配置 ==================== STORAGE_TYPE=local LOCAL_STORAGE_DIR=uploads LOCAL_STORAGE_URL=http://localhost:3001/uploads # ==================== CORS配置 ==================== CORS_ORIGIN=http://localhost:5173 # ==================== 日志配置 ==================== LOG_LEVEL=debug ``` --- ### 4.2 Phase 2:实现RedisCacheAdapter ? #### **步骤2.1:修改RedisCacheAdapter.ts** ```typescript // 文件:backend/src/common/cache/RedisCacheAdapter.ts import Redis from 'ioredis'; import type { CacheAdapter } from './CacheAdapter.js'; import { logger } from '../logging/index.js'; /** * Redis缓存适配? * * 使用ioredis客户端,支持? * - 字符?对象自动序列? * - TTL过期时间 * - 连接池管? * - 错误重试 * * @example * const cache = new RedisCacheAdapter({ * host: 'localhost', * port: 6379, * password: 'xxx', * db: 0 * }); * * await cache.set('key', { data: 'value' }, 60); * const value = await cache.get('key'); */ export class RedisCacheAdapter implements CacheAdapter { private redis: Redis; constructor(options?: { host?: string; port?: number; password?: string; db?: number; }) { this.redis = new Redis({ host: options?.host || 'localhost', port: options?.port || 6379, password: options?.password || undefined, db: options?.db || 0, // 连接配置 retryStrategy: (times) => { const delay = Math.min(times * 50, 2000); logger.warn(`Redis连接重试 ${times} 次,${delay}ms后重试`); return delay; }, maxRetriesPerRequest: 3, enableReadyCheck: true, // 连接池配? lazyConnect: false, keepAlive: 30000, }); // 监听连接事件 this.redis.on('connect', () => { logger.info('Redis连接成功'); }); this.redis.on('error', (error) => { logger.error('Redis连接错误', { error: error.message }); }); this.redis.on('close', () => { logger.warn('Redis连接关闭'); }); this.redis.on('reconnecting', () => { logger.warn('Redis正在重连...'); }); } /** * 获取缓存? */ async get(key: string): Promise { try { const value = await this.redis.get(key); if (!value) { logger.debug('Redis缓存未命?, { key }); return null; } logger.debug('Redis缓存命中', { key, size: value.length }); // 尝试解析JSON try { return JSON.parse(value) as T; } catch { // 如果不是JSON,返回原始字符串 return value as unknown as T; } } catch (error) { logger.error('Redis GET失败', { key, error }); return null; // 降级:返回null而不是抛异常 } } /** * 设置缓存? * @param ttl 过期时间(秒),不传则永不过期(不推荐) */ async set(key: string, value: T, ttl?: number): Promise { try { // 序列化? const serialized = typeof value === 'string' ? value : JSON.stringify(value); // 设置值(带TTL? if (ttl) { await this.redis.setex(key, ttl, serialized); logger.debug('Redis SET成功(带TTL?, { key, ttl, size: serialized.length }); } else { await this.redis.set(key, serialized); logger.warn('Redis SET成功(无TTL?, { key }); // 警告:无过期时间 } } catch (error) { logger.error('Redis SET失败', { key, ttl, error }); // 不抛出异常,允许系统继续运行 } } /** * 删除缓存 */ async delete(key: string): Promise { try { const result = await this.redis.del(key); logger.debug('Redis DEL', { key, deleted: result > 0 }); return result > 0; } catch (error) { logger.error('Redis DEL失败', { key, error }); return false; } } /** * 检查key是否存在 */ async has(key: string): Promise { try { const result = await this.redis.exists(key); return result > 0; } catch (error) { logger.error('Redis EXISTS失败', { key, error }); return false; } } /** * 清空所有缓存(危险操作!) */ async clear(): Promise { try { await this.redis.flushdb(); logger.warn('Redis FLUSHDB执行(所有缓存已清空?); } catch (error) { logger.error('Redis FLUSHDB失败', { error }); } } /** * 测试Redis连接 */ async ping(): Promise { try { const result = await this.redis.ping(); return result === 'PONG'; } catch (error) { logger.error('Redis PING失败', { error }); return false; } } /** * 关闭连接(用于优雅关闭) */ async disconnect(): Promise { try { await this.redis.quit(); logger.info('Redis连接已关?); } catch (error) { logger.error('Redis关闭连接失败', { error }); } } } ``` --- #### **步骤2.2:更新CacheFactory.ts(添加降级策略)** ```typescript // 文件:backend/src/common/cache/CacheFactory.ts import { config } from '../../config/env.js'; import type { CacheAdapter } from './CacheAdapter.js'; import { MemoryCacheAdapter } from './MemoryCacheAdapter.js'; import { RedisCacheAdapter } from './RedisCacheAdapter.js'; import { logger } from '../logging/index.js'; /** * 缓存工厂(单例) * * 根据环境变量自动选择缓存实现? * - CACHE_TYPE=memory ?MemoryCacheAdapter * - CACHE_TYPE=redis ?RedisCacheAdapter(支持降级) * * @example * import { cache } from '@/common/cache' * await cache.set('user:123', userData, 60) * const user = await cache.get('user:123') */ export class CacheFactory { private static instance: CacheAdapter | null = null; private static fallbackToMemory = false; // 降级标记 /** * 获取缓存实例(单例) */ static getInstance(): CacheAdapter { if (!this.instance) { this.instance = this.createCache(); } return this.instance; } /** * 创建缓存实例 */ private static createCache(): CacheAdapter { const cacheType = config.cacheType || 'memory'; logger.info('[CacheFactory] 初始化缓存系?, { cacheType }); switch (cacheType) { case 'redis': return this.createRedisCache(); case 'memory': default: return this.createMemoryCache(); } } /** * 创建Redis缓存(带降级策略? */ private static createRedisCache(): CacheAdapter { try { logger.info('[CacheFactory] 正在连接Redis...', { host: config.redisHost, port: config.redisPort, db: config.redisDb, }); const redisCache = new RedisCacheAdapter({ host: config.redisHost, port: config.redisPort, password: config.redisPassword, db: config.redisDb, }); // 测试连接(同步等待) redisCache.ping().then((isConnected) => { if (isConnected) { logger.info('[CacheFactory] ?Redis连接成功'); } else { logger.error('[CacheFactory] ?Redis连接失败,已降级到内存缓?); this.fallbackToMemory = true; } }).catch((error) => { logger.error('[CacheFactory] ?Redis连接异常,已降级到内存缓?, { error }); this.fallbackToMemory = true; }); return redisCache; } catch (error) { logger.error('[CacheFactory] ?Redis初始化失败,降级到内存缓?, { error }); this.fallbackToMemory = true; return this.createMemoryCache(); } } /** * 创建内存缓存 */ private static createMemoryCache(): MemoryCacheAdapter { logger.info('[CacheFactory] 使用内存缓存'); return new MemoryCacheAdapter(); } /** * 检查是否已降级到内存缓? */ static isFallbackMode(): boolean { return this.fallbackToMemory; } } /** * 导出单例 */ export const cache = CacheFactory.getInstance(); ``` --- #### **步骤2.3:更新env.ts配置** ```typescript // 文件:backend/src/config/env.ts // 确保Redis配置正确读取 export const config = { // ... 其他配置 ... /** 缓存类型 */ cacheType: process.env.CACHE_TYPE || 'memory', /** Redis配置 */ redisHost: process.env.REDIS_HOST || 'localhost', redisPort: parseInt(process.env.REDIS_PORT || '6379', 10), redisPassword: process.env.REDIS_PASSWORD || undefined, redisDb: parseInt(process.env.REDIS_DB || '0', 10), redisUrl: process.env.REDIS_URL || 'redis://localhost:6379', /** 队列类型 */ queueType: process.env.QUEUE_TYPE || 'memory', // ... 其他配置 ... }; // 验证配置 export function validateConfig() { console.log('?[Config] 环境变量加载成功'); console.log('[Config] 应用配置:'); console.log(` - 缓存: ${config.cacheType}`); console.log(` - 队列: ${config.queueType}`); if (config.cacheType === 'redis') { console.log(` - Redis: ${config.redisHost}:${config.redisPort}/${config.redisDb}`); } } ``` --- ### 4.3 Phase 3:本地测?? #### **步骤3.1:创建Redis测试脚本** ```typescript // 文件:backend/src/scripts/test-redis.ts import { cache } from '../common/cache/index.js'; import { logger } from '../common/logging/index.js'; async function testRedis() { console.log('\n🧪 开始测试Redis缓存...\n'); try { // 测试1:基本读? console.log('📝 测试1:基本读?); await cache.set('test:hello', 'world', 10); const value1 = await cache.get('test:hello'); console.log(` ?写入: "hello" ?"world"`); console.log(` ?读取: "${value1}"`); console.assert(value1 === 'world', '值应该匹?); // 测试2:对象序列化 console.log('\n📝 测试2:对象序列化'); const obj = { id: 123, name: '测试', data: [1, 2, 3] }; await cache.set('test:object', obj, 10); const value2 = await cache.get('test:object'); console.log(` ?写入对象:`, obj); console.log(` ?读取对象:`, value2); console.assert(value2?.id === 123, 'ID应该匹配'); // 测试3:TTL过期 console.log('\n📝 测试3:TTL过期?秒)'); await cache.set('test:expire', 'will-expire', 2); console.log(` ?写入(TTL=2秒)`); console.log(` ?等待1?..`); await sleep(1000); const value3a = await cache.get('test:expire'); console.log(` ?1秒后读取: "${value3a}"`); console.assert(value3a === 'will-expire', '应该还存?); console.log(` ?等待2?..`); await sleep(2000); const value3b = await cache.get('test:expire'); console.log(` ?3秒后读取: ${value3b}`); console.assert(value3b === null, '应该已过?); // 测试4:has和delete console.log('\n📝 测试4:has和delete'); await cache.set('test:delete', 'to-be-deleted', 10); const exists1 = await cache.has('test:delete'); console.log(` ?写入后exists: ${exists1}`); await cache.delete('test:delete'); const exists2 = await cache.has('test:delete'); console.log(` ?删除后exists: ${exists2}`); console.assert(!exists2, '应该不存?); // 测试5:大对象?0KB? console.log('\n📝 测试5:大对象缓存(模拟LLM结果?); const bigObj = { literatureId: 'xxx', fields: { 研究类型: 'RCT', 样本? '500', 干预措施: '药物A 100mg', // ... 12个字? }, metadata: { model: 'deepseek-v3', tokens: 8000, timestamp: Date.now(), }, rawOutput: 'x'.repeat(40000), // 模拟大输? }; const start = Date.now(); await cache.set('test:bigobj', bigObj, 3600); const value5 = await cache.get('test:bigobj'); const duration = Date.now() - start; const size = JSON.stringify(bigObj).length; console.log(` ?写入+读取大对? ${size} bytes,耗时 ${duration}ms`); console.assert(value5 !== null, '应该能读?); console.log('\n?所有测试通过!\n'); process.exit(0); } catch (error) { console.error('\n?测试失败:', error); process.exit(1); } } function sleep(ms: number): Promise { return new Promise((resolve) => setTimeout(resolve, ms)); } // 运行测试 testRedis(); ``` #### **步骤3.2:执行测?* ```bash cd backend # 1. 启动后端(确保Redis配置生效?npm run dev # 2. 新开一个终端,运行测试脚本 npx tsx src/scripts/test-redis.ts ``` **预期输出**?``` 🧪 开始测试Redis缓存... 📝 测试1:基本读? ?写入: "hello" ?"world" ?读取: "world" 📝 测试2:对象序列化 ?写入对象: { id: 123, name: '测试', data: [ 1, 2, 3 ] } ?读取对象: { id: 123, name: '测试', data: [ 1, 2, 3 ] } 📝 测试3:TTL过期?秒) ?写入(TTL=2秒) ?等待1?.. ?1秒后读取: "will-expire" ?等待2?.. ?3秒后读取: null 📝 测试4:has和delete ?写入后exists: true ?删除后exists: false 📝 测试5:大对象缓存(模拟LLM结果? ?写入+读取大对? 40123 bytes,耗时 5ms ?所有测试通过?``` --- #### **步骤3.3:测试业务代?* ```bash # 1. 测试HealthCheckService(DC模块?# 上传一个Excel文件,查看日志: [HealthCheck] Cache miss, processing file [HealthCheck] Check completed [HealthCheck] Cache SET: health:xxx, TTL=86400 # 第二次上传同一个文?[HealthCheck] Cache hit ?成功从Redis读取 ``` ```bash # 2. 测试LLM12FieldsService(ASL模块?# 提交全文复筛任务,查看日志: [LLM12FieldsService] 调用LLM提取12字段 [LLM12FieldsService] Result cached with key: fulltext:xxx [LLM12FieldsService] 缓存写入成功 # 重新运行同一篇PDF [LLM12FieldsService] Cache hit, returning cached result ?节省API费用?``` --- ### 4.4 Phase 4:阿里云Redis配置 ? #### **步骤4.1:购买Redis实例** 1. 登录阿里云控制台 2. 进入 **云数据库 Redis** 产品?3. 点击 **创建实例** 4. 按照截图配置选择? - 产品:Redis 开源版 - 部署模式:云原生(高可用? - 地域:华?(北京)—?**与SAE同地域!** - 版本:Redis 7.0 - 分片规格?56 MB - 付费方式:包年包?5. 提交订单并支? #### **步骤4.2:配置白名单** ``` 阿里云控制台 ?Redis实例 ?白名单设? 添加?1. 本地开发IP(用于本地测试) - 你的公网IP/32 2. SAE应用IP(生产环境) - 0.0.0.0/0 (临时,后续改为SAE VPC? ? - SAE实例的VPC网段 ``` #### **步骤4.3:获取连接信?* ``` 阿里云控制台 ?Redis实例 ?连接信息 复制以下信息?- 连接地址:r-xxxxxxxxxxxx.redis.rds.aliyuncs.com - 端口?379 - 实例ID:r-xxxxxxxxxxxx - 密码:点?修改密码"设置 ``` #### **步骤4.4:更新SAE环境变量** ``` 阿里云控制台 ?SAE应用 ?配置管理 ?环境变量 添加?CACHE_TYPE=redis REDIS_HOST=r-xxxxxxxxxxxx.redis.rds.aliyuncs.com REDIS_PORT=6379 REDIS_PASSWORD=你设置的密码 REDIS_DB=0 ``` --- ### 4.5 Phase 5:启用Redis队列 🔴 **必须实施** > **重要变更**:经分析,Redis队列对ASL和DC Tool B模块?*必须?*,不是可选的? > **理由**?小时长任务在SAE环境下不用Redis队列失败?> 95% #### **步骤5.1:安装BullMQ** ```bash cd backend npm install bullmq --save # BullMQ已在package.json中,只需确认安装 npm list bullmq # 应该显示:bullmq@5.65.0 ``` #### **步骤5.2:实现RedisQueue.ts** ```typescript // 文件:backend/src/common/jobs/RedisQueue.ts import { Queue, Worker, Job as BullJob, QueueEvents } from 'bullmq'; import type { Job, JobQueue, JobHandler } from './types.js'; import { logger } from '../logging/index.js'; import { config } from '../../config/env.js'; /** * Redis队列实现(基于BullMQ? * * 核心功能? * - 任务持久化(实例重启不丢失) * - 自动重试(失败后指数退避) * - 分布式任务分配(多实例协调) * - 进度跟踪 */ export class RedisQueue implements JobQueue { private queues: Map = new Map(); private workers: Map = new Map(); private queueEvents: Map = new Map(); private connection = { host: config.redisHost, port: config.redisPort, password: config.redisPassword, db: config.redisDb, }; /** * 推送任务到队列 */ async push(type: string, data: T, options?: any): Promise { try { // 获取或创建队? let queue = this.queues.get(type); if (!queue) { queue = new Queue(type, { connection: this.connection, defaultJobOptions: { removeOnComplete: 100, // 保留最?00个完成任? removeOnFail: false, // 失败任务不删除(便于排查? attempts: 3, // 失败重试3? backoff: { type: 'exponential', delay: 2000, // 2秒?秒?? }, } }); this.queues.set(type, queue); } // 添加任务 const job = await queue.add(type, data, { ...options, jobId: options?.jobId, // 支持自定义jobId }); logger.info(`[RedisQueue] 任务入队成功`, { type, jobId: job.id, dataSize: JSON.stringify(data).length }); return { id: job.id!, type, data, status: 'pending', createdAt: new Date(), }; } catch (error) { logger.error(`[RedisQueue] 任务入队失败`, { type, error }); throw error; } } /** * 注册任务处理? */ process(type: string, handler: JobHandler): void { try { // 创建Worker const worker = new Worker( type, async (job: BullJob) => { logger.info(`[RedisQueue] 开始处理任务`, { type, jobId: job.id, attemptsMade: job.attemptsMade, attemptsTotal: job.opts.attempts }); const startTime = Date.now(); try { // 调用业务处理函数 const result = await handler({ id: job.id!, type, data: job.data as T, status: 'processing', createdAt: new Date(job.timestamp), }); const duration = Date.now() - startTime; logger.info(`[RedisQueue] 任务处理成功`, { type, jobId: job.id, duration: `${duration}ms` }); return result; } catch (error) { const duration = Date.now() - startTime; logger.error(`[RedisQueue] 任务处理失败`, { type, jobId: job.id, attemptsMade: job.attemptsMade, duration: `${duration}ms`, error: error instanceof Error ? error.message : 'Unknown error' }); throw error; // 抛出错误,触发重? } }, { connection: this.connection, concurrency: 1, // 每个Worker并发处理1个任? } ); this.workers.set(type, worker); // 监听Worker事件 worker.on('completed', (job) => { logger.info(`[RedisQueue] ?任务完成`, { type, jobId: job.id, returnvalue: job.returnvalue }); }); worker.on('failed', (job, err) => { logger.error(`[RedisQueue] ?任务失败`, { type, jobId: job?.id, attemptsMade: job?.attemptsMade, error: err.message, stack: err.stack }); }); worker.on('error', (err) => { logger.error(`[RedisQueue] Worker错误`, { type, error: err.message }); }); logger.info(`[RedisQueue] Worker已注册`, { type }); } catch (error) { logger.error(`[RedisQueue] Worker注册失败`, { type, error }); throw error; } } /** * 获取任务状? */ async getJob(id: string): Promise { try { // 遍历所有队列查找任? for (const [type, queue] of this.queues) { const job = await queue.getJob(id); if (job) { return { id: job.id!, type, data: job.data, status: await this.getJobStatus(job), progress: job.progress as number || 0, createdAt: new Date(job.timestamp), error: job.failedReason, }; } } return null; } catch (error) { logger.error(`[RedisQueue] getJob失败`, { id, error }); return null; } } /** * 获取任务状? */ private async getJobStatus(job: BullJob): Promise { const state = await job.getState(); switch (state) { case 'completed': return 'completed'; case 'failed': return 'failed'; case 'active': return 'processing'; case 'waiting': return 'pending'; case 'delayed': return 'pending'; default: return 'pending'; } } /** * 更新任务进度 */ async updateProgress(id: string, progress: number, message?: string): Promise { try { for (const queue of this.queues.values()) { const job = await queue.getJob(id); if (job) { await job.updateProgress(progress); if (message) { await job.log(message); } logger.debug(`[RedisQueue] 进度更新`, { id, progress, message }); return; } } logger.warn(`[RedisQueue] 任务不存在,无法更新进度`, { id }); } catch (error) { logger.error(`[RedisQueue] 更新进度失败`, { id, error }); } } /** * 取消任务 */ async cancelJob(id: string): Promise { try { for (const queue of this.queues.values()) { const job = await queue.getJob(id); if (job) { await job.remove(); logger.info(`[RedisQueue] 任务已取消`, { id }); return true; } } logger.warn(`[RedisQueue] 任务不存在,无法取消`, { id }); return false; } catch (error) { logger.error(`[RedisQueue] 取消任务失败`, { id, error }); return false; } } /** * 重试失败任务 */ async retryJob(id: string): Promise { try { for (const queue of this.queues.values()) { const job = await queue.getJob(id); if (job) { await job.retry(); logger.info(`[RedisQueue] 任务已重试`, { id }); return true; } } logger.warn(`[RedisQueue] 任务不存在,无法重试`, { id }); return false; } catch (error) { logger.error(`[RedisQueue] 重试任务失败`, { id, error }); return false; } } /** * 清理旧任? */ async cleanup(olderThan: number = 86400000): Promise { try { let totalCleaned = 0; for (const [type, queue] of this.queues) { // 清理完成的任务(保留最?00个) const completed = await queue.clean(olderThan, 100, 'completed'); // 清理失败的任务(保留最?0个) const failed = await queue.clean(olderThan, 50, 'failed'); const cleaned = completed.length + failed.length; totalCleaned += cleaned; if (cleaned > 0) { logger.info(`[RedisQueue] 队列清理完成`, { type, completed: completed.length, failed: failed.length }); } } return totalCleaned; } catch (error) { logger.error(`[RedisQueue] 清理任务失败`, { error }); return 0; } } /** * 关闭所有连接(优雅关闭? */ async close(): Promise { try { // 关闭所有Workers for (const [type, worker] of this.workers) { await worker.close(); logger.info(`[RedisQueue] Worker已关闭`, { type }); } // 关闭所有Queues for (const [type, queue] of this.queues) { await queue.close(); logger.info(`[RedisQueue] Queue已关闭`, { type }); } // 关闭所有QueueEvents for (const [type, events] of this.queueEvents) { await events.close(); logger.info(`[RedisQueue] QueueEvents已关闭`, { type }); } logger.info(`[RedisQueue] 所有连接已关闭`); } catch (error) { logger.error(`[RedisQueue] 关闭连接失败`, { error }); } } } ``` #### **步骤5.3:更新JobFactory支持Redis队列** ```typescript // 文件:backend/src/common/jobs/JobFactory.ts import { JobQueue } from './types.js' import { MemoryQueue } from './MemoryQueue.js' import { RedisQueue } from './RedisQueue.js' // ?新增 import { logger } from '../logging/index.js' import { config } from '../../config/env.js' export class JobFactory { private static instance: JobQueue | null = null static getInstance(): JobQueue { if (!this.instance) { this.instance = this.createQueue() } return this.instance } private static createQueue(): JobQueue { const queueType = config.queueType || 'memory' logger.info('[JobFactory] 初始化任务队?, { queueType }); switch (queueType) { case 'redis': // ?新增 return this.createRedisQueue() case 'memory': return this.createMemoryQueue() default: logger.warn(`[JobFactory] Unknown QUEUE_TYPE: ${queueType}, fallback to memory`) return this.createMemoryQueue() } } /** * 创建Redis队列(带降级策略? */ private static createRedisQueue(): JobQueue { try { logger.info('[JobFactory] 正在连接Redis队列...'); const redisQueue = new RedisQueue(); logger.info('[JobFactory] ?Redis队列初始化成?); return redisQueue; } catch (error) { logger.error('[JobFactory] ?Redis队列初始化失败,降级到内存队?, { error }); return this.createMemoryQueue(); } } private static createMemoryQueue(): MemoryQueue { logger.info('[JobFactory] 使用内存队列') const queue = new MemoryQueue() // 定期清理(避免内存泄漏) if (process.env.NODE_ENV !== 'test') { setInterval(() => { queue.cleanup() }, 60 * 60 * 1000) } return queue } static reset(): void { this.instance = null } } ``` #### **步骤5.4:修改业务代码使用队?* ```typescript // 示例:ASL文献筛选改?// 文件:backend/src/modules/asl/services/screeningService.ts import { jobQueue } from '../../../common/jobs/index.js'; export async function startScreeningTask(projectId: string, userId: string) { // 1. 创建数据库任务记? const task = await prisma.aslScreeningTask.create({ data: { projectId, status: 'pending', totalItems: literatures.length, // ... } }); // 2. 推送到Redis队列(不阻塞请求? await jobQueue.push('asl:title-screening', { taskId: task.id, projectId, literatureIds: literatures.map(lit => lit.id), }); logger.info('任务已入?, { taskId: task.id }); // 3. 立即返回(前端轮询进度) return task; } // 注册Worker(在应用启动时) // 文件:backend/src/index.ts jobQueue.process('asl:title-screening', async (job) => { const { taskId, projectId, literatureIds } = job.data; logger.info('开始处理筛选任?, { taskId, total: literatureIds.length }); for (let i = 0; i < literatureIds.length; i++) { const literatureId = literatureIds[i]; // 处理单篇文献 await processSingleLiterature(literatureId, projectId); // 更新进度 const progress = ((i + 1) / literatureIds.length) * 100; await jobQueue.updateProgress(job.id, progress); // 更新数据? await prisma.aslScreeningTask.update({ where: { id: taskId }, data: { processedItems: i + 1 } }); } // 标记完成 await prisma.aslScreeningTask.update({ where: { id: taskId }, data: { status: 'completed', completedAt: new Date() } }); logger.info('筛选任务完?, { taskId }); return { success: true, processed: literatureIds.length }; }); ``` #### **步骤5.5:更?env配置** ```env # backend/.env # ==================== 任务队列配置 ==================== QUEUE_TYPE=redis # ?改为redis # Redis配置(与缓存共用?REDIS_HOST=localhost REDIS_PORT=6379 REDIS_PASSWORD= REDIS_DB=0 ``` #### **步骤5.6:测试Redis队列** ```bash # 1. 启动后端 cd backend npm run dev # 应该看到日志?# [JobFactory] 正在连接Redis队列... # [JobFactory] ?Redis队列初始化成? # 2. 提交测试任务(使用REST Client或Postman?POST http://localhost:3001/api/v1/asl/projects/:projectId/screening # 3. 观察日志 # [RedisQueue] 任务入队成功 { type: 'asl:title-screening', jobId: '1' } # [RedisQueue] 开始处理任?{ type: 'asl:title-screening', jobId: '1' } # [RedisQueue] ?任务完成 { type: 'asl:title-screening', jobId: '1' } # 4. 测试实例重启恢复 # 提交任务 ?等待处理?0% ?Ctrl+C停止 ?重新启动 # 任务应该自动从Redis恢复并继续处?``` --- ## 5. 测试方案 ### 5.1 单元测试清单 | 测试?| 预期结果 | 实际结果 | 状?| |--------|---------|---------|------| | Redis连接测试 | PONG | ?| ?待测 | | 基本读写测试 | 值匹?| ?| ?待测 | | 对象序列化测?| 对象完整 | ?| ?待测 | | TTL过期测试 | 2秒后为null | ?| ?待测 | | 大对象测试(50KB?| <10ms | ?| ?待测 | | 降级策略测试 | 自动切换内存 | ?| ?待测 | ### 5.2 集成测试清单 | 测试?| 操作步骤 | 预期结果 | 状?| |--------|---------|---------|------| | **HealthCheckService** | 上传Excel 2?| ?次缓存命?| ?待测 | | **LLM12FieldsService** | 提取同一PDF 2?| ?次缓存命?| ?待测 | | **多实例缓存共?* | 启动2个后端实例,实例A写入,实例B读取 | B能读到A的缓?| ?待测 | | **实例重启数据持久?* | 写入缓存 ?重启后端 ?读取 | 能读取到 | ?待测 | ### 5.3 压力测试 ```bash # 使用ab或wrk测试并发读写 # 测试1:并发写?ab -n 1000 -c 10 http://localhost:3001/api/test/cache-write # 测试2:并发读?ab -n 10000 -c 50 http://localhost:3001/api/test/cache-read # 预期结果?# - QPS > 1000 # - 响应时间 < 50ms # - 错误?= 0% ``` ### 5.4 故障模拟测试 | 故障场景 | 模拟方法 | 预期行为 | 状?| |---------|---------|---------|------| | **Redis突然挂掉** | `docker stop ai-clinical-redis` | 系统降级到内存缓存,应用继续运行 | ?待测 | | **Redis网络延迟** | `tc qdisc add dev eth0 root netem delay 500ms` | 超时重试,最终返回null | ?待测 | | **Redis内存?* | 写入大量数据?56MB | 触发LRU驱逐,不影响新写入 | ?待测 | | **Redis密码错误** | 修改密码 | 连接失败,降级到内存缓存 | ?待测 | --- ## 6. 风险评估与缓? ### 6.1 风险矩阵 | 风险 | 严重?| 概率 | 影响 | 缓解措施 | 状?| |------|--------|------|------|----------|------| | **Redis连接失败** | 🔴 ?| 🟡 ?| 系统不可?| ?降级策略 | ?已实?| | **数据丢失** | 🟡 ?| 🟢 ?| 缓存失效 | ?关键数据双写DB | ?待实?| | **内存溢出(OOM?* | 🔴 ?| 🟡 ?| Redis崩溃 | ?严格TTL + 监控 | ?待实?| | **网络延迟** | 🟢 ?| 🟢 ?| 响应变慢 | ?批量操作 | ?可选优?| | **配置错误** | 🟡 ?| 🟡 ?| 启动失败 | ?配置验证 | ?已实?| | **密码泄露** | 🔴 ?| 🟡 ?| 数据泄露 | ?KMS管理 | ?待实?| ### 6.2 关键缓解措施 #### **缓解措施1:降级策略(必须?* ?```typescript // 已在CacheFactory中实?// Redis不可用时自动切换到MemoryCache ``` #### **缓解措施2:关键数据双写(推荐?* ?```typescript // 需要在业务代码中添? // 示例:任务进度双?export async function updateTaskProgress(taskId: string, progress: number) { // 1. 写Redis(快速查询) await cache.set(`task:${taskId}:progress`, progress, 3600); // 2. 同时写DB(持久化? await prisma.aslScreeningTask.update({ where: { id: taskId }, data: { processedItems: progress } }); } ``` #### **缓解措施3:内存监控(推荐?* ?```typescript // 创建:backend/src/scripts/monitor-redis.ts import { cache } from '../common/cache/index.js'; setInterval(async () => { // 检查内存使? const info = await redis.info('memory'); const used = parseInt(info.match(/used_memory:(\d+)/)[1]); const max = 256 * 1024 * 1024; if (used > max * 0.8) { logger.warn('⚠️ Redis内存使用超过80%', { used, max }); // TODO: 发送钉?邮件告警 } }, 60000); ``` #### **缓解措施4:配置验证(已实现)** ?```typescript // env.ts中的validateConfig() // 启动时检查Redis配置 ``` --- ## 7. 上线计划 ### 7.1 上线时间表(V2.0更新? | 阶段 | 时间 | 任务 | 负责?| 状?| |------|------|------|--------|------| | **Phase 1** | Day 1上午 | 本地开发环境准?| 开?| ?待开?| | **Phase 2** | Day 1下午 | 实现RedisCacheAdapter | 开?| ?待开?| | **Phase 3** | Day 2全天 | Redis缓存本地测试 | 开?测试 | ?待开?| | **Phase 4** | Day 3上午 | 阿里云Redis购买&配置 | 运维 | ?待开?| | **Phase 5** | Day 3下午-Day 5 | 🔴 实现RedisQueue(必须)| 开?| ?待开?| | **Phase 6** | Day 6全天 | Redis队列本地测试 + 业务集成 | 开?测试 | ?待开?| | **Phase 7** | Day 7上午 | SAE测试环境验证 | 开?测试 | ?待开?| | **Phase 8** | Day 7下午 | 生产环境上线 | 全员 | ?待开?| | **Phase 9** | Day 7? | 监控观察?4小时?| 运维 | ?待开?| **总工作量**?天(比原计划增加4天,但确保核心功能可用) ### 7.2 上线步骤(生产环境) #### **Step 1:发布前检查(15分钟?* ```bash ?代码已提交Git ?本地测试全部通过 ?阿里云Redis已就??SAE环境变量已配??回滚方案已准??监控已就?``` #### **Step 2:灰度发布(30分钟?* ``` 1. SAE控制??选择应用 2. 应用部署 ?分批发布 3. ?批:10%实例(观?5分钟?4. ?批:50%实例(观?0分钟?5. ?批:100%实例 ``` #### **Step 3:验证(15分钟?* ```bash # 1. 检查Redis连接 curl https://your-api.com/api/health # 应该返回:{ cache: "redis", status: "ok" } # 2. 测试缓存写入 # 上传Excel ?查看日志 ?确认Redis写入 # 3. 测试缓存读取 # 再次上传 ?查看日志 ?确认缓存命中 # 4. 检查Redis内存 阿里云控制台 ?Redis监控 ?内存使用 ``` #### **Step 4:监控观察(24小时?* ``` 关注指标?- Redis连接数(应该稳定?- 内存使用率(应该 < 50%?- 缓存命中率(应该 > 80%?- 应用错误日志(应该无Redis相关错误?- API成本(应该下降) ``` --- ## 8. 回滚方案 ### 8.1 快速回滚(5分钟内) #### **场景1:Redis连接失败,应用无法启?* ```bash # 方法1:修改SAE环境变量 阿里云控制台 ?SAE应用 ?配置管理 ?环境变量 修改:CACHE_TYPE=memory 保存 ?应用重启 # 方法2:重新部署上一个版?SAE控制??应用部署 ?版本管理 ?回滚 ``` #### **场景2:Redis性能问题,响应变?* ```bash # 临时降级到内存缓?CACHE_TYPE=memory # 或保留Redis但检查网?ping r-xxxxxxxxxxxx.redis.rds.aliyuncs.com ``` #### **场景3:Redis内存满,无法写入** ```bash # 方法1:清理Redis(危险!?redis-cli -h r-xxx.redis.rds.aliyuncs.com -a password > FLUSHDB # 方法2:升级Redis规格 阿里云控制台 ?Redis实例 ?变配 ?512MB ``` ### 8.2 回滚检查清? ```bash ?应用能否正常启动??缓存是否工作(内存模式)??API响应是否正常??错误日志是否清除??用户是否能正常使用? ``` --- ## 9. 监控与运? ### 9.1 监控指标 #### **Redis指标(阿里云控制台)** | 指标 | 正常范围 | 告警阈?| 处理方案 | |------|---------|---------|---------| | **内存使用?* | < 50% | > 80% | 检查大key,考虑升配 | | **连接?* | < 50 | > 100 | 检查连接泄?| | **QPS** | < 1000 | > 5000 | 考虑分片 | | **命中?* | > 80% | < 50% | 检查缓存策?| | **响应时间** | < 5ms | > 50ms | 检查网?| #### **应用指标(SAE日志?* ```bash # 查找Redis相关错误 grep "Redis" logs/app.log | grep "ERROR" # 查找缓存命中情况 grep "Cache hit" logs/app.log | wc -l grep "Cache miss" logs/app.log | wc -l # 计算命中?命中?= hits / (hits + misses) * 100% ``` ### 9.2 运维命令 #### **常用Redis CLI命令** ```bash # 连接Redis redis-cli -h r-xxx.redis.rds.aliyuncs.com -p 6379 -a your_password # 查看所有key KEYS * # 查看任务相关key KEYS task:* # 查看缓存相关key KEYS fulltext:* # 查看key的TTL TTL task:abc123:progress # 查看key的?GET task:abc123:progress # 查看内存使用 INFO memory # 查看连接?INFO clients # 实时监控命令 MONITOR # 清空数据库(危险!) FLUSHDB ``` #### **内存分析** ```bash # 查找大key?10KB?redis-cli -h xxx -a password --bigkeys # 查看key的内存占?MEMORY USAGE task:abc123:progress ``` ### 9.3 故障排查流程 ``` 问题:Redis连接失败 ?1. 检查Redis实例状态(阿里云控制台? ?2. 检查白名单配置(是否包含SAE IP? ?3. 检查密码是否正? ?4. ping测试网络连通? ?5. 查看应用日志 ?6. 如无法快速解??降级到内存缓?``` ### 9.4 定期维护 | 维度 | 频率 | 内容 | |------|------|------| | **日常监控** | 每天 | 查看内存使用、连接数、错误日?| | **性能分析** | 每周 | 分析缓存命中率、响应时?| | **容量评估** | 每月 | 评估256MB是否够用,是否需要升?| | **安全检?* | 每月 | 检查白名单、密码强度、访问日?| --- ## 10. 成功标准 ### 10.1 技术指标(V2.0更新? | 指标 | 当前值(内存?| 目标值(Redis?| 衡量方法 | |------|---------------|----------------|---------| | **缓存持久?* | ?实例重启丢失 | ?持久化保?| 重启后仍能读?| | **多实例共?* | ?各实例独?| ?全局共享 | 实例A写,实例B能读 | | **LLM API重复调用** | 🔴 ?| 🟢 ?| 同一PDF只调??| | **缓存命中?* | N/A | > 60% | 监控日志统计 | | **任务持久?* | ?实例销毁丢?| ?任务继续 | 🔴 **新增** | | **长任务成功率** | 10-30% | > 99% | 🔴 **关键指标** | | **系统可用?* | 99% | 99.9% | 降级策略保障 | ### 10.2 业务指标(V2.0更新? | 指标 | 改造前 | 改造后 | 衡量方法 | |------|--------|--------|---------| | **LLM API成本** | ¥X/?| 降低40-60% | 对比账单 | | **任务丢失?* | 🔴 70-95% | < 1% | 🔴 **最重要** | | **任务完成时间** | 不确?| 稳定 | 监控日志 | | **用户重复提交次数** | 平均3?| 几乎? | 用户行为分析 | | **用户满意?* | 基线 | 显著提升 | 问卷调查 | ### 10.3 验收标准(V2.0更新? #### **Redis缓存验收** ```bash ?所有缓存单元测试通过 ?HealthCheckService缓存命中测试通过 ?LLM12FieldsService缓存命中测试通过 ?实例重启后缓存仍存在 ?缓存命中?> 60% ?LLM API调用次数下降 > 40% ``` #### **Redis队列验收** 🔴 **新增关键?* ```bash ?Redis队列单元测试通过 ?长任务(2小时)测试通过 ?实例重启后任务自动恢??任务失败自动重试?次) ?1000篇文献筛选成功率 > 99% ?无用户投诉任务丢??进度实时更新正常 ``` #### **故障恢复测试** 🔴 **重要** ```bash ?模拟实例销??任务自动恢复 ?模拟Redis宕机 ?系统降级运行 ?模拟网络延迟 ?任务正常完成 ?模拟并发任务 ?正确分配处理 ``` #### **生产环境验收** ```bash ?生产环境运行48小时无错??2个完整的1000篇文献筛选任务成??监控指标正常(内存、连接数、QPS??无用户投?``` --- ## 11. FAQ ### Q1:如果Redis在半夜突然挂了怎么办? **A1**:系统会自动降级到内存缓存,应用继续运行。第二天运维检查并恢复Redis? ### Q2?56MB够用吗?什么时候需要升配? **A2**?- 当前预估? 50% 使用?- 触发升配信号? - 内存使用 > 80% - 频繁触发LRU驱? - 监控告警 - 升配方式:阿里云控制台一键升级,无需重启 ### Q3:Redis会影响系统性能吗? **A3**?- 本地内存? 0.1ms - 本地Redis:~1ms - 阿里云Redis(同地域):~2-5ms - 影响可忽略,且有批量操作优化 ### Q4:如果发现Redis不适合,能回退到内存缓存吗?**A4**:可以!修改 `CACHE_TYPE=memory` 即可,代码支持热切换? ### Q5:需要学习Redis命令吗? **A5**?- 开发:不需要,代码已封?- 运维:建议学?个基础命令(GET/SET/KEYS/TTL/INFO? --- ## 12. 相关文档 - [云原生开发规范](../04-开发规?08-云原生开发规?md) - [SAE部署完全指南](./02-SAE部署完全指南(产品经理?.md) - [SAE环境变量配置指南](./03-SAE环境变量配置指南.md) - [Redis官方文档](https://redis.io/docs/) - [ioredis文档](https://github.com/redis/ioredis) - [阿里云Redis文档](https://help.aliyun.com/product/26340.html) --- ## 13. 附录 ### 附录A:完整的.env配置模板 ```env # ==================== 数据?==================== DATABASE_URL=postgresql://postgres:password@localhost:5432/ai_clinical_research # ==================== Redis配置 ==================== CACHE_TYPE=redis REDIS_HOST=localhost REDIS_PORT=6379 REDIS_PASSWORD= REDIS_DB=0 # 或使用连接字符串 # REDIS_URL=redis://localhost:6379 # ==================== 队列配置 ==================== QUEUE_TYPE=memory # 第一阶段用memory,第二阶段改为redis # ==================== JWT ==================== JWT_SECRET=your-secret-key-change-in-production JWT_EXPIRES_IN=7d # ==================== LLM API ==================== DEEPSEEK_API_KEY=sk-xxxxxxxxxxxxxx DASHSCOPE_API_KEY=sk-xxxxxxxxxxxxxx CLOSEAI_API_KEY=sk-xxxxxxxxxxxxxx CLOSEAI_OPENAI_BASE_URL=https://api.openai-proxy.org/v1 CLOSEAI_CLAUDE_BASE_URL=https://api.openai-proxy.org/anthropic # ==================== Dify ==================== DIFY_API_URL=http://localhost/v1 DIFY_API_KEY=dataset-xxxxxxxxxxxxxx # ==================== Server ==================== PORT=3001 NODE_ENV=development # ==================== 存储配置 ==================== STORAGE_TYPE=local LOCAL_STORAGE_DIR=uploads LOCAL_STORAGE_URL=http://localhost:3001/uploads # ==================== CORS配置 ==================== CORS_ORIGIN=http://localhost:5173 # ==================== 日志配置 ==================== LOG_LEVEL=debug ``` ### 附录B:Redis内存计算? ``` 单个LLM结果缓存:~50KB 单个健康检查缓存:~5KB 预估容量?- 1000个LLM结果 = 50MB - 1000个健康检?= 5MB - 系统开销 = 20MB ----------------------------- 总计 = 75MB / 256MB = 29% 使用?``` ### 附录C:故障演练脚? ```bash #!/bin/bash # 文件:backend/scripts/disaster-recovery-drill.sh echo "🚨 Redis故障演练开?.." # 1. 停止Redis echo "1. 停止Redis..." docker stop ai-clinical-redis sleep 2 # 2. 测试应用是否正常 echo "2. 测试应用健康检?.." response=$(curl -s http://localhost:3001/api/health) echo "响应: $response" if [[ $response == *"memory"* ]]; then echo "?降级成功,使用内存缓? else echo "?降级失败" exit 1 fi # 3. 恢复Redis echo "3. 恢复Redis..." docker start ai-clinical-redis sleep 5 # 4. 测试Redis恢复 echo "4. 测试Redis恢复..." response=$(curl -s http://localhost:3001/api/health) echo "响应: $response" if [[ $response == *"redis"* ]]; then echo "?Redis恢复成功" else echo "⚠️ Redis未恢复,仍使用内存缓? fi echo "🎉 故障演练完成? ``` --- **文档维护者:** 技术团? **最后更新:** 2025-12-12 **文档状态:** ?待审? **下次更新?* 改造完成后总结经验教训 --- ## ?改造完成检查清? 在完成Redis改造后,请逐项检查: ### 代码层面 - [ ] `ioredis` 已安?- [ ] `RedisCacheAdapter` 已实?- [ ] `CacheFactory` 已添加降级逻辑 - [ ] `.env` 配置已更?- [ ] 所有使?`cache.set()` 的地方都设置了TTL ### 测试层面 - [ ] 单元测试全部通过 - [ ] 集成测试全部通过 - [ ] 压力测试达标 - [ ] 故障模拟测试通过 ### 部署层面 - [ ] 阿里云Redis已购?- [ ] 白名单已配置 - [ ] SAE环境变量已配?- [ ] 生产环境已验? ### 文档层面 - [ ] 改造文档已更新 - [ ] 运维文档已补?- [ ] 监控指标已记?- [ ] 经验教训已总结 --- **祝改造顺利!如有问题,请及时沟通?* 🚀