Java中如何等待FTP服务器文件完全下载完成后再读取处理
问题核心原因
两个调度线程池未对文件写入状态做协同标识,线程池2无法区分pending目录内的文件是「写入中未完成」还是「写入完成可处理」,直接读取并移动半写文件,最终导致数据截断丢失。
可落地方案
方案1:临时后缀标记(改动成本最低)
- 实现逻辑:
- 线程池1下载文件时,写入过程统一给文件加临时后缀,例如下载目标为
order_20240601.csv时,写入中的文件命名为order_20240601.csv.downloading - 等文件完全写入、IO流正常关闭、本地文件大小与FTP服务器记录的源文件大小校验一致后,执行同目录下的原子重命名,去掉
.downloading后缀转为正式待处理文件 - 线程池2扫描pending目录时,直接过滤所有带
.downloading后缀的文件,仅处理命名符合正式规则的文件
- 线程池1下载文件时,写入过程统一给文件加临时后缀,例如下载目标为
- 优势:不需要引入额外组件,文件系统级标记天然持久化,服务重启也不会出现状态错乱;同分区下文件重命名为原子操作,不会出现中间状态。
- 注意点:必须加文件大小校验步骤,避免网络闪断、下载线程异常退出时,半写文件被误改名。
方案2:目录隔离拆分(生产环境首选)
- 实现逻辑:
- 新增独立的
downloading临时目录,线程池1所有下载中的文件全部写入该目录,文件名可搭配临时后缀做双重标记 - 文件下载完成并通过完整性校验后,通过原子移动操作将文件从
downloading目录转移到pending目录 - 线程池2仅扫描
pending目录下的文件,完全不接触下载流程中的文件
- 新增独立的
- 优势:两个线程池的操作目录完全隔离,从流程上杜绝了读取半写文件的可能,后续做异常补偿、失败重试也更清晰:重启服务时只需要扫描
downloading目录的残留文件,判断是否需要重新下载即可。
方案3:文件锁+状态校验(适配无法改文件名/目录的场景)
- 实现逻辑:
- 线程池1写入文件时,持有当前文件的操作系统级排他锁(例如Java生态使用
FileLock实现),写入完成关闭流时自动释放锁 - 线程池2扫描到文件后,先尝试获取文件的共享锁,如果获取失败说明文件仍被写入线程占用,直接跳过等待下一个调度周期
- 加兜底校验:对成功获取锁的文件,间隔2秒两次读取文件大小,两次大小完全一致再做处理
- 线程池1写入文件时,持有当前文件的操作系统级排他锁(例如Java生态使用
- 注意点:不要使用JVM内存级锁,避免服务重启后锁状态失效;部署前需要验证存储介质对文件锁的支持,部分网络文件系统(NFS)的文件锁机制存在兼容性问题。
方案4:内存状态标记(适配低延迟处理场景)
- 实现逻辑:
- 维护线程安全的下载中文件集合(例如使用
ConcurrentHashMap存储文件名、下载开始时间等元数据),线程池1开始下载时将文件标记为下载中,完成校验后移除标记 - 线程池2扫描文件时,首先过滤掉集合内标记为下载中的文件
- 同样保留文件大小双次校验的兜底逻辑,避免服务重启导致内存标记丢失,误处理半写文件
- 维护线程安全的下载中文件集合(例如使用
- 优势:不需要改动文件命名和目录结构,文件下载完成后可以最快速度被线程池2感知,不需要等固定调度周期。
选型建议
优先选择目录隔离+临时后缀的组合方案,开发量极小,可靠性最高,对原有业务逻辑的侵入性最低,就算出现宕机、网络中断等异常,也可以通过扫描临时目录的残留文件做补偿,不会出现数据丢失问题。
内容的提问来源于stack exchange,提问作者Malav Mevada
相关产品推荐
相关产品推荐

