惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

H
Help Net Security
T
ThreatConnect
SecWiki News
SecWiki News
F
Future of Privacy Forum
AWS News Blog
AWS News Blog
C
Cisco Blogs
A
Arctic Wolf
Vercel News
Vercel News
The GitHub Blog
The GitHub Blog
Scott Helme
Scott Helme
V
V2EX
博客园 - 叶小钗
阮一峰的网络日志
阮一峰的网络日志
K
Kaspersky official blog
G
Google Developers Blog
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
P
Privacy International News Feed
C
Cyber Attacks, Cyber Crime and Cyber Security
N
News | PayPal Newsroom
Schneier on Security
Schneier on Security
NISL@THU
NISL@THU
Microsoft Azure Blog
Microsoft Azure Blog
量子位
The Hacker News
The Hacker News
Stack Overflow Blog
Stack Overflow Blog
Security Latest
Security Latest
M
Microsoft Research Blog - Microsoft Research
Google Online Security Blog
Google Online Security Blog
博客园_首页
C
CXSECURITY Database RSS Feed - CXSecurity.com
I
InfoQ
Google DeepMind News
Google DeepMind News
Y
Y Combinator Blog
The Cloudflare Blog
Microsoft Security Blog
Microsoft Security Blog
Martin Fowler
Martin Fowler
Cisco Talos Blog
Cisco Talos Blog
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
T
Troy Hunt's Blog
F
Fox-IT International blog
S
Security @ Cisco Blogs
博客园 - 司徒正美
cs.CV updates on arXiv.org
cs.CV updates on arXiv.org
C
Comments on: Blog
Threat Intelligence Blog | Flashpoint
Threat Intelligence Blog | Flashpoint
L
LINUX DO - 最新话题
GbyAI
GbyAI
Project Zero
Project Zero
腾讯CDC
T
Tailwind CSS Blog

博客园_首页

