gongxiangp2/docs/resume-upload-flowchart.md

10 KiB
Raw Blame History

断点续传流程图:前端IndexedDB vs 后端内存Map/Redis

🔄 完整上传流程

┌─────────────────────────────────────────────────────────────────┐
│                    断点续传完整流程                                │
└─────────────────────────────────────────────────────────────────┘

用户操作               前端(IndexedDB)          后端(内存Map/Redis)
─────────────────────────────────────────────────────────────────
1. 选择文件
   └─> 生成fileId
       └─> 检查IndexedDB
           └─> 没有记录 → 新上传
           
2. 初始化上传
   └─> 调用 /api/upload/init
       └─>                    └─> 创建uploadSession
                               └─> 保存到内存Map/Redis
                               └─> 返回uploadId
   └─> 保存到IndexedDB
       fileId, uploadId, uploadedChunks: []
       
3. 上传块1
   └─> uploadChunk(uploadId, 0, chunk1)
       └─>                    └─> 验证uploadSession
                               └─> 保存块文件到磁盘
                               └─> 更新uploadSession.chunks
                               └─> chunks[0] = "/path/to/chunk-0"
   └─> 更新IndexedDB
       uploadedChunks: [0]
       
4. 上传块2
   └─> uploadChunk(uploadId, 1, chunk2)
       └─>                    └─> 验证uploadSession
                               └─> 保存块文件到磁盘
                               └─> 更新uploadSession.chunks
                               └─> chunks[1] = "/path/to/chunk-1"
   └─> 更新IndexedDB
       uploadedChunks: [0, 1]
       
5. ... (继续上传其他块)
   
6. 完成上传
   └─> completeUpload(uploadId)
       └─>                    └─> 从uploadSession读取所有块路径
                               └─> 合并所有块文件
                               └─> 创建数据库记录
                               └─> 删除uploadSession
                               └─> 清理临时文件
   └─> 删除IndexedDB记录

🔄 页面刷新恢复流程

┌─────────────────────────────────────────────────────────────────┐
│                    页面刷新恢复流程                                │
└─────────────────────────────────────────────────────────────────┘

用户操作               前端(IndexedDB)          后端(内存Map/Redis)
─────────────────────────────────────────────────────────────────
1. 刷新页面
   └─> 重新加载文件
       └─> 生成fileId(相同文件 → 相同fileId)
           └─> 检查IndexedDB
               └─> ✅ 找到记录
                   uploadId: "upload-xxx"
                   uploadedChunks: [0, 1, 2]
                   
2. 恢复上传
   └─> 跳过已上传块 [0, 1, 2]
   └─> 从块3开始上传
       └─> uploadChunk(uploadId, 3, chunk3)
           └─>                    └─> 检查uploadSession
                                   └─> ✅ 找到会话(如果后端数据还在)
                                   └─> 保存块3
                                   └─> 更新uploadSession.chunks
   └─> 更新IndexedDB
       uploadedChunks: [0, 1, 2, 3]
       
3. 继续上传剩余块
   └─> 块4, 5, 6...

⚠️ 服务器重启场景对比

场景A:使用内存Map(当前实现)

时间线                前端(IndexedDB)          后端(内存Map)
─────────────────────────────────────────────────────────────────
T1: 上传块1-5
    └─> uploadedChunks: [0,1,2,3,4]  └─> chunks: {0,1,2,3,4}
    
T2: 服务器重启
    └─> uploadedChunks: [0,1,2,3,4]  └─> ❌ 数据丢失
    
T3: 继续上传块6
    └─> uploadChunk(uploadId, 5, chunk5)
        └─>                    └─> ❌ uploadSession不存在
                                └─> 返回错误:会话不存在
    └─> ❌ 无法恢复
    └─> 需要重新初始化上传
    └─> 但之前的块文件还在临时目录
    └─> ❌ 无法恢复,需要重新上传

场景B:使用Redis(生产环境推荐)

时间线                前端(IndexedDB)          后端(Redis)
─────────────────────────────────────────────────────────────────
T1: 上传块1-5
    └─> uploadedChunks: [0,1,2,3,4]  └─> chunks: {0,1,2,3,4}
                                      └─> ✅ 持久化到磁盘
    
