选用文件存储而非内存队列,因文件可持久化防进程崩溃或断电丢任务;需按优先级分文件、二进制序列化、原子重命名保障不重复不丢失;gob优于json;须校验磁盘空间与权限,避免静默失败。

为什么不用内存队列而选文件存储
因为进程崩溃或机器断电时,内存里的任务就丢了,而文件能持久化。但别直接用 os.WriteFile 往一个文件里追加任务——并发写会丢数据,且没法按优先级排序读取。
- 真正可用的方案是:每个优先级一个独立文件(如
queue_high.bin、queue_low.bin),用二进制格式序列化任务,配合os.O_APPEND | os.O_CREATE写入 - 读取时按优先级从高到低依次尝试打开对应文件,用
bufio.Scanner或encoding/binary.Read逐条解析,读完立即os.Remove整个文件(避免部分消费) - 注意:Linux 下同一文件被多个 goroutine 同时
os.OpenFile读没问题,但写必须串行;所以写操作要包在sync.Mutex或用 channel 序列化
如何保证任务不重复执行且不丢失
文件队列没有 Redis 的原子 RPOP,得靠“先移动再处理”来模拟。不能读出来就删,否则处理中途崩溃,任务就永远消失了。
- 写入时:任务写入
queue_high.bin,完成后调用os.Sync()确保落盘 - 消费时:先
os.Rename把queue_high.bin改成queue_high.bin.processing,再读取;成功处理完再os.Remove - 启动时扫描所有
*.processing文件,当作“中断残留任务”重新入队(或人工干预) - 别依赖文件名时间戳判断新旧——NFS 或容器挂载下可能不准;优先级靠文件名前缀控制,不是修改时间
gob 和 json 序列化选哪个
gob 是 Go 原生二进制格式,快、紧凑、支持私有字段;json 可读、跨语言,但慢、体积大、无法序列化 func 或 chan。
- 如果任务结构固定、只在 Go 服务内流转,无条件选
gob:用gob.NewEncoder(f).Encode(task)写,gob.NewDecoder(f).Decode(&task)读 - 用
json的唯一合理场景是:需要人工检查或调试队列内容,或未来要接入其他语言消费者——但这时得额外加版本字段,比如Version int `json:"v"`,不然结构一变就解码失败 - 无论哪种,都别把整个任务结构体指针写进文件;
gob对指针处理不稳定,json会忽略 nil 字段,导致反序列化后字段为空
磁盘满或权限错误时队列直接卡死怎么办
文件 I/O 错误不会自动重试,一旦 os.OpenFile 返回 syscall.ENOSPC 或 os.ErrPermission,后续所有写入都会失败,且不抛 panic,容易静默阻塞。
立即学习“go语言免费学习笔记(深入)”;
- 每次写入前加简单空间检查:
stat, _ := os.Stat("/path/to/queue"); if stat != nil && stat.Sys().(*syscall.Stat_t).Blocks*512 > 0.9*totalSpace { /* 告警并拒绝入队 */ } - 所有文件操作必须检查 error,遇到
os.IsNotExist就自动建目录,遇到os.IsPermission直接 log.Fatal,别吞掉 - 不要在 defer 里关文件句柄还同时做
os.Remove——万一Remove失败,文件没删干净,下次又撞上同名文件,可能覆盖或报错
最麻烦的其实是多实例竞争同一个队列目录:两个服务往同一组文件写,谁先 Rename 谁赢。真要分布式,就得加文件锁(syscall.Flock)或者换用 SQLite 做队列表——但那就不是“基于文件”了。