华为公司发布半导体演进新范式 - “韬(τ)定律”(Tau Law) Linux时区修改为CST Go 语言入门学习笔记基础版 给热水器装上“电量显示”:用 Shelly Gen4 脚本实现零改装水量预测 踩坑实录:接口正常Feign调用字段值为空 耿同学学术打假,就是学术版《狂人日记》;学术打假,就是清扫垃圾 浙江事业编笔试上周出分!面试进入倒计时,该如何高效冲刺 - 里奥不吃奥利奥 FastApiAdmin 后端接口开发好了,前端管理界面怎么调用与显示? 我写了 50 个 Claude Code Skill 才发现,前 30 个都白写了 告别 "cd /var/log" !用 journalctl 统一掌控 Linux 日志 我用自己的微信聊天记录,微调了一个“数字分身” AI运动APP开发的常见问题集锦一 复盘梳理-如何深入并抽象 告别手动复制!公众号文章批量导出工具,极致提升内容运营效率 【学习笔记】《Python编程 从入门到实践》第2章:变量命名规则、字符串操作与数值类型详解 Docker--Docker简介及系统架构 别再瞎搞 AI 了!大厂AI业务落地的五个关键环节!(建议新手直接照搬) [MAF的Agent管道详解-01]塑智能体边界,从AIAgent抽象类开始 平台智能化到了分水岭:为什么配置代码化才是 AI Coding 的下一代接口 P.4文本统计工具 高光谱拼接算法(二)Harris 角点探测 - 哥布林学者 Claude Code “悄悄”装了 Python 包?别再让它“投错胎”了 - only赟 影刀 vs 八爪鱼 RPA:到底选哪个?一篇讲透 AI Coding开始进入第四个时代,我还没上车呢! 完整学习LLM(四):Token是什么 【Agentic RL / 强化学习 / OPD】OpenClaw-RL 源码阅读笔记 --- (1)---基础 LitCTF2026web部分wp CAD子系统,是自研还是外包? 什么是教程地狱?5个信号说明你已经陷入(附3步摆脱方法) polygon出题教程 Manim物理模拟:别自己写欧拉了! AI 学习笔记:Agent 的应用演示 - 凌杰 分享一个CAN报文编辑器软件 洛谷P13016 [GESP202506 六级] 最大因数 MiniCPM-V 4.6 部署实战:基于 GPUStack 与 SGLang 的端侧多模态模型部署 用 FRP 打通云服务器与本地 Ubuntu,让 Codex 远程调试本地硬件 软考 - 架构设计师 知识点总结 给 FastApiAdmin 加个“会议纪要”模块,我把后端二次开发的坑踩了个遍 聊一聊 MES系统如何实现多种标签打印并支持不同打印机 2026第四届LitCTF网络安全挑战赛Pwn的wp 断尺问题:戴德金分割现实悖论 给句子做个“语义审计”:从词向量到句子向量的方法论 当AI“卡壳”在生产环境:MCP Server 如何帮我们破局 ofdkit-harmony 0.2.0 发布:鸿蒙原生 OFD 阅读库,已上架 ohpm 有了AI测试工具,还需要掌握Playwright、Pytest、Selenium这些框架吗? 组织转型实录——我把传统研发团队改成AI驱动,踩了无数坑 为什么 AI Coding 难进生产环境?深入了解 Everything-Claude-Code ! 到底 TMD 用哪个: npm, pnpm, Yarn, Bun, Deno? 傻瓜, 当然用 npm 啦 上周热点回顾(5.18-5.24) [对比学习LangChain和MAF-04]针对消息的设计 TrueAsync Server 为 PHP 带来了原生的高性能 HTTP 服务器 规则漂移 帆软市场部为什么能成为高人效增长系统? 22. LangChain LCEL,用 | 串联AI的魔法语言 - 老陈说编程 完整学习LLM(二):大模型到底是什么 洛谷-P11942 [KTSC 2025] 重塑矩阵 题解 哈哈哈哈哈打不过我吧,没有办法我(vllm)就是这么强大! Hermes Edu Skills 从 170 到 188:一次中文教育 Agent Skill Pack 的工程化升级 一个外行,半年搞定机械臂:我的从0到1踩坑实录 新写了个直播录制工具,可录制抖音快手斗鱼直播 15天学会AI应用开发(一)搭建AI大模型应用开发环境 Childhood,23款童年卡牌游戏复刻 Github Copilot配置GPT5.5报错:'temperature' does not support 0.1 with this model. Only the default (1) value is supported. - Eric zhou 单曲循环 ClassIn 在 Linux 下无法播放音频 把 TeXstudio / LaTeX 工程交给 AI:texstudio-mcp 功能详解 .NET 8 Web开发入门(六):Blazor 全栈开发——告别 JavaScript 焦虑 别让 LLM 写文件:一套 Agent 进度跟踪的工程化范式 - BurningFish Qt Bridges for C# 深度技术解析 Multus 多网卡方案:IPVLAN 模式 被流量逼出来的架构:从一台服务器到云原生的 17 次蜕变 —— 集群、缓存、MQ、微服务、Docker、K8S 的前世今生 Claude Code安装全流程 Windows保姆级教程 awk 命令练习(从入门到进阶) Java + Spring实现Hermes Agent之龙虾、Skills、Mcp和沙箱代码执行环境思路 轨迹的蓝图:方程求解与交点计算 Agent新技术分享-Forge论文已被ACM接受 PowerMem 记忆系统的遗忘设计,从神经元到代码工程 我用了FastApiAdmin后,连夜把踩过的坑都整理出来了 一个程序员眼中的 AI 核心概念,讲透 LLM 、Agent 、MCP 、Skill 、RAG... 网络安全在线就能打的内网靶场推荐 & Dawn Breaker 单域靶场 WP CTF 中如何用提示词发挥大模型的最大实力:从聊天助手到大手子 PyTorch KernelAgent 源码解读 ---(6)--- Composer 高光谱拼接算法(一)扫推式成像和航带拼接算法 一文看懂fofa常用语法,告别混淆,精准打击! 从零搭建量化投资系统:用 Qlib 一行代码搞定均线分析 企业 AI 落地,第一件事不是买模型,而是建好企业知识库 如何在Oracle Agent Factory中配置国内厂商的LLM? Codex 换模型太麻烦?这个开源桌面工具帮你一键切换 Avalonia中的动画 2026软考|十大管理超全通俗笔记,备考闭眼记! rv1126b内置phy接hub交换机芯片 React 可拖拽列宽 + 点击行选中 ProTable 封装笔记 五大实锤证据:AI不会终结低代码,只会倒逼技术进化 【硬核脑洞】16位实模式最后的疯狂:我们能否在 640KB 常规内存里手搓一个 MD 模拟器? 基于.Net的NetCoreKevin框架中AgentFramework实现AI智能体Skill和工具动态管理和加载 PostgreSQL 高可用集群 patroni 自动故障转移测试 自己使用C++开发的仿OpenClaw、Hermes智能体工具 记一次 .NET 某集群管理软件 内存暴涨分析 StarBlog番外(5) 从1.6到1.10,基于Avalonia AOT 开发的 Publisher 半年进化之路 Anthropic 把 SOC 误报率从 33% 砍到 7%,真正在干活的不是 Claude
做共享目录实时同步,踩过这些坑
LucaJu · 2026-05-26 · via 博客园_首页