T2: 服务器重启
    └─> uploadedChunks: [0,1,2,3,4]  └─> ✅ 从磁盘恢复数据
                                      └─> chunks: {0,1,2,3,4}
    
T3: 继续上传块6
    └─> uploadChunk(uploadId, 5, chunk5)
        └─>                    └─> ✅ 从Redis读取uploadSession
                                └─> ✅ 找到会话
                                └─> 保存块5
                                └─> 更新Redis
    └─> ✅ 完美恢复
    └─> 从50%继续上传

🔍 数据一致性检查

理想情况:两者数据一致

前端 IndexedDB             后端 内存Map/Redis
─────────────────────────────────────────────
uploadedChunks: [0,1,2]    chunks: {0,1,2}
✅ 数据一致
✅ 可以正常继续上传

情况1:前端数据丢失

前端 IndexedDB             后端 内存Map/Redis
─────────────────────────────────────────────
❌ 数据被清除              chunks: {0,1,2}
                          ✅ 数据还在

处理方案:
1. 前端重新初始化上传
2. 后端检测到uploadId已存在
3. 后端可以返回已上传的块列表
4. 前端可以跳过已上传的块

情况2:后端数据丢失(内存Map)

前端 IndexedDB             后端 内存Map
─────────────────────────────────────────────
uploadedChunks: [0,1,2]    ❌ 数据丢失(重启)

处理方案:
1. 前端尝试上传块3
2. 后端返回错误:会话不存在
3. 前端检测到会话丢失
4. 前端重新初始化上传
5. ❌ 无法恢复,需要重新上传

情况3:后端数据丢失(Redis,未持久化)

前端 IndexedDB             后端 Redis
─────────────────────────────────────────────
uploadedChunks: [0,1,2]    ❌ 数据丢失(重启,未持久化)

处理方案:
与内存Map相同,无法恢复

情况4:后端数据保留(Redis,已持久化)

前端 IndexedDB             后端 Redis
─────────────────────────────────────────────
uploadedChunks: [0,1,2]    ✅ 数据保留(重启,已持久化)
                          chunks: {0,1,2}

处理方案:
1. 前端尝试上传块3
2. 后端从Redis读取会话
3. ✅ 找到会话,继续上传
4. ✅ 完美恢复

💡 关键理解

1. 前端IndexedDB的作用

  • 记录上传进度 - 客户端知道哪些块已上传
  • 页面刷新恢复 - 刷新后可以从断点继续
  • 跳过已上传块 - 节省时间和带宽
  • 用户体验 - 显示上传进度

2. 后端uploadSessions的作用

  • 验证上传会话 - 确保上传请求合法
  • 记录已接收块 - 服务器知道哪些块已收到
  • 存储块文件路径 - 知道每个块文件存储位置
  • 权限控制 - 确保用户只能访问自己的上传
  • 合并文件 - 完成上传时知道如何合并

3. 两者的关系

  • 前端记录进度 - 基于网络请求成功
  • 后端记录会话 - 基于实际文件写入
  • 双重保障 - 前端和后端都有记录
  • 断点续传 - 两者配合实现完整功能

4. 内存Map vs Redis的影响

  • 内存Map:

    • 服务器重启 → 数据丢失 → ❌ 无法恢复
    • 前端可以恢复,但后端无法验证
    • 需要重新初始化上传
  • Redis:

    • 服务器重启 → 数据保留 → ✅ 可以恢复
    • 前端和后端都可以恢复
    • 完美的断点续传体验

🎯 总结

断点续传 = 前端IndexedDB + 后端uploadSessions

  1. 前端IndexedDB:

    • 负责客户端恢复(页面刷新、浏览器关闭)
    • 记录上传进度
    • 跳过已上传块
  2. 后端uploadSessions:

    • 负责服务器验证(会话验证、权限控制)
    • 记录已接收块
    • 存储块文件路径
  3. 两者配合:

    • 前端知道进度,后端知道状态
    • 两者配合实现完整的断点续传
    • 前端可以恢复,后端也可以恢复(使用Redis)
  4. 内存Map vs Redis:

    • 内存Map:服务器重启后无法恢复
    • Redis:服务器重启后可以恢复(如果配置持久化)
    • 生产环境推荐使用Redis

📚 相关文档