文件分块

在这个架构图中,Redis 被用于存储文件的元信息,其使用原因和优势如下:
1. 高效的缓存机制:
Redis 是一个内存数据库,拥有极快的读写速度。在文件上传过程中,尤其是当文件分块上传时,频繁的读写文件元信息(例如,文件的 Hash 值、上传进度、文件分块信息等)可能会拖慢系统性能。将这些元信息存储在 Redis 中,能够大幅减少数据库访问的延迟,提高系统的整体性能。
2. 临时性数据存储:
文件元信息通常是短期内需要频繁访问但不需要长期保存的。Redis 支持 TTL(过期时间)机制,可以让这些元信息在不再需要时自动过期和清除。因此,Redis 非常适合存储这些临时的文件状态和元信息数据。
3. 快速查询与更新:
分块上传过程中,需要不断查询和更新文件的元信息(如文件的各个分块是否上传完成、合并进度等)。Redis 的数据结构设计(如哈希表、列表等)非常适合这类频繁的查询和更新操作,能够在极短的时间内完成这些操作,提升用户的上传体验。
4. 降低数据库负载:
如果把这些文件元信息存储在传统关系型数据库中,频繁的查询和更新操作会给数据库带来很大的压力,影响整个系统的性能。而 Redis 作为内存数据库,可以承担大部分这类高频操作,显著减轻数据库的负载,提升系统的可扩展性。
5. 数据一致性要求较低:
文件上传的元信息虽然频繁变动,但大多数情况下对数据的一致性要求并不严格。例如,某些上传进度信息即便在极端情况下丢失也不会造成严重问题。Redis 采用的最终一致性模型能够很好地满足这些需求,且性能远远优于严格一致性的数据库。
6. 微服务支持:
在微服务架构下,Redis 能够作为各个微服务之间的共享存储,方便不同的服务快速访问和更新文件元信息。在多个服务协同工作时,Redis 提供了一个轻量化、快速的共享数据存储方案。
分块上传
type MultipartUploadInfo struct {
	FileHash   string
	FileSize   int
	UploadID   string
	ChunkSize  int
	ChunkCount int
}
这段代码实现了一个完整的分块上传功能,包括初始化分块上传、上传每个文件分块以及通知合并分块的操作。下面是各个步骤的详细解析:
1. 分块上传初始化 (InitialMultipartUploadHandler)
1.1 解析用户请求参数
r.ParseForm()
username := r.Form.Get("username")
filehash := r.Form.Get("filehash")
filesize, err := strconv.Atoi(r.Form.Get("filesize"))
- 解析用户提交的参数:用户名、文件哈希、文件大小。
r.ParseForm()将 HTTP 请求中的参数解析到Form字段中。 
1.2 获得 Redis 连接
rConn := rPool.RedisPool().Get()
defer rConn.Close()
- 从 Redis 连接池中获取一个 Redis 连接,用于后续操作。
defer确保连接在函数结束时被关闭。 
1.3 生成分块上传的初始化信息
upInfo := MultipartUploadInfo{
    FileHash:   filehash,
    FileSize:   filesize,
    UploadID:   username + fmt.Sprintf("%x", time.Now().UnixNano()),
    ChunkSize:  5 * 1024 * 1024, // 5MB
    ChunkCount: int(math.Ceil(float64(filesize) / (5 * 1024 * 1024))),
}
MultipartUploadInfo是用于保存分块上传信息的结构体。ChunkSize设置为 5MB,即每个分块的大小。ChunkCount计算文件总共需要分为多少块,math.Ceil用于向上取整。
1.4 将分块上传初始化信息写入 Redis
rConn.Do("HSET", "MP_"+upInfo.UploadID, "chunkcount", upInfo.ChunkCount)
rConn.Do("HSET", "MP_"+upInfo.UploadID, "filehash", upInfo.FileHash)
rConn.Do("HSET", "MP_"+upInfo.UploadID, "filesize", upInfo.FileSize)
- 将生成的上传信息(分块数量、文件哈希值、文件大小等)存储在 Redis 中,使用 Hash 数据结构保存,键名格式为 
"MP_ + UploadID"。 
1.5 返回初始化数据到客户端
w.Write(util.NewRespMsg(0, "OK", upInfo).JSONBytes())
- 生成并返回包含上传初始化信息的 JSON 响应给客户端。客户端将使用 
UploadID来执行后续的分块上传。 
2. 文件分块上传 (UploadPartHandler)
2.1 解析请求参数
r.ParseForm()
uploadID := r.Form.Get("uploadid")
chunkIndex := r.Form.Get("index")
- 解析分块上传的请求参数,主要包括上传 ID (
uploadID) 和当前分块的序号 (index)。 
2.2 获得 Redis 连接
rConn := rPool.RedisPool().Get()
defer rConn.Close()
- 从 Redis 连接池中获取一个连接,用于后续记录当前分块上传的状态。
 