我们之前的知识库平台,文档入口都在平台内:用户上传文件,系统解析内容,写入向量库,后面再拿来做 RAG。

后来客户提了一个很实际的诉求:他们已经有一套共享目录的使用习惯,不想每次都打开平台再上传一遍。最好是文件丢到共享目录里,系统自己发现、自己同步到知识库。

听起来像是“监听一个目录”这么简单,但真做起来,坑还挺多。

一开始为什么没用轮询

最直观的方案当然是定时扫目录。

比如每 5 分钟把共享目录完整扫一遍,和上一次的文件列表做 diff:新增的同步,删除的删除,变更的重新解析。

这个方案实现成本低,也容易兜底。但我们没有直接把它当成主链路,主要是几个问题:

  • 延迟不好控制:5 分钟扫一次,用户上传后平均要等 2 分半才能被发现;扫得更频繁,IO 压力又上来了。
  • 目录大了以后不划算:共享目录下文件一多,每次全量遍历都挺浪费。
  • 修改判断不够稳:只看文件大小容易漏,只看修改时间又容易受文件系统和网络盘行为影响。

所以最后的思路是:实时监听做主链路,定时扫描做补偿。

实时监听这块,用的是 java.nio.file.WatchService

WatchService 基本够用,但别只看 Demo

WatchService 是 Java 对操作系统文件事件通知的一层封装。Linux 下通常走 inotify,Windows 下是 ReadDirectoryChangesW。

最简单的写法确实很短:

WatchService watchService = FileSystems.getDefault().newWatchService();
Path dir = Paths.get("/share");
dir.register(watchService, ENTRY_CREATE, ENTRY_MODIFY, ENTRY_DELETE);

然后单独起一个线程阻塞等事件:

WatchKey key = watchService.take();
for (WatchEvent<?> event : key.pollEvents()) {
    // 处理事件
}
key.reset();

这里有个小细节:key.reset() 不能忘。忘了以后,这个目录后续事件就收不到了。

但如果只按这个 Demo 写,放到共享目录同步这种场景里,很快就会踩坑。

坑一:它不会递归监听子目录

这是我们第一版最容易想当然的地方。

dir.register(...) 只监听当前这一层目录。比如你监听了 /share,那 /share/A 里面发生了文件变更,默认是收不到的。

而我们的目录结构大概是:

/share/知识库名/文档
/share/知识库名/子目录/文档

层级不固定,用户还可能随时新建目录。所以启动时必须先把整棵目录树走一遍,把每个子目录都注册上。

Files.walkFileTree(root, EnumSet.of(FileVisitOption.FOLLOW_LINKS), Integer.MAX_VALUE,
    new SimpleFileVisitor<>() {
        @Override
        public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) throws IOException {
            register(dir);
            return FileVisitResult.CONTINUE;
        }
    });

还不够。

运行过程中如果用户新建了目录,也要在收到 ENTRY_CREATE 后马上补注册:

if (kind == ENTRY_CREATE && Files.isDirectory(child)) {
    registerAll(child);
}

这一步如果漏了,系统看起来还在正常监听,但新目录下面的文件其实已经静悄悄地漏掉了。

坑二:新目录注册得再快,也可能漏文件

这个问题更隐蔽一点。

假设用户不是一个文件一个文件上传,而是直接 cp -r 拷贝一整个目录进来。操作系统可能先告诉你“新建了目录”,然后目录里的文件事件很快就出来了。

理想顺序是:

1. 收到新目录 /share/A/B
2. 给 /share/A/B 注册监听
3. 收到 B 目录下 file1.txt、file2.txt 的创建事件

但实际情况可能是:

1. 收到新目录 /share/A/B
2. 还没来得及注册监听
3. file1.txt、file2.txt 的事件已经发生了

这时文件就漏了。

我们的处理比较朴素:发现新目录后,先注册整棵子树,然后主动扫一遍这个新目录,把里面已经存在的文件按“创建事件”补发出去。

if (Files.isDirectory(child)) {
    registerAll(child);
    emitCreateForExistingFiles(child);
}
private void emitCreateForExistingFiles(Path dir) {
    try (Stream<Path> pathStream = Files.walk(dir)) {
        pathStream
            .filter(Files::isRegularFile)
            .forEach(eventHandler::onCreate);
    }
}

这个逻辑看起来有点重复,但在目录批量拷贝场景下很有用。我们后来也接受了它可能带来的重复事件,因为下游会做幂等。

坑三:MODIFY 事件会比想象中多

文件写入不是一个瞬间完成的动作。

