用文件做任务队列是权宜之计,只适合单机、低频、无可靠性要求的场景;真要进程解耦,必须换Redis或RabbitMQ这类带持久化和原子操作的消息中间件。

直接说结论:用文件做任务队列是权宜之计,只适合单机、低频、无可靠性要求的场景;真要进程解耦,必须换 Redis 或 RabbitMQ 这类带持久化和原子操作的消息中间件。
为什么不能用普通文件当队列
文件系统不提供原子的“取一条并删除”语义。你读完一个任务文件后删它,中间若 Worker 崩溃,这个任务就永远丢失;若多个 Worker 同时扫描目录,可能重复消费同一文件。更麻烦的是,flock 在 NFS 或容器挂载卷上行为不可靠,rename 跨文件系统也不保证原子性。
- 常见错误现象:
open /tasks/task_abc.json: no such file or directory(被其他进程抢先删了)或任务被处理两次 - 性能影响:每秒扫百次目录 + stat 每个文件,I/O 开销远高于内存/网络队列
- 兼容性问题:Docker 容器里挂载的
/tmp或 hostPath 卷,inotify事件经常丢失,无法可靠监听新文件
如果非要用文件,至少做到这三点
不是推荐,而是“真要硬上,别踩最深的坑”:
- 所有任务文件写入前先
os.Rename(tmpfile, finalpath),确保写入完成才可见(依赖同文件系统) - Worker 读取后不直接
os.Remove,改名成task_abc.json.done,再异步清理;失败时可通过.done文件判断是否已处理 - 用单一协调进程管理任务生命周期,其他 Worker 只读不删——本质已退化为“伪队列”,不如直接用
channel+goroutine
真正解耦的关键在进程边界而非存储介质
进程解耦的核心是:Server 和 Worker 必须是独立可重启的二进制,通信不依赖共享内存或本地文件状态。文件路径本身不是解耦,只是把耦合从代码挪到了运维(比如 NFS 挂载点配置错一格,整个队列就断)。
初始化并管理阿里云SDK Go 客户端,涵盖 sync.Once 单例、goroutine 安全、endpoint 与 region 配置、VPC 端点及调试。
立即学习“go语言免费学习笔记(深入)”;
- 正确做法:Server 写
rdb.LPush(ctx, "upload_queue", payload),Worker 调rdb.BRPOP(ctx, "upload_queue", 0)—— Redis 的BRPOP天然阻塞+原子出队 - 参数差异:
BRPOP第二个参数设为0表示永久阻塞,比轮询文件目录省 99% CPU;设为5则 5 秒超时,方便做健康检查 - 容易忽略的点:Worker 进程启动时必须先
rdb.Exists(ctx, "upload_queue").Val() == 0才清空残留状态,否则旧任务会堆积
从文件队列迁移到 Redis 的最小改动
不用重写业务逻辑,只需替换队列接入层:
- 原文件写入逻辑:
ioutil.WriteFile("/tasks/"+id+".json", data, 0644)→ 改为rdb.Set(ctx, "task:"+id, data, 24*time.Hour)+rdb.LPush(ctx, "queue:pending", id) - 原文件扫描逻辑:
filepath.Glob("/tasks/*.json")→ 改为rdb.BRPOP(ctx, "queue:pending", 0),拿到id后再rdb.Get(ctx, "task:"+id) - 务必加
defer rdb.Del(ctx, "task:"+id)在处理成功后,避免任务卡住;失败则rdb.LPush(ctx, "queue:retry", id)
真正难的从来不是“怎么存”,而是“谁负责清理、谁负责重试、谁保证至少一次投递”——这些职责一旦落到文件系统头上,你就得自己实现 Redis 已经验证十年的逻辑。

