2.3 获得文件句柄并存储分块内容
fpath := "/data/" + uploadID + "/" + chunkIndex
os.MkdirAll(path.Dir(fpath), 0744)
fd, err := os.Create(fpath)
defer fd.Close()
- 根据 
uploadID和chunkIndex构造存储分块的文件路径,并创建相应的目录和文件用于存储分块内容。 os.Create用于创建文件,MkdirAll确保路径中的目录存在。
2.4 读取上传的分块数据并写入文件
buf := make([]byte, 1024*1024)
for {
    n, err := r.Body.Read(buf)
    fd.Write(buf[:n])
    if err != nil {
        break
    }
}
- 从 HTTP 请求的 
Body中读取上传的分块数据,并逐步写入到刚才创建的文件中。每次读取 1MB 数据直到读取完成。 
2.5 更新 Redis 缓存状态
rConn.Do("HSET", "MP_"+uploadID, "chkidx_"+chunkIndex, 1)
- 在 Redis 中更新当前分块的状态,表示该分块已上传完成。键的格式为 
"chkidx_ + 分块序号",值为1表示完成。 
2.6 返回处理结果到客户端
w.Write(util.NewRespMsg(0, "OK", nil).JSONBytes())
- 返回成功的 JSON 响应,通知客户端该分块上传已完成。
 
3. 分块合并与上传完成 (CompleteUploadHandler)
3.1 解析请求参数
r.ParseForm()
upid := r.Form.Get("uploadid")
username := r.Form.Get("username")
filehash := r.Form.Get("filehash")
filesize := r.Form.Get("filesize")
filename := r.Form.Get("filename")
- 解析客户端发来的参数,包括 
uploadid、filehash、filesize等,用于后续的文件合并和验证。 
3.2 获取 Redis 连接
rConn := rPool.RedisPool().Get()
defer rConn.Close()
- 从 Redis 连接池获取一个连接,用于检查分块上传的状态。
 
3.3 查询分块上传状态
data, err := redis.Values(rConn.Do("HGETALL", "MP_"+upid))
totalCount := 0
chunkCount := 0
for i := 0; i < len(data); i += 2 {
    k := string(data[i].([]byte))
    v := string(data[i+1].([]byte))
    if k == "chunkcount" {
        totalCount, _ = strconv.Atoi(v)
    } else if strings.HasPrefix(k, "chkidx_") && v == "1" {
        chunkCount++
    }
}
- 通过 Redis 查询上传的分块信息,判断所有分块是否已经上传完毕。
HGETALL获取所有键值对,逐个检查是否所有分块(chkidx_x)的状态都为1,即上传完成。 
3.4 检查是否所有分块完成
if totalCount != chunkCount {
    w.Write(util.NewRespMsg(-2, "invalid request", nil).JSONBytes())
    return
}
- 如果 Redis 记录的总分块数与已上传完成的分块数不一致,返回错误响应,表示请求无效。
 
3.5 合并分块
// 4. TODO:合并分块
- 这一步是文件上传过程中最后的操作,负责将所有分块合并成一个完整的文件。当前尚未实现。
 
3.6 更新数据库并记录上传完成
fsize, _ := strconv.Atoi(filesize)
dblayer.OnFileUploadFinished(filehash, filename, int64(fsize), "")
dblayer.OnUserFileUploadFinished(username, filehash, filename, int64(fsize))
- 更新数据库,标记该文件的上传已完成。
OnFileUploadFinished和OnUserFileUploadFinished分别用于记录文件的元数据和用户上传记录。 
3.7 返回响应
w.Write(util.NewRespMsg(0, "OK", nil).JSONBytes())
- 返回成功的 JSON 响应,通知客户端整个文件上传流程已完成。
 