尤其是大文件、网络共享目录、Office 文档这类场景,系统可能连续抛出很多 MODIFY 事件。如果每个事件都触发一次解析和上传,资源会被打爆,处理结果也不稳定。

所以事件进来以后,我们没有立刻执行重活,而是先做了一层防抖。

String debounceKey = eventType + "|" + fullPath;
long now = System.currentTimeMillis();
Long last = debounceMap.get(debounceKey);
if (last != null && now - last < windowMillis) {
    return;
}
debounceMap.put(debounceKey, now);

这里的 key 用的是“事件类型 + 文件路径”。这样同一个文件的 CREATEMODIFY 不会互相覆盖。

实际工程里,我们还会等文件稳定一小段时间再处理。否则有些文件刚创建出来,还没写完,解析线程就冲上去了。

坑四:应用重启期间一定会丢事件

WatchService 只负责运行时监听。应用停了,事件就没了,不会帮你补。

这件事在本地测试时不明显,但生产环境一定要考虑:发布重启、机器重启、服务异常退出,这些时间窗口里用户仍然可能往共享目录里放文件。

所以我们加了一套快照比对,专门做补偿。

补偿扫描时,会记录当前目录下文件的这些信息:

  • 文件路径
  • 文件大小
  • 最后修改时间
  • 文件 hash

这些快照存在 MySQL,而不是本地文件。当前虽然是单实例,但后面如果做集群,状态至少不是绑死在某台机器上。

下一次扫描时,把当前快照和上一次快照做 diff:

  • 新出现的文件,补发 CREATE
  • 大小、修改时间或 hash 变化的文件,补发 MODIFY
  • 快照里有、当前目录没有的文件,补发 DELETE

补偿扫描有两个触发点:

  • 服务启动后立刻跑一次,补重启期间的事件
  • 定时跑一次,默认 5 分钟,兜底处理监听漏掉的情况
@Scheduled(fixedDelayString = "${doc.share-sync.reconcile-delay-ms:300000}")
public void scheduledReconcile() {
    reconcile("scheduled");
}

这也是为什么前面说,我们不是完全不用轮询,而是不把轮询当实时链路。

最后拆成了几个组件

为了后面好排查问题,我们没有把监听、解析、补偿都塞进一个类里,而是拆成了几个比较明确的组件。

组件 职责
ShareDirWatcher 基于 WatchService 做实时监听,负责递归注册子目录
ShareFileEventHandler 做事件防抖、路径解析,以及把文件事件转换成业务处理
ShareDirReconcileJob 做停机补偿和定时快照比对
ShareFileSnapshotStore 把文件快照持久化到 MySQL

配置上也留了开关,方便分阶段上线:

doc:
  share-sync:
    enabled: true
    dry-run: true
    root-dir: /share
    settle-seconds: 5
    reconcile-delay-ms: 300000

第一阶段我们只开 dry-run,先把日志打全:监听到了什么文件、解析出了哪个知识库、会触发什么动作。路径解析和去重逻辑都确认没问题后,再打开真实写入。

还有几个容易忽略的小点

OVERFLOW 事件

WatchService 的事件队列满了以后,可能会收到 OVERFLOW。这说明中间已经有事件丢了。我们的处理是记录日志,然后依赖下一轮补偿扫描修复。

删除事件只能拿到路径,别指望再判断类型

收到 ENTRY_DELETE 时,文件已经没了。你能拿到的通常只是相对路径,不能再通过 Files.isDirectory 判断它之前是文件还是目录。

所以删除事件统一交给下游处理,由数据库里的历史记录判断之前是什么。

重复事件比漏事件更容易接受

监听程序和补偿程序可能同时发现同一个新文件,重复上传是有可能的。这个问题不能只靠内存去重解决,最后还是要靠数据库唯一索引和业务幂等兜底。

在这类同步任务里,我更愿意接受“重复发现一次”,也不愿意悄悄漏掉一个文件。

小结

这次做下来,我对 WatchService 的定位更清楚了:它适合做实时感知,但不能单独承担“可靠同步”的全部责任。

真正能上线的方案,至少要补上这几块:

  • 递归注册子目录
  • 新目录创建后的兜底扫描
  • 文件事件防抖和稳定等待
  • 重启后的快照比对补偿
  • 下游幂等和唯一索引兜底

最后我们的方案是:监听负责快,补偿负责稳,数据库负责最终一致性。

这套设计不算复杂,但每一块都少不了。尤其是共享目录这种场景,用户怎么拷文件、网络盘怎么抛事件、服务什么时候重启,都不是代码能完全控制的。能做的就是把主链路和兜底链路都设计清楚,先 dry-run 跑一段时间,再逐步放开真实写入。