KnowFlow ①:Redis Bitmap 分片上传 + MinIO + Kafka 异步流水线
项目:KnowFlow 企业级 RAG 知识库系统 | 整理时间:2026-06-07 | 适用于后端实习/校招面试
一、为什么大文件上传不能用普通方式?(面试必问)
1.1 普通上传的灾难
假设你要在网页上传一个 500MB 的 PDF 技术文档,普通方式直接 multipartFile.transferTo() 会有什么问题?
❌ 同步上传的痛点:
1 2 3 4 5 6 7 8 9
| 前端选择文件 ↓ 直接把 500MB 一次性 POST 到后端 ↓ 网络一抖动 → 断了 → 重来 → 又断了 → 又重来 😡 ↓ 后端接收时,内存里直接撑开整个文件 → OOM 风险 ↓ 文件解析、切块、向量化全部同步执行 → 请求卡 30 秒 → 网关超时
|
面试官问的就是:你们项目怎么解决这个问题的?
二、分片上传的核心思想(傻子都能懂)
2.1 什么是分片上传?
把大文件切成小块,像搬砖一样一块一块搬:
1 2 3 4 5 6 7 8 9
| 原文件:500MB 的 technical-manual.pdf
切成 100 块,每块 5MB: chunk-0: [5MB] chunk-1: [5MB] ... chunk-99: [5MB]
每块独立上传,断了只重传那一块,不用全重来。
|
2.2 分片上传的三个核心问题
| 问题 |
说明 |
KnowFlow 的解决方案 |
| 如何判断哪些分片已上传? |
断网重连后,要知道哪些传过了 |
Redis Bitmap(极省内存) |
| 分片传完在哪合并? |
不能放本地磁盘,要分布式存储 |
MinIO 对象存储 |
| 合并后怎么处理文件? |
解析+切块+向量化很慢,不能阻塞上传接口 |
Kafka 异步流水线 |
三、Redis Bitmap 秒传与断点续传(核心亮点)
3.1 什么是 Bitmap?
Bitmap 就是用每一个 bit 位来表示一个状态,极其省内存。
1 2 3 4 5 6 7 8 9 10
| Redis Key: "upload:user123:abc123def456" (abc123 是文件 MD5)
bitmap 的每一位: bit 0 → chunk-0 是否已上传 (1=已上传, 0=未上传) bit 1 → chunk-1 是否已上传 bit 2 → chunk-2 是否已上传 ... bit 99 → chunk-99 是否已上传
100 个分片 = 100 bit = 13 字节!
|
对比其他方案有多省内存:
| 方案 |
存储 100 个分片状态 |
存储 10000 个分片状态 |
| Redis Set(存已上传分片序号) |
~400 字节 |
~40 KB |
| Redis Hash(每个分片一个 field) |
~1 KB |
~100 KB |
| Redis Bitmap |
~13 字节 |
~1.3 KB |
💡 面试回答话术:”我们用 Redis Bitmap 维护分片状态,每个分片只占 1 个 bit,100 个分片只花 13 字节,相比 Set 或 Hash 省了两个数量级的内存,而且 GETBIT/SETBIT 都是 O(1)。”
3.2 源码解析:UploadService.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14
|
String redisKey = "upload:" + userId + ":" + fileMd5;
redisTemplate.opsForValue().setBit(redisKey, chunkIndex, true);
Boolean uploaded = redisTemplate.opsForValue().getBit(redisKey, chunkIndex); if (uploaded) { return; }
|
3.3 断点续传的完整流程
1 2 3 4 5 6 7 8 9 10 11
| [场景:用户上传到 60% 时网络断了]
重新打开页面 → 前端调用 getUploadedChunks() 接口 ↓ 后端从 Redis Bitmap 读取所有分片状态 ↓ 返回 [0,1,2,...,59] 已上传的分片列表 ↓ 前端只上传 [60,61,...,99] 这些没传过的 ↓ 节省 60% 的时间和流量!
|
源码中的优化亮点(getUploadedChunks 方法):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| for (int i = 0; i < totalChunks; i++) { boolean uploaded = redisTemplate.opsForValue().getBit(redisKey, i); }
byte[] bitmapData = redisTemplate.execute((RedisCallback<byte[]>) connection -> connection.get(redisKey.getBytes()));
for (int i = 0; i < totalChunks; i++) { if (isBitSet(bitmapData, i)) { uploadedChunks.add(i); } }
|
💡 面试追问:”Bitmap 的 bit 顺序是怎么样的?”
回答:Redis Bitmap 的 bit 0 是最低位(最右边),但 setBit(key, 0, true) 设置的是第一个 bit。项目中自定义了 isBitSet 方法来正确解析字节数组。
四、MinIO 分片存储与合并(对象存储)
4.1 为什么用 MinIO 而不是存在本地磁盘?
| 对比项 |
本地磁盘 |
MinIO(兼容 S3 协议) |
| 多实例部署 |
❌ 文件只在某一台机器上 |
✅ 所有实例都能访问 |
| 扩展性 |
❌ 磁盘满了要手动迁移 |
✅ 加磁盘,自动分布式存储 |
| 分片合并 |
❌ 要自己写文件流合并 |
✅ 原生支持 composeObject(服务端合并) |
| 预签名 URL |
❌ 要自己实现鉴权下载 |
✅ 一行代码生成带过期的下载链接 |
4.2 分片存储路径设计
1 2 3 4 5 6 7 8 9 10 11 12 13
| MinIO Bucket: uploads
分片存储路径: chunks/{fileMd5}/{chunkIndex} 例如: chunks/abc123def456/0 ← 第 0 个分片 chunks/abc123def456/1 ← 第 1 个分片 ...
合并后路径: merged/{fileName} 例如: merged/technical-manual.pdf
|
4.3 MinIO 服务端合并(不需要下载到本地!)
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| List<ComposeSource> sources = partPaths.stream() .map(path -> ComposeSource.builder() .bucket("uploads") .object(path) .build()) .collect(toList());
minioClient.composeObject(ComposeObjectArgs.builder() .bucket("uploads") .object(mergedPath) .sources(sources) .build());
|
💡 面试亮点:composeObject 是 MinIO 服务端操作,不把文件内容下载到应用内存,对大文件非常友好。合并 500MB 文件,应用内存几乎不增长。
五、Kafka 异步处理流水线(解耦的核心)
5.1 为什么合并完不能立即解析?
1 2 3 4 5 6 7 8
| 文件合并完成 ↓ 要做的三件大事: 1. 解析文件内容(PDF → 提取文字,可能要 5 秒) 2. 把文字切分成小块(chunk,每块 500 字,可能要 3 秒) 3. 调用大模型把每块变成向量(Embedding API 调用,可能要 10 秒) ↓ 如果同步做:用户点击上传 → 等 18 秒 → 前端超时 ❌
|
解决思路:合并完成后,往 Kafka 发一条消息,告诉后台”这个文件可以处理了”,然后立即返回给用户”上传成功”。
1 2 3 4 5
| 上传接口 → 合并分片 → 往 Kafka 发消息 → 立即返回"上传成功" ↓ Kafka 消费者异步处理 (解析 → 切块 → 向量化) 用户可以在前台看到进度
|
5.2 源码解析:FileProcessingConsumer.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
| @KafkaListener(topics = "#{kafkaConfig.getFileProcessingTopic()}") public void processTask(FileProcessingTask task) { InputStream fileStream = downloadFileFromStorage(task.getFilePath()); parseService.parseAndSave( task.getFileMd5(), fileStream, task.getUserId(), task.getOrgTag(), task.isPublic() ); vectorizationService.vectorize( task.getFileMd5(), task.getUserId(), task.getOrgTag(), task.isPublic() ); kafkaProcessedCounter.increment(); }
|
5.3 Kafka 出错怎么办?(面试必问)
Kafka 的错误处理链路:
1 2 3 4 5 6 7 8 9
| 消息处理抛出异常 ↓ DefaultErrorHandler 捕获 ↓ 重试(默认 3 次,可配置) ↓ 重试都失败了 → 扔进死信队列(Dead Letter Queue) ↓ 专门有个死信消费者,把失败记录写进数据库,告警通知运维
|
💡 面试回答话术:”Kafka 消费失败我们有三层保障:第一层是自动重试,网络抖动能自动恢复;第二层是死信队列,重试耗尽后消息不丢失;第三层是监控告警,死信堆积会触发 Prometheus 告警。”
六、完整流程图(背下来!)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41
| ┌──────────┐ │ 前端上传分片(每块 5MB) │ └──────────────────┬─────────────────────────┘ │ ▼ ┌──────────┐ │ UploadController 接收分片 │ │ - 计算分片 MD5 │ │ - 检查 Redis Bitmap 是否已上传(秒传) │ └──────────────────┬─────────────────────────┘ │ 未上传 ▼ ┌──────────┐ │ UploadService.uploadChunk() │ │ - 上传分片到 MinIO(chunks/{md5}/{i})│ │ - 标记 Redis Bitmap 对应 bit 为 1 │ │ - 写数据库 chunk_info 表 │ └──────────────────┬─────────────────────────┘ │ ▼ ┌──────────┐ │ 前端通知:所有分片上传完毕 │ └──────────────────┬─────────────────────────┘ │ ▼ ┌──────────┐ │ UploadService.mergeChunks() │ │ - MinIO composeObject 服务端合并 │ │ - 更新 file_upload 表状态为"已完成" │ │ - 清除 Redis Bitmap │ │ - 发送 Kafka 消息(触发异步处理) │ └──────────────────┬─────────────────────────┘ │ ▼ ┌──────────┐ │ Kafka Consumer 异步处理 │ │ - 解析文件内容(PDF/Word/Excel...) │ │ - 文字切块(每块 500 字,有重叠) │ │ - 调用 Embedding API 生成向量 │ │ - 写入 Elasticsearch │ └──────────┘
|
七、面试八股文高频题
Q1:Redis Bitmap 的底层原理是什么?
答:Redis 的 Bitmap 本质就是 String 类型,底层是字节数组。SETBIT key offset 1 会把对应 offset 的 bit 设为 1,Redis 会自动扩展字节数组。访问是 O(1) 的,非常快。8 个 bit 存成一个 byte,所以 100 个分片状态只占用 13 个字节(100/8 向上取整)。
Q2:如果 Redis 挂了,分片状态丢失怎么办?
答:分片状态丢失不影响已上传到 MinIO 的分片数据(分片存在 MinIO,不依赖 Redis)。丢失的只是”哪些分片已上传”这个记录。用户重新上传时,系统会重新检查 MinIO 中是否存在对应分片文件,如果存在就补录 Bitmap 状态。项目里 uploadChunk 方法中有这个逻辑:chunkUploaded=true 但数据库没记录时,会检查 MinIO 文件是否存在,存在就跳过上传。
Q3:MinIO 和阿里云 OSS/AWS S3 的关系?
答:MinIO 是兼容 S3 协议的开源对象存储,可以私有化部署。API 和 S3 完全兼容,所以代码里用 MinIO SDK 写的,将来要迁移到阿里云 OSS 也几乎不用改代码(OSS 也支持 S3 协议)。这是云原生架构的常用做法——存储和计算分离,应用无状态化。
Q4:Kafka 怎么保证消息不丢失?
答:三层保障:
- 生产者:开启
acks=all,保证消息写入所有副本才返回成功;
- Broker:开启
min.insync.replicas=2,保证至少 2 个副本确认;
- 消费者:关闭自动提交 offset(
enable.auto.commit=false),等处理完再手动提交。如果处理失败抛出异常,Kafka 会重试,重试耗尽后进死信队列,不会丢消息。
Q5:为什么不用 RabbitMQ 而用 Kafka?
答:Kafka 更适合这种”日志型”的异步处理流水线,吞吐量远高于 RabbitMQ(Kafka 百万级 QPS,RabbitMQ 万级)。而且 Kafka 的 partition 机制可以做到同一个文件的处理任务只被一个消费者处理(用 fileMd5 做 partition key),天然避免了重复处理。
八、简历上怎么写这句话(可直接用)
原句参考:”针对大文件长连接上传易中断、解析耗时阻塞主线程等痛点,引入 Redis Bitmap 维护分片状态,以极低内存开销实现秒传与断点续传;基于 MinIO + Kafka 构建异步文档处理流水线,将文本解析、切块与大模型向量化彻底解耦,保障核心服务不被计算密集型任务拖垮。”
面试时被问到这句话,就按本文的脉络讲:
- 先说痛点(大文件上传易失败)→
- 说方案(分片 + Redis Bitmap + MinIO + Kafka)→
- 说细节(Bitmap 多省内存、MinIO 服务端合并、Kafka 异步解耦)→
- 说效果(上传接口 200ms 内返回,解析在后台跑,互不影响)
© 2026 KnowFlow 面试手册 · 转载请注明出处