文件分块

在这个架构图中,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()
  • 根据 uploadIDchunkIndex 构造存储分块的文件路径,并创建相应的目录和文件用于存储分块内容。
  • 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")
  • 解析客户端发来的参数,包括 uploadidfilehashfilesize 等,用于后续的文件合并和验证。

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))
  • 更新数据库,标记该文件的上传已完成。OnFileUploadFinishedOnUserFileUploadFinished 分别用于记录文件的元数据和用户上传记录。

3.7 返回响应

w.Write(util.NewRespMsg(0, "OK", nil).JSONBytes())
  • 返回成功的 JSON 响应,通知客户端整个文件上传流程已完成。

断点续传

文件断点续传的工作流程

  1. 初始化上传:客户端请求服务器,告知准备上传文件。服务器根据文件大小和分块策略,创建一个上传会话,并返回一个 UploadID 用于标识该次上传。

  2. 分块上传:文件被分为多个小块(Chunk),每个分块会独立上传到服务器。在每个分块上传成功后,客户端与服务器都会记录已完成的分块信息。

  3. 上传中断与重试:如果上传过程中发生中断,客户端可以通过 UploadID 和已上传的分块索引,向服务器查询已成功上传的分块,并从上次中断的地方继续上传剩余的部分。

  4. 上传完成与合并:当所有分块上传完成后,客户端通知服务器进行分块合并,最终生成完整的文件。

实现文件断点续传的关键技术点

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. 文件验证

在完成上传并合并文件后,服务器可以对合并后的文件进行验证,确保文件完整性。常见的验证方式包括:

  • 文件哈希验证:客户端和服务器都计算文件的哈希值,确保两者一致。
  • 文件大小验证:对比合并后的文件大小与客户端提供的文件大小,确保上传过程中没有数据丢失。
posted @ 2024-09-07 22:36  daligh  阅读(60)  评论(0)    收藏  举报