5.9 KiB
5.9 KiB
上传会话存储方案对比
🔍 快速对比
场景1:单实例部署(当前情况)
┌─────────────────┐
│ Docker容器 │
│ ┌───────────┐ │
│ │ Next.js │ │
│ │ 内存Map │ │ ← 可以工作 ✅
│ └───────────┘ │
└─────────────────┘
用户请求 → 同一个容器 → 内存Map中有数据 → 正常工作
场景2:多实例部署(未来可能)
┌──────────────┐
│ 负载均衡器 │
└──────┬───────┘
│
┌───────┴───────┐
│ │
┌────▼───┐ ┌───▼────┐
│容器1 │ │容器2 │
│内存Map │ │内存Map │
└────────┘ └────────┘
用户请求1 → 容器1 → 初始化上传 → 数据存在容器1内存
用户请求2 → 容器2 → 上传块 → 容器2找不到数据 → 失败 ❌
场景3:使用Redis(多实例)
┌──────────────┐
│ 负载均衡器 │
└──────┬───────┘
│
┌───────┴───────┐
│ │
┌────▼───┐ ┌───▼────┐
│容器1 │ │容器2 │
└────┬───┘ └───┬────┘
│ │
└──────┬───────┘
│
┌─────▼─────┐
│ Redis │ ← 共享存储 ✅
└───────────┘
用户请求1 → 容器1 → 初始化上传 → 数据存在Redis
用户请求2 → 容器2 → 上传块 → 从Redis读取数据 → 正常工作 ✅
📊 详细对比表
| 特性 | 内存Map | Redis |
|---|---|---|
| 数据存储位置 | 进程内存 | 独立内存(可持久化) |
| 数据持久化 | ❌ 进程重启丢失 | ✅ 可配置RDB/AOF |
| 多实例支持 | ❌ 每个实例独立 | ✅ 全局共享 |
| 自动过期 | ❌ 需手动清理 | ✅ 支持TTL |
| 部署复杂度 | ✅ 简单(无需额外服务) | ❌ 需要Redis服务 |
| 性能 | ✅ 极快(本地内存访问) | ✅ 很快(网络内存访问) |
| 内存占用 | ✅ 小(应用内存) | ❌ 额外进程(~10-50MB) |
| 高可用性 | ❌ 单点故障 | ✅ 支持主从、集群 |
| 跨服务共享 | ❌ 无法跨进程 | ✅ 支持跨服务 |
| 适用场景 | 单实例、开发测试 | 生产环境、多实例 |
🎯 实际案例
案例1:服务器重启
时间线:
10:00 - 用户开始上传1GB文件
10:05 - 上传进度50%
10:10 - 服务器重启(部署更新)
内存Map方案:
- 上传会话丢失 ❌
- 用户需要重新开始上传
- 断点续传功能失效
Redis方案:
- 上传会话保存在Redis(如果配置了持久化)
- 服务器重启后,会话数据仍然存在
- 用户可以从50%继续上传 ✅
案例2:多实例部署
场景:使用Nginx负载均衡,2个容器实例
用户上传流程:
1. 初始化上传 → 请求路由到容器1 → 会话存在容器1内存
2. 上传块1 → 请求路由到容器2 → 容器2找不到会话 → 失败 ❌
Redis方案:
1. 初始化上传 → 请求路由到容器1 → 会话存在Redis
2. 上传块1 → 请求路由到容器2 → 从Redis读取会话 → 成功 ✅
案例3:内存泄漏
内存Map方案:
- 如果清理函数失败或未执行
- 内存会持续增长
- 最终导致OOM(内存溢出)
Redis方案:
- 使用TTL自动过期
- 即使应用崩溃,Redis也会自动清理
- 内存管理更可靠
💡 迁移建议
阶段1:当前(单实例)
- ✅ 继续使用内存Map
- ✅ 简单、快速、无需额外服务
- ✅ 适合当前部署方式
阶段2:扩展(多实例)
- 🔄 迁移到Redis
- 🔄 支持负载均衡
- 🔄 支持高可用性
阶段3:优化(生产环境)
- 🚀 Redis集群
- 🚀 主从复制
- 🚀 自动故障转移
📝 代码迁移示例
当前代码(内存Map)
// lib/upload-sessions.ts
export const uploadSessions = new Map<string, UploadSession>()
// 使用
uploadSessions.set(uploadId, session)
const session = uploadSessions.get(uploadId)
迁移后(Redis)
// lib/upload-sessions.ts
import { saveUploadSession, getUploadSession } from './upload-sessions-redis'
// 使用(接口相同)
await saveUploadSession(uploadId, session)
const session = await getUploadSession(uploadId)
适配层(平滑迁移)
// lib/upload-sessions-adapter.ts
const USE_REDIS = process.env.USE_REDIS === 'true'
export async function saveSession(uploadId: string, session: UploadSession) {
if (USE_REDIS) {
return saveUploadSessionRedis(uploadId, session)
} else {
uploadSessionsMemory.set(uploadId, session)
}
}
🚀 总结
什么时候用内存Map?
- ✅ 单实例部署
- ✅ 开发/测试环境
- ✅ 小规模使用
- ✅ 简单部署
什么时候用Redis?
- ✅ 多实例部署
- ✅ 生产环境
- ✅ 高可用性要求
- ✅ 数据持久化需求
- ✅ 大量并发上传
当前建议
- 继续使用内存Map(当前单实例部署可以工作)
- 准备Redis方案(为未来扩展做准备)
- 实现适配层(可以平滑迁移)