[{"data":1,"prerenderedAt":2225},["ShallowReactive",2],{"post-\u002Fposts\u002Fcloudvault-go-cloud-storage-system":3,"all-posts-nav":1950},{"id":4,"title":5,"body":6,"categories":1930,"date":1932,"description":12,"draft":1933,"extension":1934,"hidden":1933,"meta":1935,"navigation":1936,"path":1937,"published":1933,"seo":1938,"stem":1939,"tags":1940,"__hash__":1949},"posts\u002Fposts\u002Fcloudvault-go-cloud-storage-system.md","CloudVault 项目详解：基于 Go 的云端存储与网盘系统架构设计",{"type":7,"value":8,"toc":1825},"minimark",[9,13,22,25,29,32,38,41,46,49,52,91,94,97,100,110,113,118,121,124,139,143,146,150,153,167,171,174,191,195,198,204,207,213,216,219,222,225,228,234,237,240,246,257,269,272,275,278,284,287,293,296,299,302,308,311,314,320,323,326,329,332,335,341,344,347,350,356,359,362,365,368,374,377,383,386,400,403,406,414,417,423,426,544,551,554,557,560,563,569,572,586,589,595,598,601,607,610,613,630,633,639,642,645,648,651,654,660,663,667,670,676,679,693,696,699,719,722,726,729,732,735,749,752,755,761,764,768,771,774,777,794,797,800,806,809,815,818,821,838,841,847,851,854,868,871,875,878,881,884,890,893,896,899,905,908,911,914,920,923,926,932,935,938,941,944,964,967,973,976,980,983,986,989,1123,1126,1129,1132,1138,1141,1145,1148,1151,1157,1160,1163,1166,1169,1186,1189,1192,1195,1201,1204,1210,1213,1216,1219,1222,1245,1248,1254,1257,1260,1263,1266,1269,1289,1292,1298,1301,1304,1307,1324,1328,1331,1334,1340,1343,1347,1350,1353,1357,1360,1366,1369,1375,1378,1382,1385,1391,1394,1400,1403,1407,1410,1427,1430,1433,1436,1439,1453,1456,1470,1473,1490,1493,1507,1510,1521,1524,1527,1530,1536,1539,1545,1548,1554,1557,1563,1566,1572,1576,1582,1585,1588,1591,1594,1597,1614,1617,1620,1622,1633,1636,1639,1641,1661,1664,1667,1669,1686,1690,1693,1695,1712,1715,1718,1723,1727,1730,1735,1738,1742,1745,1749,1752,1756,1759,1763,1770,1774,1777,1781,1784,1788,1791,1795,1798,1802,1805,1809,1812,1815,1818,1821],[10,11,12],"p",{},"CloudVault 是一个基于 Go 的云端存储与网盘系统，面向大文件传输、高并发访问和文件管理场景设计。它的核心目标不是简单做一个“文件上传下载 Demo”，而是围绕真实网盘系统中的关键问题展开：大文件如何稳定上传、分片如何管理、断点续传如何恢复、离线下载任务如何异步执行、热门文件如何缓存、文件如何搜索、下载链接如何安全分享，以及后续如何接入 AI Agent 做自然语言文件查询。",[10,14,15,16],{},"项目地址：",[17,18,19],"a",{"href":19,"rel":20},"https:\u002F\u002Fgithub.com\u002FbestVictor2\u002FCloudVault",[21],"nofollow",[10,23,24],{},"这篇文章会从系统定位、核心业务、存储架构、上传下载链路、异步任务、缓存设计、搜索系统、AI 扩展和面试表达几个角度，完整拆解 CloudVault 这个项目。",[26,27,28],"h2",{"id":28},"项目一句话介绍",[10,30,31],{},"CloudVault 可以概括为：",[33,34,35],"blockquote",{},[10,36,37],{},"一个基于 Go 的云端存储与网盘系统，使用 MinIO 承载对象文件，MySQL 管理业务元数据，Redis 处理缓存、锁和高并发状态，RabbitMQ 承载异步任务，Elasticsearch 提供文件检索能力，并预留 AI Agent \u002F RAG 能力用于自然语言文件查询。",[10,39,40],{},"如果放到简历或者面试里，可以这样讲：",[33,42,43],{},[10,44,45],{},"我做了一个面向大文件传输和高并发场景的云端网盘系统，支持文件上传、下载、分享、回收站、离线下载、搜索和预览等功能。系统采用对象存储与业务元数据解耦架构，文件本体放在 MinIO，用户、目录、文件索引、分片状态等元数据放在 MySQL；通过 Redis 做热点缓存、分布式锁和上传状态管理；通过 RabbitMQ 将离线下载、文件处理等耗时任务异步化；通过 Elasticsearch 建立文件搜索索引；后续还设计了 AI Agent 与 RAG 文档问答扩展。",[26,47,48],{"id":48},"它解决的核心问题",[10,50,51],{},"一个网盘系统看起来只是“上传文件、下载文件”，但真正做起来会遇到很多工程问题：",[53,54,55,59,62,65,68,71,74,77,85,88],"ol",{},[56,57,58],"li",{},"大文件不能一次性读入内存，否则会造成内存爆炸。",[56,60,61],{},"网络不稳定时，上传中断后不能从头重传，否则体验很差。",[56,63,64],{},"多用户并发上传同一个文件时，需要避免重复存储。",[56,66,67],{},"文件元数据和文件二进制内容需要解耦，否则数据库压力巨大。",[56,69,70],{},"下载大文件时需要支持流式传输，避免服务端内存占用过高。",[56,72,73],{},"分享链接需要有权限控制、过期时间和防盗链机制。",[56,75,76],{},"离线下载这类长任务不能阻塞 HTTP 请求线程。",[56,78,79,80,84],{},"搜索不能只靠 MySQL ",[81,82,83],"code",{},"LIKE","，否则性能和效果都不够。",[56,86,87],{},"热门文件和用户信息需要缓存，否则数据库容易成为瓶颈。",[56,89,90],{},"后续如果要接入 AI，需要把文件内容变成可检索、可问答的知识。",[10,92,93],{},"CloudVault 的设计本质上是在回答这些问题。",[26,95,96],{"id":96},"总体架构",[10,98,99],{},"从截图描述看，CloudVault 的核心组件包括：",[101,102,108],"pre",{"className":103,"code":105,"language":106,"meta":107},[104],"language-text","用户 \u002F 浏览器 \u002F 客户端\n        |\n        v\nGo 后端 API 服务\n        |\n        |-- MySQL：用户、目录、文件、分享、回收站、分片状态等元数据\n        |-- MinIO：文件对象、分片对象、合并后的文件对象\n        |-- Redis：用户缓存、热点文件缓存、分布式锁、上传进度\n        |-- RabbitMQ：离线下载、文件处理、失败重试、DLQ\n        |-- Elasticsearch：文件名、路径、内容摘要、标签等搜索索引\n        |-- AI Agent \u002F RAG：自然语言查询、文件语义检索、下载链接生成\n","text","",[81,109,105],{"__ignoreMap":107},[10,111,112],{},"这个架构有一个很重要的思想：",[33,114,115],{},[10,116,117],{},"文件本体和业务元数据分离。",[10,119,120],{},"也就是说，真正的大文件不放 MySQL，而是放 MinIO 这类对象存储；MySQL 只记录文件的业务信息，例如文件名、大小、Hash、对象存储 key、所属用户、目录关系、分享状态、删除状态等。",[10,122,123],{},"这样做的好处是：",[125,126,127,130,133,136],"ul",{},[56,128,129],{},"MySQL 不需要承载大对象，读写压力更可控。",[56,131,132],{},"MinIO 更适合存储大文件和对象数据。",[56,134,135],{},"文件迁移、扩容、备份和生命周期管理更灵活。",[56,137,138],{},"后续可以把 MinIO 替换成 S3、OSS、COS 等云厂商对象存储。",[26,140,142],{"id":141},"为什么用-minio-mysql-解耦","为什么用 MinIO + MySQL 解耦",[10,144,145],{},"很多初学者做文件系统会把文件直接存到本地磁盘，或者把文件内容塞到数据库 BLOB 字段里。这两种方式在小 Demo 中可以工作，但在真实场景中问题很大。",[147,148,149],"h3",{"id":149},"本地磁盘的问题",[10,151,152],{},"如果文件只放在单机本地磁盘：",[125,154,155,158,161,164],{},[56,156,157],{},"服务扩容后，不同机器看到的文件不一致。",[56,159,160],{},"机器宕机后，文件可能不可用。",[56,162,163],{},"文件迁移和备份麻烦。",[56,165,166],{},"很难做统一的权限、生命周期和对象管理。",[147,168,170],{"id":169},"mysql-blob-的问题","MySQL BLOB 的问题",[10,172,173],{},"如果文件直接存 MySQL：",[125,175,176,179,182,185,188],{},[56,177,178],{},"大文件会让数据库体积迅速膨胀。",[56,180,181],{},"备份恢复非常慢。",[56,183,184],{},"数据库 buffer pool 被大对象污染。",[56,186,187],{},"文件下载会占用数据库连接和网络资源。",[56,189,190],{},"高并发下载时数据库压力极大。",[147,192,194],{"id":193},"minio-的作用","MinIO 的作用",[10,196,197],{},"MinIO 是兼容 S3 API 的对象存储服务，适合存储文件对象。CloudVault 可以把每个文件或分片当作对象写入 MinIO，例如：",[101,199,202],{"className":200,"code":201,"language":106,"meta":107},[104],"bucket: cloudvault\nobject key: users\u002F{user_id}\u002Ffiles\u002F{file_hash}\nobject key: chunks\u002F{upload_id}\u002F{chunk_index}\n",[81,203,201],{"__ignoreMap":107},[10,205,206],{},"MySQL 中只保存引用：",[101,208,211],{"className":209,"code":210,"language":106,"meta":107},[104],"file_id\nuser_id\nfilename\nfile_hash\nsize\nmime_type\nobject_key\nstorage_bucket\nstatus\ncreated_at\nupdated_at\n",[81,212,210],{"__ignoreMap":107},[10,214,215],{},"因此，MinIO 负责“存文件”，MySQL 负责“管文件”。",[26,217,218],{"id":218},"数据模型设计",[10,220,221],{},"一个网盘系统至少需要以下核心表。",[147,223,224],{"id":224},"用户表",[10,226,227],{},"负责登录、权限和用户空间统计：",[101,229,232],{"className":230,"code":231,"language":106,"meta":107},[104],"users\n- id\n- username\n- email\n- password_hash\n- avatar\n- used_quota\n- total_quota\n- created_at\n- updated_at\n",[81,233,231],{"__ignoreMap":107},[147,235,236],{"id":236},"文件表",[10,238,239],{},"记录逻辑文件信息：",[101,241,244],{"className":242,"code":243,"language":106,"meta":107},[104],"files\n- id\n- user_id\n- parent_id\n- filename\n- file_hash\n- size\n- mime_type\n- object_key\n- ref_count\n- status\n- created_at\n- updated_at\n",[81,245,243],{"__ignoreMap":107},[10,247,248,249,252,253,256],{},"其中 ",[81,250,251],{},"file_hash"," 和 ",[81,254,255],{},"ref_count"," 很关键。",[125,258,259,264],{},[56,260,261,263],{},[81,262,251],{}," 用于判断文件内容是否相同。",[56,265,266,268],{},[81,267,255],{}," 用于实现秒传和去重存储。",[10,270,271],{},"如果两个用户上传同一个文件，系统可以只保存一份对象内容，然后通过多条用户文件记录引用同一个对象。",[147,273,274],{"id":274},"分片上传表",[10,276,277],{},"用于断点续传和分片状态管理：",[101,279,282],{"className":280,"code":281,"language":106,"meta":107},[104],"upload_sessions\n- id\n- user_id\n- filename\n- file_hash\n- total_size\n- chunk_size\n- total_chunks\n- uploaded_chunks\n- status\n- created_at\n- updated_at\n",[81,283,281],{"__ignoreMap":107},[10,285,286],{},"分片明细表：",[101,288,291],{"className":289,"code":290,"language":106,"meta":107},[104],"upload_chunks\n- id\n- upload_id\n- chunk_index\n- chunk_hash\n- size\n- object_key\n- status\n- created_at\n",[81,292,290],{"__ignoreMap":107},[10,294,295],{},"通过这两张表，可以知道某个大文件已经上传了哪些分片，断线后只需要补传缺失分片。",[147,297,298],{"id":298},"分享表",[10,300,301],{},"负责分享链接、访问码、过期时间和权限：",[101,303,306],{"className":304,"code":305,"language":106,"meta":107},[104],"shares\n- id\n- user_id\n- file_id\n- share_code\n- access_token\n- expire_at\n- allow_download\n- visit_count\n- created_at\n",[81,307,305],{"__ignoreMap":107},[147,309,310],{"id":310},"回收站表",[10,312,313],{},"文件删除时可以先软删除：",[101,315,318],{"className":316,"code":317,"language":106,"meta":107},[104],"trash_records\n- id\n- user_id\n- file_id\n- deleted_at\n- expire_at\n",[81,319,317],{"__ignoreMap":107},[10,321,322],{},"这样用户可以恢复文件，也方便后台定时清理真正过期的对象。",[26,324,325],{"id":325},"文件上传链路",[10,327,328],{},"CloudVault 的上传链路可以拆成普通上传、秒传、分片上传、断点续传、分片合并几个部分。",[147,330,331],{"id":331},"普通上传",[10,333,334],{},"小文件可以直接上传：",[101,336,339],{"className":337,"code":338,"language":106,"meta":107},[104],"客户端选择文件\n  -> 后端接收 multipart file\n  -> 计算文件 Hash\n  -> 上传到 MinIO\n  -> 写入 MySQL 文件元数据\n  -> 返回文件 ID\n",[81,340,338],{"__ignoreMap":107},[10,342,343],{},"这里需要注意：后端不能把整个文件一次性读入内存，应该使用流式读取，边读边计算 Hash 或写入对象存储。",[147,345,346],{"id":346},"秒传机制",[10,348,349],{},"秒传的核心是文件 Hash。",[101,351,354],{"className":352,"code":353,"language":106,"meta":107},[104],"客户端先计算文件 Hash\n  -> 请求后端检查 Hash 是否存在\n  -> 如果存在，直接创建文件引用\n  -> 如果不存在，走正常上传\n",[81,355,353],{"__ignoreMap":107},[10,357,358],{},"这样可以避免重复上传同一个文件。",[10,360,361],{},"例如，一个热门安装包已经被用户 A 上传过，用户 B 再上传时，只要 Hash 一致，系统就可以直接复用对象存储中的文件。",[147,363,364],{"id":364},"分片上传",[10,366,367],{},"大文件通常会拆成多个 chunk：",[101,369,372],{"className":370,"code":371,"language":106,"meta":107},[104],"file = chunk_0 + chunk_1 + chunk_2 + ... + chunk_n\n",[81,373,371],{"__ignoreMap":107},[10,375,376],{},"上传流程：",[101,378,381],{"className":379,"code":380,"language":106,"meta":107},[104],"1. 客户端计算文件 Hash 和分片信息\n2. 创建上传会话 upload_session\n3. 客户端并发上传多个 chunk\n4. 后端保存 chunk 到 MinIO\n5. 后端记录 chunk 状态到 MySQL \u002F Redis\n6. 所有 chunk 完成后触发合并\n7. 合并成功后生成最终文件对象\n8. 删除临时 chunk 或设置生命周期清理\n",[81,382,380],{"__ignoreMap":107},[10,384,385],{},"这种方式的优势是：",[125,387,388,391,394,397],{},[56,389,390],{},"可以并发上传，提高带宽利用率。",[56,392,393],{},"某个分片失败，只需要重传这个分片。",[56,395,396],{},"上传中断后，可以查询已上传分片列表继续上传。",[56,398,399],{},"服务端可以更细粒度地控制上传进度。",[147,401,402],{"id":402},"断点续传",[10,404,405],{},"断点续传依赖两个能力：",[53,407,408,411],{},[56,409,410],{},"服务端记录分片状态。",[56,412,413],{},"客户端能查询缺失分片。",[10,415,416],{},"典型接口设计如下：",[101,418,421],{"className":419,"code":420,"language":106,"meta":107},[104],"POST \u002Fapi\u002Fuploads\u002Finit\nGET  \u002Fapi\u002Fuploads\u002F{upload_id}\u002Fstatus\nPUT  \u002Fapi\u002Fuploads\u002F{upload_id}\u002Fchunks\u002F{chunk_index}\nPOST \u002Fapi\u002Fuploads\u002F{upload_id}\u002Fcomplete\n",[81,422,420],{"__ignoreMap":107},[10,424,425],{},"当上传中断后，客户端请求 status：",[101,427,431],{"className":428,"code":429,"language":430,"meta":107,"style":107},"language-json shiki shiki-themes github-dark-dimmed github-light","{\n  \"upload_id\": \"u_123\",\n  \"total_chunks\": 100,\n  \"uploaded_chunks\": [0, 1, 2, 5, 6],\n  \"missing_chunks\": [3, 4, 7, 8]\n}\n","json",[81,432,433,442,459,473,509,538],{"__ignoreMap":107},[434,435,438],"span",{"class":436,"line":437},"line",1,[434,439,441],{"class":440},"ssh_m","{\n",[434,443,445,449,452,456],{"class":436,"line":444},2,[434,446,448],{"class":447},"sJK54","  \"upload_id\"",[434,450,451],{"class":440},": ",[434,453,455],{"class":454},"sXfbr","\"u_123\"",[434,457,458],{"class":440},",\n",[434,460,462,465,467,471],{"class":436,"line":461},3,[434,463,464],{"class":447},"  \"total_chunks\"",[434,466,451],{"class":440},[434,468,470],{"class":469},"swcJU","100",[434,472,458],{"class":440},[434,474,476,479,482,485,488,491,493,496,498,501,503,506],{"class":436,"line":475},4,[434,477,478],{"class":447},"  \"uploaded_chunks\"",[434,480,481],{"class":440},": [",[434,483,484],{"class":469},"0",[434,486,487],{"class":440},", ",[434,489,490],{"class":469},"1",[434,492,487],{"class":440},[434,494,495],{"class":469},"2",[434,497,487],{"class":440},[434,499,500],{"class":469},"5",[434,502,487],{"class":440},[434,504,505],{"class":469},"6",[434,507,508],{"class":440},"],\n",[434,510,512,515,517,520,522,525,527,530,532,535],{"class":436,"line":511},5,[434,513,514],{"class":447},"  \"missing_chunks\"",[434,516,481],{"class":440},[434,518,519],{"class":469},"3",[434,521,487],{"class":440},[434,523,524],{"class":469},"4",[434,526,487],{"class":440},[434,528,529],{"class":469},"7",[434,531,487],{"class":440},[434,533,534],{"class":469},"8",[434,536,537],{"class":440},"]\n",[434,539,541],{"class":436,"line":540},6,[434,542,543],{"class":440},"}\n",[10,545,546,547,550],{},"客户端只补传 ",[81,548,549],{},"missing_chunks"," 即可。",[26,552,553],{"id":553},"分片合并与一致性",[10,555,556],{},"分片合并是高并发上传中最容易出问题的地方。",[147,558,559],{"id":559},"为什么需要分布式锁",[10,561,562],{},"假设客户端因为网络重试，同时发了两次 complete 请求。如果没有保护，后端可能会同时执行两次合并：",[101,564,567],{"className":565,"code":566,"language":106,"meta":107},[104],"请求 A：发现所有分片已上传 -> 开始合并\n请求 B：发现所有分片已上传 -> 也开始合并\n",[81,568,566],{"__ignoreMap":107},[10,570,571],{},"结果可能是：",[125,573,574,577,580,583],{},[56,575,576],{},"重复生成对象。",[56,578,579],{},"重复写 MySQL 元数据。",[56,581,582],{},"ref_count 错乱。",[56,584,585],{},"临时分片被提前删除。",[10,587,588],{},"因此 CloudVault 引入 Redis 分布式锁来保护合并阶段：",[101,590,593],{"className":591,"code":592,"language":106,"meta":107},[104],"lock key: upload:merge:{upload_id}\n",[81,594,592],{"__ignoreMap":107},[10,596,597],{},"只有拿到锁的请求才能执行合并，其它请求返回“合并中”或等待重试。",[147,599,600],{"id":600},"合并流程",[101,602,605],{"className":603,"code":604,"language":106,"meta":107},[104],"获取 Redis 分布式锁\n  -> 校验 upload_session 状态\n  -> 校验所有分片都已上传\n  -> 按 chunk_index 顺序读取分片\n  -> 合并生成最终对象\n  -> 计算最终文件 Hash 并校验\n  -> 写入 files 元数据\n  -> 更新 upload_session 为 completed\n  -> 删除临时分片或异步清理\n  -> 释放锁\n",[81,606,604],{"__ignoreMap":107},[147,608,609],{"id":609},"一致性策略",[10,611,612],{},"由于 MinIO、MySQL、Redis 是不同系统，无法简单依赖本地事务覆盖所有操作。比较稳妥的做法是：",[125,614,615,618,621,624,627],{},[56,616,617],{},"MySQL 记录 upload_session 的状态机。",[56,619,620],{},"对象存储操作尽量幂等。",[56,622,623],{},"合并前后都校验状态。",[56,625,626],{},"Redis 锁防止并发合并。",[56,628,629],{},"异步清理临时对象，即使失败也可以后续补偿。",[10,631,632],{},"上传会话可以设计成状态机：",[101,634,637],{"className":635,"code":636,"language":106,"meta":107},[104],"initialized -> uploading -> merging -> completed\n                         \\-> failed\n                         \\-> expired\n",[81,638,636],{"__ignoreMap":107},[10,640,641],{},"这样即使中途失败，也能知道恢复或清理应该从哪里开始。",[26,643,644],{"id":644},"文件下载设计",[10,646,647],{},"下载看似简单，但大文件下载同样需要优化。",[147,649,650],{"id":650},"流式下载",[10,652,653],{},"后端不应该一次性把文件读进内存，而应该从 MinIO 获取对象流，再写入 HTTP Response：",[101,655,658],{"className":656,"code":657,"language":106,"meta":107},[104],"MinIO object stream -> Go API response writer -> client\n",[81,659,657],{"__ignoreMap":107},[10,661,662],{},"这样内存占用和文件大小无关，更适合大文件。",[147,664,666],{"id":665},"预签名-url","预签名 URL",[10,668,669],{},"对于大文件下载，可以生成 MinIO 预签名 URL，让客户端直接从对象存储下载：",[101,671,674],{"className":672,"code":673,"language":106,"meta":107},[104],"客户端请求下载\n  -> 后端鉴权\n  -> 后端生成短期有效 presigned URL\n  -> 客户端直接访问 MinIO 下载\n",[81,675,673],{"__ignoreMap":107},[10,677,678],{},"好处是：",[125,680,681,684,687,690],{},[56,682,683],{},"降低应用服务器带宽压力。",[56,685,686],{},"下载链路更短。",[56,688,689],{},"URL 可以设置过期时间。",[56,691,692],{},"适合大文件和高并发下载。",[147,694,695],{"id":695},"直链与防盗链",[10,697,698],{},"如果支持分享直链，需要考虑：",[125,700,701,704,707,710,713,716],{},[56,702,703],{},"链接是否过期。",[56,705,706],{},"是否需要访问码。",[56,708,709],{},"是否绑定用户权限。",[56,711,712],{},"是否限制下载次数。",[56,714,715],{},"是否限制 IP 或 Referer。",[56,717,718],{},"是否允许批量下载。",[10,720,721],{},"一个安全的下载链接不应该永久有效，否则很容易被外部传播造成带宽滥用。",[26,723,725],{"id":724},"zip-批量打包下载","ZIP 批量打包下载",[10,727,728],{},"网盘常见需求是选中多个文件夹或文件后打包下载。",[147,730,731],{"id":731},"同步打包的问题",[10,733,734],{},"如果用户一次选择几百个文件，同步打包会导致：",[125,736,737,740,743,746],{},[56,738,739],{},"HTTP 请求长时间占用。",[56,741,742],{},"服务端 CPU 和 IO 压力大。",[56,744,745],{},"中途失败后无法恢复。",[56,747,748],{},"用户无法查看打包进度。",[147,750,751],{"id":751},"更合理的设计",[10,753,754],{},"可以将 ZIP 打包设计成异步任务：",[101,756,759],{"className":757,"code":758,"language":106,"meta":107},[104],"用户提交批量下载请求\n  -> API 创建 zip_task\n  -> 投递 RabbitMQ 消息\n  -> Worker 拉取文件并生成 ZIP\n  -> ZIP 上传到 MinIO\n  -> 更新任务状态和下载链接\n  -> 用户轮询或 WebSocket 获取结果\n",[81,760,758],{"__ignoreMap":107},[10,762,763],{},"如果文件数量较少，也可以边压缩边流式返回。但对于大型目录，异步 ZIP 更稳定。",[26,765,767],{"id":766},"rabbitmq-异步任务系统","RabbitMQ 异步任务系统",[10,769,770],{},"CloudVault 使用 RabbitMQ 构建离线下载任务系统，核心目的是把耗时任务从 HTTP 请求中拆出去。",[147,772,773],{"id":773},"为什么需要消息队列",[10,775,776],{},"离线下载可能需要几十秒甚至几分钟，如果直接在 API 请求里执行：",[125,778,779,782,785,788,791],{},[56,780,781],{},"请求容易超时。",[56,783,784],{},"API 服务线程被占用。",[56,786,787],{},"无法控制并发。",[56,789,790],{},"失败后不容易重试。",[56,792,793],{},"用户无法查看任务状态。",[10,795,796],{},"RabbitMQ 可以将任务排队，由 Worker 后台消费。",[147,798,799],{"id":799},"离线下载流程",[101,801,804],{"className":802,"code":803,"language":106,"meta":107},[104],"用户提交离线下载 URL\n  -> API 校验 URL 和权限\n  -> MySQL 创建 download_task\n  -> RabbitMQ 投递任务消息\n  -> Worker 消费任务\n  -> Worker 下载远程文件\n  -> 写入 MinIO\n  -> 更新 MySQL 任务状态\n  -> 同步 ES 索引\n  -> 通知用户完成\n",[81,805,803],{"__ignoreMap":107},[10,807,808],{},"任务状态可以设计为：",[101,810,813],{"className":811,"code":812,"language":106,"meta":107},[104],"pending -> running -> success\n        \\-> retrying -> running\n        \\-> failed\n        \\-> canceled\n",[81,814,812],{"__ignoreMap":107},[147,816,817],{"id":817},"失败重试",[10,819,820],{},"下载失败可能有很多原因：",[125,822,823,826,829,832,835],{},[56,824,825],{},"远程 URL 超时。",[56,827,828],{},"网络连接断开。",[56,830,831],{},"文件过大。",[56,833,834],{},"目标网站限制下载。",[56,836,837],{},"对象存储写入失败。",[10,839,840],{},"可以采用延迟重试：",[101,842,845],{"className":843,"code":844,"language":106,"meta":107},[104],"第一次失败：10 秒后重试\n第二次失败：1 分钟后重试\n第三次失败：5 分钟后重试\n超过最大次数：进入 DLQ\n",[81,846,844],{"__ignoreMap":107},[147,848,850],{"id":849},"dead-letter-queue","Dead Letter Queue",[10,852,853],{},"DLQ 是 Dead Letter Queue，死信队列。任务多次失败后不应该无限重试，否则会浪费资源。进入 DLQ 后可以：",[125,855,856,859,862,865],{},[56,857,858],{},"记录失败原因。",[56,860,861],{},"后台人工排查。",[56,863,864],{},"管理员手动重放任务。",[56,866,867],{},"给用户展示明确失败信息。",[10,869,870],{},"面试中讲到 RabbitMQ，如果能主动提到重试、延迟队列和 DLQ，会比只说“用了 MQ 异步处理”更有工程深度。",[26,872,874],{"id":873},"redis-在项目中的作用","Redis 在项目中的作用",[10,876,877],{},"Redis 在 CloudVault 中不是单纯做缓存，它至少承担四类职责。",[147,879,880],{"id":880},"用户信息缓存",[10,882,883],{},"用户信息、权限、空间配额这类读多写少的数据可以缓存：",[101,885,888],{"className":886,"code":887,"language":106,"meta":107},[104],"user:{user_id} -> 用户基础信息\nquota:{user_id} -> 空间使用量\n",[81,889,887],{"__ignoreMap":107},[10,891,892],{},"这样可以减少 MySQL 查询。",[147,894,895],{"id":895},"热门文件元数据缓存",[10,897,898],{},"热门文件的元数据、下载次数、分享信息可以放 Redis：",[101,900,903],{"className":901,"code":902,"language":106,"meta":107},[104],"file:meta:{file_id}\nshare:{share_code}\nhot:files\n",[81,904,902],{"__ignoreMap":107},[10,906,907],{},"对于高频分享链接，缓存能明显降低数据库压力。",[147,909,910],{"id":910},"分布式锁",[10,912,913],{},"分片合并、秒传引用计数更新、分享链接更新等场景都可能需要分布式锁。",[101,915,918],{"className":916,"code":917,"language":106,"meta":107},[104],"lock:upload:merge:{upload_id}\nlock:file:ref:{file_hash}\n",[81,919,917],{"__ignoreMap":107},[147,921,922],{"id":922},"上传进度与临时状态",[10,924,925],{},"分片上传进度可以短期放 Redis：",[101,927,930],{"className":928,"code":929,"language":106,"meta":107},[104],"upload:{upload_id}:chunks -> bitmap \u002F set\nupload:{upload_id}:progress -> percentage\n",[81,931,929],{"__ignoreMap":107},[10,933,934],{},"这样查询进度更快，也减少频繁写 MySQL。",[26,936,937],{"id":937},"缓存一致性与失效策略",[10,939,940],{},"缓存不是加上 Redis 就完事了，关键是怎么保证数据不过期、不脏读。",[10,942,943],{},"常见策略：",[53,945,946,949,952,955,958,961],{},[56,947,948],{},"Cache Aside：先读缓存，miss 后读 MySQL，再写缓存。",[56,950,951],{},"更新数据库后删除缓存，而不是直接更新缓存。",[56,953,954],{},"给缓存设置 TTL，防止永久脏数据。",[56,956,957],{},"热点 key 加随机过期时间，避免缓存雪崩。",[56,959,960],{},"对不存在的数据缓存空值，防止缓存穿透。",[56,962,963],{},"热点文件可以提前预热。",[10,965,966],{},"例如文件元数据更新：",[101,968,971],{"className":969,"code":970,"language":106,"meta":107},[104],"更新 MySQL files 表\n  -> 删除 Redis file:meta:{file_id}\n  -> 下次读取时重新加载\n",[81,972,970],{"__ignoreMap":107},[10,974,975],{},"为什么是删除缓存而不是更新缓存？因为删除更简单，也更不容易出现并发覆盖问题。",[26,977,979],{"id":978},"elasticsearch-文件搜索","Elasticsearch 文件搜索",[10,981,982],{},"MySQL 适合精确查询和事务，但不适合复杂全文搜索。CloudVault 引入 Elasticsearch 用于文件检索。",[147,984,985],{"id":985},"可以索引什么",[10,987,988],{},"文件搜索索引可以包含：",[101,990,992],{"className":428,"code":991,"language":430,"meta":107,"style":107},"{\n  \"file_id\": 123,\n  \"user_id\": 45,\n  \"filename\": \"flow matching notes.pdf\",\n  \"path\": \"\u002Fpapers\u002Fgenerative-models\u002F\",\n  \"mime_type\": \"application\u002Fpdf\",\n  \"tags\": [\"AI\", \"Diffusion\", \"Flow Matching\"],\n  \"summary\": \"一份关于 Flow Matching 的学习笔记\",\n  \"created_at\": \"2026-05-10T10:00:00Z\",\n  \"updated_at\": \"2026-05-10T10:00:00Z\"\n}\n",[81,993,994,998,1010,1022,1034,1046,1058,1081,1094,1107,1118],{"__ignoreMap":107},[434,995,996],{"class":436,"line":437},[434,997,441],{"class":440},[434,999,1000,1003,1005,1008],{"class":436,"line":444},[434,1001,1002],{"class":447},"  \"file_id\"",[434,1004,451],{"class":440},[434,1006,1007],{"class":469},"123",[434,1009,458],{"class":440},[434,1011,1012,1015,1017,1020],{"class":436,"line":461},[434,1013,1014],{"class":447},"  \"user_id\"",[434,1016,451],{"class":440},[434,1018,1019],{"class":469},"45",[434,1021,458],{"class":440},[434,1023,1024,1027,1029,1032],{"class":436,"line":475},[434,1025,1026],{"class":447},"  \"filename\"",[434,1028,451],{"class":440},[434,1030,1031],{"class":454},"\"flow matching notes.pdf\"",[434,1033,458],{"class":440},[434,1035,1036,1039,1041,1044],{"class":436,"line":511},[434,1037,1038],{"class":447},"  \"path\"",[434,1040,451],{"class":440},[434,1042,1043],{"class":454},"\"\u002Fpapers\u002Fgenerative-models\u002F\"",[434,1045,458],{"class":440},[434,1047,1048,1051,1053,1056],{"class":436,"line":540},[434,1049,1050],{"class":447},"  \"mime_type\"",[434,1052,451],{"class":440},[434,1054,1055],{"class":454},"\"application\u002Fpdf\"",[434,1057,458],{"class":440},[434,1059,1061,1064,1066,1069,1071,1074,1076,1079],{"class":436,"line":1060},7,[434,1062,1063],{"class":447},"  \"tags\"",[434,1065,481],{"class":440},[434,1067,1068],{"class":454},"\"AI\"",[434,1070,487],{"class":440},[434,1072,1073],{"class":454},"\"Diffusion\"",[434,1075,487],{"class":440},[434,1077,1078],{"class":454},"\"Flow Matching\"",[434,1080,508],{"class":440},[434,1082,1084,1087,1089,1092],{"class":436,"line":1083},8,[434,1085,1086],{"class":447},"  \"summary\"",[434,1088,451],{"class":440},[434,1090,1091],{"class":454},"\"一份关于 Flow Matching 的学习笔记\"",[434,1093,458],{"class":440},[434,1095,1097,1100,1102,1105],{"class":436,"line":1096},9,[434,1098,1099],{"class":447},"  \"created_at\"",[434,1101,451],{"class":440},[434,1103,1104],{"class":454},"\"2026-05-10T10:00:00Z\"",[434,1106,458],{"class":440},[434,1108,1110,1113,1115],{"class":436,"line":1109},10,[434,1111,1112],{"class":447},"  \"updated_at\"",[434,1114,451],{"class":440},[434,1116,1117],{"class":454},"\"2026-05-10T10:00:00Z\"\n",[434,1119,1121],{"class":436,"line":1120},11,[434,1122,543],{"class":440},[10,1124,1125],{},"如果做得更进一步，还可以对文档内容提取文本后索引。",[147,1127,1128],{"id":1128},"索引同步",[10,1130,1131],{},"文件上传成功后，需要同步 ES 索引：",[101,1133,1136],{"className":1134,"code":1135,"language":106,"meta":107},[104],"文件元数据写入 MySQL\n  -> 投递 file_index 任务\n  -> Worker 写入 Elasticsearch\n",[81,1137,1135],{"__ignoreMap":107},[10,1139,1140],{},"为什么建议异步同步？因为 ES 暂时不可用时，不应该影响主链路上传成功。可以先保证 MySQL 和 MinIO 成功，再由后台任务补偿索引。",[147,1142,1144],{"id":1143},"mysql-回退查询","MySQL 回退查询",[10,1146,1147],{},"截图中提到“索引同步与 MySQL 回退查询”。这点很重要。",[10,1149,1150],{},"如果 Elasticsearch 查询失败，系统可以降级到 MySQL：",[101,1152,1155],{"className":1153,"code":1154,"language":106,"meta":107},[104],"优先查 ES\n  -> ES 失败或超时\n  -> 回退 MySQL filename like \u002F metadata query\n  -> 返回基础结果\n",[81,1156,1154],{"__ignoreMap":107},[10,1158,1159],{},"这样搜索能力不会因为 ES 故障完全不可用。",[26,1161,1162],{"id":1162},"文件预览设计",[10,1164,1165],{},"网盘系统通常需要支持图片、PDF、文本、视频等预览。",[10,1167,1168],{},"可以按文件类型拆分：",[125,1170,1171,1174,1177,1180,1183],{},[56,1172,1173],{},"图片：直接返回预签名 URL 或缩略图。",[56,1175,1176],{},"PDF：浏览器内嵌预览。",[56,1178,1179],{},"文本 \u002F Markdown：后端读取前 N KB 内容并返回。",[56,1181,1182],{},"视频：支持 Range 请求，实现拖动播放。",[56,1184,1185],{},"Office 文档：可以异步转 PDF 或使用在线预览服务。",[10,1187,1188],{},"预览时要注意权限校验，不能只要知道 object key 就能访问文件。",[26,1190,1191],{"id":1191},"回收站设计",[10,1193,1194],{},"用户删除文件时，不建议立刻删除对象存储中的文件。更好的方式是软删除：",[101,1196,1199],{"className":1197,"code":1198,"language":106,"meta":107},[104],"用户删除文件\n  -> files.status = trashed\n  -> 写入 trash_records\n  -> 文件从普通列表隐藏\n  -> 用户可以恢复\n  -> 过期后后台任务真正清理\n",[81,1200,1198],{"__ignoreMap":107},[10,1202,1203],{},"真正清理时还要考虑引用计数：",[101,1205,1208],{"className":1206,"code":1207,"language":106,"meta":107},[104],"ref_count > 1：只删除当前用户引用，不删对象\nref_count = 1：删除对象存储中的真实文件\n",[81,1209,1207],{"__ignoreMap":107},[10,1211,1212],{},"这和秒传、去重存储是配套的。",[26,1214,1215],{"id":1215},"分享系统设计",[10,1217,1218],{},"分享系统的核心不是生成一个 URL，而是权限控制。",[10,1220,1221],{},"一个分享链接可以包含：",[125,1223,1224,1227,1230,1233,1236,1239,1242],{},[56,1225,1226],{},"share_code：短链接标识。",[56,1228,1229],{},"access_token：访问令牌。",[56,1231,1232],{},"password：可选访问码。",[56,1234,1235],{},"expire_at：过期时间。",[56,1237,1238],{},"allow_download：是否允许下载。",[56,1240,1241],{},"visit_count：访问次数。",[56,1243,1244],{},"max_visit_count：最大访问次数。",[10,1246,1247],{},"访问流程：",[101,1249,1252],{"className":1250,"code":1251,"language":106,"meta":107},[104],"用户访问分享链接\n  -> 校验 share_code 是否存在\n  -> 校验是否过期\n  -> 校验访问码\n  -> 校验文件是否还存在\n  -> 返回文件列表或下载链接\n",[81,1253,1251],{"__ignoreMap":107},[10,1255,1256],{},"如果要防止链接被滥用，可以对分享下载加限速和访问次数限制。",[26,1258,1259],{"id":1259},"安全性设计",[10,1261,1262],{},"文件系统项目必须重视安全。",[147,1264,1265],{"id":1265},"上传安全",[10,1267,1268],{},"需要限制：",[125,1270,1271,1274,1277,1280,1283,1286],{},[56,1272,1273],{},"单文件大小。",[56,1275,1276],{},"用户总容量。",[56,1278,1279],{},"文件类型。",[56,1281,1282],{},"分片大小。",[56,1284,1285],{},"并发上传数量。",[56,1287,1288],{},"恶意文件名和路径穿越。",[10,1290,1291],{},"例如文件名不能直接拼到本地路径中，否则可能出现：",[101,1293,1296],{"className":1294,"code":1295,"language":106,"meta":107},[104],"..\u002F..\u002Fetc\u002Fpasswd\n",[81,1297,1295],{"__ignoreMap":107},[10,1299,1300],{},"对象存储 key 应该由服务端生成，而不是完全信任用户输入。",[147,1302,1303],{"id":1303},"下载安全",[10,1305,1306],{},"下载前必须校验：",[125,1308,1309,1312,1315,1318,1321],{},[56,1310,1311],{},"用户是否登录。",[56,1313,1314],{},"文件是否属于用户。",[56,1316,1317],{},"分享链接是否有效。",[56,1319,1320],{},"文件是否已经被删除。",[56,1322,1323],{},"是否有下载权限。",[147,1325,1327],{"id":1326},"url-安全","URL 安全",[10,1329,1330],{},"预签名 URL 应该短期有效，并且只在鉴权通过后生成。",[10,1332,1333],{},"例如：",[101,1335,1338],{"className":1336,"code":1337,"language":106,"meta":107},[104],"有效期 5 分钟\n只允许 GET\n只对应某一个 object key\n",[81,1339,1337],{"__ignoreMap":107},[10,1341,1342],{},"这样即使 URL 泄露，风险也有限。",[26,1344,1346],{"id":1345},"ai-agent-与-rag-扩展","AI Agent 与 RAG 扩展",[10,1348,1349],{},"截图中提到：集成 AI Agent（Function Calling），实现自然语言文件查询与下载链接生成，并设计 RAG 文档问答架构。",[10,1351,1352],{},"这部分可以理解为 CloudVault 的智能化扩展。",[147,1354,1356],{"id":1355},"function-calling-能做什么","Function Calling 能做什么",[10,1358,1359],{},"用户可以问：",[101,1361,1364],{"className":1362,"code":1363,"language":106,"meta":107},[104],"帮我找一下上周上传的 Flow Matching PDF\n把我最近的简历下载链接发给我\n找出所有和 RAG 有关的文档\n",[81,1365,1363],{"__ignoreMap":107},[10,1367,1368],{},"AI Agent 不应该直接访问数据库，而是通过工具函数调用后端能力：",[101,1370,1373],{"className":1371,"code":1372,"language":106,"meta":107},[104],"search_files(query, user_id, filters)\nget_file_detail(file_id, user_id)\ngenerate_download_link(file_id, user_id)\nsummarize_document(file_id, user_id)\n",[81,1374,1372],{"__ignoreMap":107},[10,1376,1377],{},"这样可以保证权限控制仍然在后端服务内完成。",[147,1379,1381],{"id":1380},"rag-文档问答流程","RAG 文档问答流程",[10,1383,1384],{},"如果要支持“基于文件内容问答”，需要构建 RAG 管线：",[101,1386,1389],{"className":1387,"code":1388,"language":106,"meta":107},[104],"文件上传成功\n  -> 判断是否可解析\n  -> 提取文本内容\n  -> 文本切 chunk\n  -> 生成 embedding\n  -> 写入向量数据库 \u002F ES dense vector\n  -> 用户提问时检索相关 chunk\n  -> LLM 基于检索内容回答\n",[81,1390,1388],{"__ignoreMap":107},[10,1392,1393],{},"例如用户问：",[101,1395,1398],{"className":1396,"code":1397,"language":106,"meta":107},[104],"我那篇 Flow Matching 文章里 Rectified Flow 是怎么解释的？\n",[81,1399,1397],{"__ignoreMap":107},[10,1401,1402],{},"系统应该先检索该用户有权限访问的文件内容，再让模型基于检索片段回答，而不是让模型凭空编。",[147,1404,1406],{"id":1405},"ai-权限边界","AI 权限边界",[10,1408,1409],{},"AI Agent 接入文件系统时必须注意：",[125,1411,1412,1415,1418,1421,1424],{},[56,1413,1414],{},"Agent 只能访问当前用户有权限的文件。",[56,1416,1417],{},"下载链接必须由后端鉴权后生成。",[56,1419,1420],{},"RAG 检索必须带 user_id \u002F permission filter。",[56,1422,1423],{},"不允许模型自己拼 object key 访问对象存储。",[56,1425,1426],{},"工具调用需要审计日志。",[10,1428,1429],{},"这也是 AI Infra 项目里很重要的工程点。",[26,1431,1432],{"id":1432},"高并发优化总结",[10,1434,1435],{},"CloudVault 的高并发优化可以总结为几类。",[147,1437,1438],{"id":1438},"存储层",[125,1440,1441,1444,1447,1450],{},[56,1442,1443],{},"文件本体放对象存储，避免压垮 MySQL。",[56,1445,1446],{},"大文件分片上传，失败只重传分片。",[56,1448,1449],{},"Hash 去重，减少重复存储。",[56,1451,1452],{},"预签名 URL，降低应用服务器带宽压力。",[147,1454,1455],{"id":1455},"数据库层",[125,1457,1458,1461,1464,1467],{},[56,1459,1460],{},"MySQL 只存元数据。",[56,1462,1463],{},"高频查询字段建立索引。",[56,1465,1466],{},"文件列表按目录和用户分页查询。",[56,1468,1469],{},"删除使用软删除状态。",[147,1471,1472],{"id":1472},"缓存层",[125,1474,1475,1478,1481,1484,1487],{},[56,1476,1477],{},"用户信息缓存。",[56,1479,1480],{},"热门文件元数据缓存。",[56,1482,1483],{},"分享链接缓存。",[56,1485,1486],{},"上传进度缓存。",[56,1488,1489],{},"分布式锁保护关键阶段。",[147,1491,1492],{"id":1492},"异步层",[125,1494,1495,1498,1501,1504],{},[56,1496,1497],{},"离线下载异步化。",[56,1499,1500],{},"ZIP 打包异步化。",[56,1502,1503],{},"ES 索引同步异步化。",[56,1505,1506],{},"失败重试和 DLQ 兜底。",[147,1508,1509],{"id":1509},"搜索层",[125,1511,1512,1515,1518],{},[56,1513,1514],{},"Elasticsearch 承担全文搜索。",[56,1516,1517],{},"MySQL 作为回退查询。",[56,1519,1520],{},"文件内容可进一步接入 RAG。",[26,1522,1523],{"id":1523},"典型接口设计",[10,1525,1526],{},"下面是一组比较合理的 API 草图。",[147,1528,1529],{"id":1529},"文件接口",[101,1531,1534],{"className":1532,"code":1533,"language":106,"meta":107},[104],"GET    \u002Fapi\u002Ffiles?parent_id=xxx\nPOST   \u002Fapi\u002Ffiles\u002Ffolders\nDELETE \u002Fapi\u002Ffiles\u002F{file_id}\nPOST   \u002Fapi\u002Ffiles\u002F{file_id}\u002Frestore\nGET    \u002Fapi\u002Ffiles\u002F{file_id}\u002Fpreview\nGET    \u002Fapi\u002Ffiles\u002F{file_id}\u002Fdownload\n",[81,1535,1533],{"__ignoreMap":107},[147,1537,1538],{"id":1538},"上传接口",[101,1540,1543],{"className":1541,"code":1542,"language":106,"meta":107},[104],"POST \u002Fapi\u002Fuploads\u002Fcheck\nPOST \u002Fapi\u002Fuploads\u002Finit\nPUT  \u002Fapi\u002Fuploads\u002F{upload_id}\u002Fchunks\u002F{chunk_index}\nGET  \u002Fapi\u002Fuploads\u002F{upload_id}\u002Fstatus\nPOST \u002Fapi\u002Fuploads\u002F{upload_id}\u002Fcomplete\n",[81,1544,1542],{"__ignoreMap":107},[147,1546,1547],{"id":1547},"分享接口",[101,1549,1552],{"className":1550,"code":1551,"language":106,"meta":107},[104],"POST   \u002Fapi\u002Fshares\nGET    \u002Fapi\u002Fshares\u002F{share_code}\nPOST   \u002Fapi\u002Fshares\u002F{share_code}\u002Fverify\nGET    \u002Fapi\u002Fshares\u002F{share_code}\u002Fdownload\nDELETE \u002Fapi\u002Fshares\u002F{share_id}\n",[81,1553,1551],{"__ignoreMap":107},[147,1555,1556],{"id":1556},"搜索接口",[101,1558,1561],{"className":1559,"code":1560,"language":106,"meta":107},[104],"GET \u002Fapi\u002Fsearch?q=xxx&type=pdf&from=0&size=20\n",[81,1562,1560],{"__ignoreMap":107},[147,1564,1565],{"id":1565},"离线下载接口",[101,1567,1570],{"className":1568,"code":1569,"language":106,"meta":107},[104],"POST \u002Fapi\u002Foffline-downloads\nGET  \u002Fapi\u002Foffline-downloads\u002F{task_id}\nPOST \u002Fapi\u002Foffline-downloads\u002F{task_id}\u002Fcancel\n",[81,1571,1569],{"__ignoreMap":107},[147,1573,1575],{"id":1574},"ai-查询接口","AI 查询接口",[101,1577,1580],{"className":1578,"code":1579,"language":106,"meta":107},[104],"POST \u002Fapi\u002Fai\u002Fchat\nPOST \u002Fapi\u002Fai\u002Fsearch-files\nPOST \u002Fapi\u002Fai\u002Fgenerate-download-link\n",[81,1581,1579],{"__ignoreMap":107},[26,1583,1584],{"id":1584},"项目难点",[10,1586,1587],{},"如果面试官追问这个项目难在哪里，可以重点讲下面几个。",[147,1589,1590],{"id":1590},"分片上传状态一致性",[10,1592,1593],{},"难点在于分片并发上传、complete 重复请求、服务重启、对象存储成功但数据库失败等边界情况。",[10,1595,1596],{},"解决思路：",[125,1598,1599,1602,1605,1608,1611],{},[56,1600,1601],{},"上传会话状态机。",[56,1603,1604],{},"分片状态持久化。",[56,1606,1607],{},"Redis 分布式锁保护合并。",[56,1609,1610],{},"合并操作幂等。",[56,1612,1613],{},"定时任务清理过期 session 和孤儿 chunk。",[147,1615,1616],{"id":1616},"秒传和去重的一致性",[10,1618,1619],{},"如果多个用户同时上传同一个 Hash，需要保证 ref_count 正确。",[10,1621,1596],{},[125,1623,1624,1627,1630],{},[56,1625,1626],{},"file_hash 建唯一索引或单独 object 表。",[56,1628,1629],{},"更新引用计数时使用事务或分布式锁。",[56,1631,1632],{},"对象写入和元数据写入要有补偿机制。",[147,1634,1635],{"id":1635},"离线下载的可靠性",[10,1637,1638],{},"离线下载可能失败、超时、重试，也可能 Worker 崩溃。",[10,1640,1596],{},[125,1642,1643,1646,1649,1652,1655,1658],{},[56,1644,1645],{},"RabbitMQ ack 机制。",[56,1647,1648],{},"任务状态持久化。",[56,1650,1651],{},"最大重试次数。",[56,1653,1654],{},"延迟重试。",[56,1656,1657],{},"DLQ 保存失败任务。",[56,1659,1660],{},"Worker 幂等处理。",[147,1662,1663],{"id":1663},"搜索索引一致性",[10,1665,1666],{},"ES 和 MySQL 之间可能短暂不一致。",[10,1668,1596],{},[125,1670,1671,1674,1677,1680,1683],{},[56,1672,1673],{},"主数据以 MySQL 为准。",[56,1675,1676],{},"ES 异步同步。",[56,1678,1679],{},"失败任务重试。",[56,1681,1682],{},"定时全量校验或重建索引。",[56,1684,1685],{},"ES 故障时回退 MySQL。",[147,1687,1689],{"id":1688},"ai-agent-的权限控制","AI Agent 的权限控制",[10,1691,1692],{},"AI 查询文件时很容易出现越权风险。",[10,1694,1596],{},[125,1696,1697,1700,1703,1706,1709],{},[56,1698,1699],{},"所有工具函数都必须带 user_id。",[56,1701,1702],{},"检索时强制加权限过滤。",[56,1704,1705],{},"下载链接必须后端生成。",[56,1707,1708],{},"工具调用写审计日志。",[56,1710,1711],{},"模型只负责决策，不直接访问底层存储。",[26,1713,1714],{"id":1714},"简历写法建议",[10,1716,1717],{},"可以把这个项目写成下面这种格式：",[33,1719,1720],{},[10,1721,1722],{},"CloudVault（基于 Go 的云端存储与网盘系统）：面向大文件传输与高并发场景，完成文件上传、下载、分享、回收站、离线下载、搜索与预览等核心功能。采用 MinIO 对象存储 + MySQL 元数据解耦架构，支持分片上传、断点续传、Hash 秒传和引用计数去重；使用 Redis 实现热点缓存、上传进度缓存和分布式锁，保障分片合并一致性；基于 RabbitMQ 构建离线下载任务系统，支持任务入队、Worker 并发消费、失败重试、延迟重试和 DLQ；引入 Elasticsearch 构建文件检索能力，并设计 AI Agent \u002F RAG 扩展，实现自然语言文件查询与下载链接生成。",[26,1724,1726],{"id":1725},"面试-1-分钟讲法","面试 1 分钟讲法",[10,1728,1729],{},"如果面试官让你介绍这个项目，可以这样说：",[33,1731,1732],{},[10,1733,1734],{},"CloudVault 是我做的一个 Go 云端网盘系统，核心是解决大文件上传下载和高并发文件管理问题。架构上我把文件本体和业务元数据解耦，文件放 MinIO，对象 key、Hash、目录、权限、分享等元数据放 MySQL。上传侧支持分片上传、断点续传和 Hash 秒传，分片合并时用 Redis 分布式锁保证并发一致性。下载侧支持流式下载、预签名 URL 和 ZIP 批量下载，降低应用服务器带宽压力。异步任务方面用 RabbitMQ 做离线下载，支持 Worker 消费、失败重试、延迟重试和 DLQ。搜索方面用 Elasticsearch 建文件索引，ES 不可用时回退 MySQL。后续还设计了 AI Agent 和 RAG，让用户可以用自然语言查文件、生成下载链接和基于文档内容问答。",[26,1736,1737],{"id":1737},"面试高频追问",[147,1739,1741],{"id":1740},"q1为什么不用本地磁盘存文件","Q1：为什么不用本地磁盘存文件？",[10,1743,1744],{},"本地磁盘不利于多实例部署和扩容，服务迁移、容灾、备份都麻烦。对象存储更适合大文件，可以统一管理对象、生命周期和访问权限。",[147,1746,1748],{"id":1747},"q2为什么文件本体不放-mysql","Q2：为什么文件本体不放 MySQL？",[10,1750,1751],{},"MySQL 更适合存结构化元数据，不适合承载大文件。大文件会导致数据库体积、备份恢复、连接占用和 IO 压力都变大。因此文件放 MinIO，MySQL 只保存 object key 和业务元数据。",[147,1753,1755],{"id":1754},"q3断点续传怎么实现","Q3：断点续传怎么实现？",[10,1757,1758],{},"客户端把文件切成多个 chunk，服务端记录 upload_session 和每个 chunk 的上传状态。中断后客户端查询已上传分片列表，只补传缺失分片。所有分片完成后再合并。",[147,1760,1762],{"id":1761},"q4分片合并怎么保证不会重复执行","Q4：分片合并怎么保证不会重复执行？",[10,1764,1765,1766,1769],{},"使用 Redis 分布式锁保护 ",[81,1767,1768],{},"upload_id"," 对应的合并流程，同时在 MySQL 中维护 session 状态机。complete 请求重复到达时，只有一个请求能拿到锁，其它请求返回合并中或读取最终状态。",[147,1771,1773],{"id":1772},"q5秒传怎么实现","Q5：秒传怎么实现？",[10,1775,1776],{},"上传前先计算文件 Hash，服务端检查 Hash 是否已经存在。如果存在，只创建新的用户文件记录并增加引用计数；如果不存在，再真正上传对象内容。",[147,1778,1780],{"id":1779},"q6离线下载为什么要用-rabbitmq","Q6：离线下载为什么要用 RabbitMQ？",[10,1782,1783],{},"离线下载是长耗时任务，直接在 HTTP 请求中执行容易超时，也不利于控制并发。RabbitMQ 可以让任务入队，由 Worker 异步消费，并支持失败重试、延迟重试和 DLQ。",[147,1785,1787],{"id":1786},"q7redis-缓存如何保持一致","Q7：Redis 缓存如何保持一致？",[10,1789,1790],{},"使用 Cache Aside 模式。读时先查缓存，miss 后查 MySQL 并回填；写时先更新 MySQL，再删除缓存，让下次读取重新加载。同时设置 TTL 防止永久脏数据。",[147,1792,1794],{"id":1793},"q8elasticsearch-和-mysql-数据不一致怎么办","Q8：Elasticsearch 和 MySQL 数据不一致怎么办？",[10,1796,1797],{},"MySQL 作为主数据源，ES 作为搜索索引。索引更新通过异步任务同步，失败可以重试或定时重建。查询时如果 ES 不可用，可以降级到 MySQL 模糊查询。",[147,1799,1801],{"id":1800},"q9预签名-url-有什么好处","Q9：预签名 URL 有什么好处？",[10,1803,1804],{},"预签名 URL 可以让客户端下载直接走对象存储，减少应用服务器带宽压力。同时 URL 可以设置短期有效，兼顾性能和安全。",[147,1806,1808],{"id":1807},"q10ai-agent-接入文件系统怎么防越权","Q10：AI Agent 接入文件系统怎么防越权？",[10,1810,1811],{},"Agent 只能调用后端暴露的工具函数，所有工具函数都必须带 user_id 和权限校验。RAG 检索也必须加用户权限过滤，下载链接只能由后端鉴权后生成。",[26,1813,1814],{"id":1814},"总结",[10,1816,1817],{},"CloudVault 这个项目的价值在于它覆盖了网盘系统的核心工程问题：对象存储与元数据解耦、大文件分片上传、断点续传、Hash 秒传、分布式锁、异步任务、失败重试、DLQ、缓存一致性、全文搜索、预签名下载链接以及 AI 文件查询扩展。",[10,1819,1820],{},"如果只是从功能看，它是一个云盘；但如果从架构看，它是一个很适合展示后端工程能力的综合项目。它能体现你对 Go 后端、对象存储、MySQL、Redis、RabbitMQ、Elasticsearch、高并发一致性和 AI Agent 工程化的理解。",[1822,1823,1824],"style",{},"html pre.shiki code .ssh_m, html code.shiki .ssh_m{--shiki-default:#ADBAC7;--shiki-light:#24292E}html pre.shiki code .sJK54, html code.shiki .sJK54{--shiki-default:#8DDB8C;--shiki-light:#005CC5}html pre.shiki code .sXfbr, html code.shiki .sXfbr{--shiki-default:#96D0FF;--shiki-light:#032F62}html pre.shiki code .swcJU, html code.shiki .swcJU{--shiki-default:#6CB6FF;--shiki-light:#005CC5}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html.light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}",{"title":107,"searchDepth":444,"depth":461,"links":1826},[1827,1828,1829,1830,1835,1842,1848,1853,1858,1862,1868,1874,1875,1880,1881,1882,1883,1888,1893,1900,1908,1915,1916,1917,1929],{"id":28,"depth":444,"text":28},{"id":48,"depth":444,"text":48},{"id":96,"depth":444,"text":96},{"id":141,"depth":444,"text":142,"children":1831},[1832,1833,1834],{"id":149,"depth":461,"text":149},{"id":169,"depth":461,"text":170},{"id":193,"depth":461,"text":194},{"id":218,"depth":444,"text":218,"children":1836},[1837,1838,1839,1840,1841],{"id":224,"depth":461,"text":224},{"id":236,"depth":461,"text":236},{"id":274,"depth":461,"text":274},{"id":298,"depth":461,"text":298},{"id":310,"depth":461,"text":310},{"id":325,"depth":444,"text":325,"children":1843},[1844,1845,1846,1847],{"id":331,"depth":461,"text":331},{"id":346,"depth":461,"text":346},{"id":364,"depth":461,"text":364},{"id":402,"depth":461,"text":402},{"id":553,"depth":444,"text":553,"children":1849},[1850,1851,1852],{"id":559,"depth":461,"text":559},{"id":600,"depth":461,"text":600},{"id":609,"depth":461,"text":609},{"id":644,"depth":444,"text":644,"children":1854},[1855,1856,1857],{"id":650,"depth":461,"text":650},{"id":665,"depth":461,"text":666},{"id":695,"depth":461,"text":695},{"id":724,"depth":444,"text":725,"children":1859},[1860,1861],{"id":731,"depth":461,"text":731},{"id":751,"depth":461,"text":751},{"id":766,"depth":444,"text":767,"children":1863},[1864,1865,1866,1867],{"id":773,"depth":461,"text":773},{"id":799,"depth":461,"text":799},{"id":817,"depth":461,"text":817},{"id":849,"depth":461,"text":850},{"id":873,"depth":444,"text":874,"children":1869},[1870,1871,1872,1873],{"id":880,"depth":461,"text":880},{"id":895,"depth":461,"text":895},{"id":910,"depth":461,"text":910},{"id":922,"depth":461,"text":922},{"id":937,"depth":444,"text":937},{"id":978,"depth":444,"text":979,"children":1876},[1877,1878,1879],{"id":985,"depth":461,"text":985},{"id":1128,"depth":461,"text":1128},{"id":1143,"depth":461,"text":1144},{"id":1162,"depth":444,"text":1162},{"id":1191,"depth":444,"text":1191},{"id":1215,"depth":444,"text":1215},{"id":1259,"depth":444,"text":1259,"children":1884},[1885,1886,1887],{"id":1265,"depth":461,"text":1265},{"id":1303,"depth":461,"text":1303},{"id":1326,"depth":461,"text":1327},{"id":1345,"depth":444,"text":1346,"children":1889},[1890,1891,1892],{"id":1355,"depth":461,"text":1356},{"id":1380,"depth":461,"text":1381},{"id":1405,"depth":461,"text":1406},{"id":1432,"depth":444,"text":1432,"children":1894},[1895,1896,1897,1898,1899],{"id":1438,"depth":461,"text":1438},{"id":1455,"depth":461,"text":1455},{"id":1472,"depth":461,"text":1472},{"id":1492,"depth":461,"text":1492},{"id":1509,"depth":461,"text":1509},{"id":1523,"depth":444,"text":1523,"children":1901},[1902,1903,1904,1905,1906,1907],{"id":1529,"depth":461,"text":1529},{"id":1538,"depth":461,"text":1538},{"id":1547,"depth":461,"text":1547},{"id":1556,"depth":461,"text":1556},{"id":1565,"depth":461,"text":1565},{"id":1574,"depth":461,"text":1575},{"id":1584,"depth":444,"text":1584,"children":1909},[1910,1911,1912,1913,1914],{"id":1590,"depth":461,"text":1590},{"id":1616,"depth":461,"text":1616},{"id":1635,"depth":461,"text":1635},{"id":1663,"depth":461,"text":1663},{"id":1688,"depth":461,"text":1689},{"id":1714,"depth":444,"text":1714},{"id":1725,"depth":444,"text":1726},{"id":1737,"depth":444,"text":1737,"children":1918},[1919,1920,1921,1922,1923,1924,1925,1926,1927,1928],{"id":1740,"depth":461,"text":1741},{"id":1747,"depth":461,"text":1748},{"id":1754,"depth":461,"text":1755},{"id":1761,"depth":461,"text":1762},{"id":1772,"depth":461,"text":1773},{"id":1779,"depth":461,"text":1780},{"id":1786,"depth":461,"text":1787},{"id":1793,"depth":461,"text":1794},{"id":1800,"depth":461,"text":1801},{"id":1807,"depth":461,"text":1808},{"id":1814,"depth":444,"text":1814},[1931],"技术","2026-05-10 10:30:00",false,"md",{},true,"\u002Fposts\u002Fcloudvault-go-cloud-storage-system",{"title":5,"description":12},"posts\u002Fcloudvault-go-cloud-storage-system",[1941,1942,1943,1944,1945,1946,1947,1948],"Go","云存储","网盘系统","分布式系统","项目架构","Redis","RabbitMQ","Elasticsearch","nwuxubtpP3RzswSVDzXEjPwJL0itHzRRMho1mKD5uRc",[1951,1964,1976,1982,1995,2005,2014,2024,2035,2044,2054,2066,2076,2079,2088,2098,2111,2122,2132,2140,2151,2157,2163,2169,2177,2186,2194,2200,2208,2216],{"slug":1952,"path":1953,"title":1954,"date":1955,"tags":1956,"description":107,"draft":1933,"hidden":1933,"published":1936,"readingTime":1096},"multimodal-rag-from-scratch","\u002Fposts\u002Fmultimodal-rag-from-scratch","从零实现多模态 RAG：BM25、Dense 检索、RRF 融合、MMR 重排全部手写","2026-06-30 18:00:00",[1957,1958,1959,1960,1961,1962,1963],"RAG","多模态","AI Infra","BM25","向量检索","混合检索","实习求职",{"slug":1965,"path":1966,"title":1967,"date":1968,"tags":1969,"description":107,"draft":1933,"hidden":1933,"published":1936,"readingTime":1120},"mini-llm-engine-deep-dive","\u002Fposts\u002Fmini-llm-engine-deep-dive","讲透 mini-llm-engine：从显存碎片到六大推理优化","2026-06-30 14:00:00",[1970,1959,1971,1972,1973,1974,1975,1963],"LLM","vLLM","PagedAttention","KV Cache","推理优化","投机解码",{"slug":1977,"path":1978,"title":1979,"date":1980,"tags":1981,"description":107,"draft":1933,"hidden":1933,"published":1936,"readingTime":1109},"mini-llm-engine-from-scratch","\u002Fposts\u002Fmini-llm-engine-from-scratch","从零实现 LLM 推理引擎：深挖 vLLM 的六大核心优化","2026-06-30 10:00:00",[1970,1959,1971,1972,1974,1975,1963],{"slug":1983,"path":1984,"title":1985,"date":1986,"tags":1987,"description":1993,"draft":1933,"hidden":1933,"published":1936,"readingTime":1994},"bytedance-recommendation-architecture-intern-interview","\u002Fposts\u002Fbytedance-recommendation-architecture-intern-interview","字节推荐架构实习生 Data 面试准备：推荐系统、实时特征与高并发八股","2026-06-09",[1988,1989,1990,1991,1992],"面试","推荐系统","后端架构","实时计算","字节跳动","面向字节跳动推荐架构团队实习岗位的八股准备清单，覆盖推荐系统链路、实时数据、特征服务、高并发后端、分布式系统与项目包装。",18,{"slug":1996,"path":1997,"title":1998,"date":1986,"tags":1999,"description":2003,"draft":1933,"hidden":1933,"published":1936,"readingTime":2004},"high-concurrency-rate-limiting-algorithms","\u002Fposts\u002Fhigh-concurrency-rate-limiting-algorithms","高并发限流算法：固定窗口、滑动窗口、漏桶与令牌桶",[2000,2001,1990,2002,1988],"高并发","限流","系统设计","面试和工程都常见的限流算法总结，讲清楚固定窗口、滑动窗口、漏桶、令牌桶、并发数限制以及分布式限流如何落地。",12,{"slug":2006,"path":2007,"title":2008,"date":1986,"tags":2009,"description":2013,"draft":1933,"hidden":1933,"published":1936,"readingTime":1083},"kafka-producer-broker-consumer","\u002Fposts\u002Fkafka-producer-broker-consumer","Kafka 入门：生产者、Broker、消费者和“消费”到底是什么意思",[2010,2011,2012,1988,1989],"Kafka","消息队列","后端","用推荐系统里的用户行为日志为例，讲清楚 Kafka 的作用、Producer、Broker、Consumer、Topic、Partition、Offset 和消费语义。",{"slug":2015,"path":2016,"title":2017,"date":1986,"tags":2018,"description":2023,"draft":1933,"hidden":1933,"published":1936,"readingTime":540},"leetcode-lru-merge-k-reverse-list","\u002Fposts\u002Fleetcode-lru-merge-k-reverse-list","链表与缓存高频题：LRU Cache、合并 K 个有序链表、反转链表",[2019,2020,2021,2022,1988],"算法","链表","LRU","LeetCode","面试高频算法题速记，整理 LRU Cache、合并 K 个有序链表、反转链表的核心思路、复杂度和 C++ 代码。",{"slug":2025,"path":2026,"title":2027,"date":2028,"tags":2029,"description":107,"draft":1933,"hidden":1933,"published":1936,"readingTime":2034},"meddococr-interpreter-source-analysis","\u002Fposts\u002Fmeddococr-interpreter-source-analysis","MedDocOCR-Interpreter 源码导读：医疗文档 OCR、结构化抽取与报告解读原型","2026-05-22 10:30:00",[2030,1958,2031,1957,2032,2033],"OCR","医疗 AI","Python","源码分析",16,{"slug":2036,"path":2037,"title":2038,"date":2039,"tags":2040,"description":107,"draft":1933,"hidden":1933,"published":1936,"readingTime":1109},"cet6-writing-model-essays","\u002Fposts\u002Fcet6-writing-model-essays","六级写作范文背诵包：10 个高频话题","2026-05-20 09:00:00",[2041,2042,2043],"English","CET6","Writing",{"slug":2045,"path":2046,"title":2047,"date":2048,"tags":2049,"description":107,"draft":1933,"hidden":1933,"published":1936,"readingTime":2053},"claude-code-context-management","\u002Fposts\u002Fclaude-code-context-management","Claude Code 上下文管理机制：从 Microcompact 到 Auto Compact","2026-05-19 10:00:00",[2050,2051,1970,1959,2052],"Claude Code","Agent","上下文工程",27,{"slug":2055,"path":2056,"title":2057,"date":2058,"tags":2059,"description":107,"draft":1933,"hidden":1933,"published":1936,"readingTime":2065},"nanotron-llm-pretraining-framework-analysis","\u002Fposts\u002Fnanotron-llm-pretraining-framework-analysis","Nanotron 项目详解：Hugging Face 的大模型预训练框架怎么做分布式训练","2026-05-10 12:10:00",[1970,2060,2061,2062,2063,1959,2064],"大模型训练","分布式训练","Nanotron","Hugging Face","PyTorch",19,{"slug":2067,"path":2068,"title":2069,"date":2070,"tags":2071,"description":107,"draft":1933,"hidden":1933,"published":1936,"readingTime":2075},"gofoundry-go-backend-foundation-framework","\u002Fposts\u002Fgofoundry-go-backend-foundation-framework","GoFoundry 项目详解：基于 Go 的后端基础框架套件设计","2026-05-10 11:20:00",[1941,2072,2073,2074,910,2011,1945],"后端框架","ORM","分布式缓存",25,{"slug":2077,"path":1937,"title":5,"date":1932,"tags":2078,"description":107,"draft":1933,"hidden":1933,"published":1936,"readingTime":2053},"cloudvault-go-cloud-storage-system",[1941,1942,1943,1944,1945,1946,1947,1948],{"slug":2080,"path":2081,"title":2082,"date":2083,"tags":2084,"description":107,"draft":1933,"hidden":1933,"published":1936,"readingTime":2087},"openclaw-source-code-analysis","\u002Fposts\u002Fopenclaw-source-code-analysis","OpenClaw 源码导读：个人 AI 助手的网关、通道、插件与运行时架构","2026-05-08 16:30:00",[2085,2051,1959,2086,2033],"OpenClaw","TypeScript",20,{"slug":2089,"path":2090,"title":2091,"date":2092,"tags":2093,"description":107,"draft":1933,"hidden":1933,"published":1936,"readingTime":1109},"flow-matching-generative-models","\u002Fposts\u002Fflow-matching-generative-models","Flow Matching：从噪声到数据的连续流生成模型","2026-05-07 00:00:00",[2094,2095,2096,2097],"生成模型","Diffusion","Flow Matching","深度学习",{"slug":2099,"path":2100,"title":2101,"date":2102,"tags":2103,"description":107,"draft":1933,"hidden":1933,"published":1936,"readingTime":2110},"database-ai-week4","\u002Fposts\u002Fdatabase-ai-week4","Week 4：数据库速成——从 Storage、Index、Query Optimization 到 Vector DB 与 RAG","2026-05-05 12:00:00",[2104,2105,2106,1957,2107,2108,2109],"数据库","CMU 15-445","Vector DB","LLM Memory","Query Optimization","Caching",15,{"slug":2112,"path":2113,"title":2114,"date":2115,"tags":2116,"description":107,"draft":1933,"hidden":1933,"published":1936,"readingTime":2034},"distributed-systems-week3","\u002Fposts\u002Fdistributed-systems-week3","Week 3：分布式系统速成——MapReduce、Raft、容错与 Distributed KV Store","2026-05-05 11:00:00",[1944,2117,2118,2119,2120,2121,2051],"MIT 6.824","MapReduce","Raft","KV Store","Ray",{"slug":2123,"path":2124,"title":2125,"date":2126,"tags":2127,"description":107,"draft":1933,"hidden":1933,"published":1936,"readingTime":2110},"gpu-inference-acceleration-week2","\u002Fposts\u002Fgpu-inference-acceleration-week2","Week 2：GPU 与推理加速——从 Kernel、算子融合到 LLM Serving","2026-05-05 10:00:00",[2097,2128,2129,1970,2130,1971,2131],"GPU","推理加速","CMU 10-414","TensorRT",{"slug":2133,"path":2134,"title":2135,"date":2136,"tags":2137,"description":107,"draft":1933,"hidden":1933,"published":1936,"readingTime":1994},"dl-framework-autograd-mini","\u002Fposts\u002Fdl-framework-autograd-mini","Week 1：DL 框架与 Autograd——从计算图、反向传播到 Mini Autograd 实现","2026-05-05 09:00:00",[2097,2138,2064,2130,2139],"Autograd","Mini Framework",{"slug":2141,"path":2142,"title":2143,"date":2144,"tags":2145,"description":107,"draft":1933,"hidden":1933,"published":1936,"readingTime":2004},"lock-free-concurrency-notes","\u002Fposts\u002Flock-free-concurrency-notes","无锁并发入门：从 CAS 到 Atomic Ring Buffer","2026-04-25",[2146,2147,2148,2149,2150],"C++","并发","无锁编程","性能优化","量化开发",{"slug":2152,"path":2153,"title":2154,"date":2155,"tags":2156,"description":107,"draft":1933,"hidden":1933,"published":1936,"readingTime":511},"agent-memory","\u002Fposts\u002Fagent-memory","Agent 对话记忆化：从原理到实现","2026-04-24",[1970,2051,1957,1988],{"slug":2158,"path":2159,"title":2160,"date":2155,"tags":2161,"description":107,"draft":1933,"hidden":1933,"published":1936,"readingTime":540},"llm-context-compression","\u002Fposts\u002Fllm-context-compression","LLM 上下文五层压缩机制详解",[1970,2051,2162,1988],"上下文压缩",{"slug":2164,"path":2165,"title":2166,"date":2167,"tags":2168,"description":107,"draft":1933,"hidden":1933,"published":1936,"readingTime":1096},"cpp-concurrency-basics","\u002Fposts\u002Fcpp-concurrency-basics","C++ 并发编程入门：从数据竞争到线程池","2026-04-15",[2146,2147,1988,2150],{"slug":2170,"path":2171,"title":2172,"date":2173,"tags":2174,"description":107,"draft":1933,"hidden":1933,"published":1936,"readingTime":437},"travel-in-shenzhen","\u002Fposts\u002Ftravel-in-shenzhen","XCPC 深圳游记","2026-04-13",[2175,2146,2176],"XCPC","比赛",{"slug":2178,"path":2179,"title":2180,"date":2181,"tags":2182,"description":107,"draft":1933,"hidden":1933,"published":1936,"readingTime":1060},"backend-stack-deep-dive","\u002Fposts\u002Fbackend-stack-deep-dive","后端五件套：FastAPI \u002F Node.js \u002F SQLAlchemy async \u002F PostgreSQL \u002F Docker 面试速通","2026-04-07",[2012,2183,2184,2185,1988],"FastAPI","PostgreSQL","Docker",{"slug":2187,"path":2188,"title":2189,"date":2190,"tags":2191,"description":107,"draft":1933,"hidden":1933,"published":1936,"readingTime":540},"deepscientist-tech-stack","\u002Fposts\u002Fdeepscientist-tech-stack","DeepScientist 技术栈全解析：一个 AI 科研平台的架构设计","2026-04-06",[2192,2183,2193,2184,1988],"全栈","Next.js",{"slug":2195,"path":2196,"title":2197,"date":2190,"tags":2198,"description":107,"draft":1933,"hidden":1933,"published":1936,"readingTime":1083},"minicode-source-analysis","\u002Fposts\u002Fminicode-source-analysis","MiniCode 源码解析：用 5000 行 TypeScript 实现一个 AI 编程助手",[2086,2199,1970,2033,1988],"CLI",{"slug":2201,"path":2202,"title":2203,"date":2190,"tags":2204,"description":107,"draft":1933,"hidden":1933,"published":1936,"readingTime":511},"nova-theme-implementation","\u002Fposts\u002Fnova-theme-implementation","我是怎么从零实现 Nova 主题的",[2205,2206,2207],"Hexo","前端","开源",{"slug":2209,"path":2210,"title":2211,"date":2212,"tags":2213,"description":107,"draft":1933,"hidden":1933,"published":1936,"readingTime":444},"git-cheatsheet","\u002Fposts\u002Fgit-cheatsheet","Git 常用操作备忘","2026-04-05 14:00:00",[2214,2215],"Git","工具",{"slug":2217,"path":2218,"title":2219,"date":2220,"tags":2221,"description":107,"draft":1933,"hidden":1933,"published":1936,"readingTime":461},"github-actions-intro","\u002Fposts\u002Fgithub-actions-intro","GitHub Actions 入门：自动化你的工作流","2026-04-04 09:00:00",[2222,2223,2224],"GitHub Actions","CI\u002FCD","自动化",1782796011452]