断点续传
文件断点续传的工作流程
- 
初始化上传:客户端请求服务器,告知准备上传文件。服务器根据文件大小和分块策略,创建一个上传会话,并返回一个
UploadID用于标识该次上传。 - 
分块上传:文件被分为多个小块(Chunk),每个分块会独立上传到服务器。在每个分块上传成功后,客户端与服务器都会记录已完成的分块信息。
 - 
上传中断与重试:如果上传过程中发生中断,客户端可以通过
UploadID和已上传的分块索引,向服务器查询已成功上传的分块,并从上次中断的地方继续上传剩余的部分。 - 
上传完成与合并:当所有分块上传完成后,客户端通知服务器进行分块合并,最终生成完整的文件。
 
实现文件断点续传的关键技术点
1. 文件分块
文件断点续传的基础是将文件分为多个小块(Chunk)。分块的大小可以根据具体情况设定,通常每块的大小为 5MB 或 10MB。这样做的好处是:
- 如果上传中断,只需要重新上传未完成的分块。
 - 每次只需处理较小的文件块,减少了内存占用。
 
upInfo := MultipartUploadInfo{
    FileHash:   filehash,
    FileSize:   filesize,
    UploadID:   username + fmt.Sprintf("%x", time.Now().UnixNano()),
    ChunkSize:  5 * 1024 * 1024, // 5MB
    ChunkCount: int(math.Ceil(float64(filesize) / (5 * 1024 * 1024))),
}
在初始化阶段,服务端生成 UploadID 和分块信息,并将其返回给客户端,方便客户端根据这些信息上传各个分块。
2. 文件分块上传
每个分块单独上传,客户端会将分块编号以及 UploadID 一同提交给服务器,服务器保存这些信息并记录每个分块的状态。当某个分块上传失败时,只需要重传失败的分块,已完成的分块不受影响。
3. 断点续传状态管理
为了实现断点续传,服务器需要保存上传过程中的状态信息,包括:
UploadID:唯一标识某次文件上传会话。- 已上传的分块信息:记录每个分块是否上传成功。
 
服务器可以通过多种方式存储这些状态信息,常见的选择包括 Redis 和 数据库。
Redis 存储状态
Redis 是一种高效的缓存系统,适合存储短期的上传状态信息。通过 HSET(Hash Set)操作,服务器可以记录每个分块的上传状态,便于快速查找和更新。
rConn.Do("HSET", "MP_"+uploadID, "chkidx_"+chunkIndex, 1)
每次成功上传一个分块,服务器就将该分块的状态更新为已上传,客户端可以通过查询 Redis 来确认哪些分块已上传。
4. 上传中断后的恢复
上传过程中如果网络中断或浏览器关闭,客户端可以通过已保存的 UploadID 重新发起上传请求。服务器会返回已上传成功的分块列表,客户端只需上传未完成的分块。
data, err := redis.Values(rConn.Do("HGETALL", "MP_"+upid))
for i := 0; i < len(data); i += 2 {
    k := string(data[i].([]byte))
    v := string(data[i+1].([]byte))
    if strings.HasPrefix(k, "chkidx_") && v == "1" {
        chunkCount++
    }
}
通过这种方式,客户端可以避免重新上传整个文件,节省上传时间。
5. 分块合并
当所有分块上传完成后,客户端通知服务器进行文件合并。服务器会根据文件的分块顺序将分块合并成一个完整的文件,并删除上传过程中生成的临时分块。
// 4. TODO:合并分块
合并完成后,服务器可以将文件元信息存储到数据库,标记该文件的上传已完成。
6. 文件验证
在完成上传并合并文件后,服务器可以对合并后的文件进行验证,确保文件完整性。常见的验证方式包括:
- 文件哈希验证:客户端和服务器都计算文件的哈希值,确保两者一致。
 - 文件大小验证:对比合并后的文件大小与客户端提供的文件大小,确保上传过程中没有数据丢失。
 
                    
                
                
            
        
浙公网安备 33010602011771号