用大 JSON 文件做持久化:防抖写入与原子替换

项目初期为了快速验证想法,没有引入数据库,而是把配置和状态直接序列化到一个 JSON 文件。随着并发写入变多,开始出现文件损坏、数据丢失的情况。本文记录我如何用防抖队列加原子替换来保障写入安全。

1. 直接 writeFile 的数据损坏风险

最开始用的是 fs.writeFileSync(file, JSON.stringify(data))。一旦进程在写入中途被 kill,文件会被截断,变成无法解析的半拉 JSON。更隐蔽的是,某些情况下文件已经清空但新内容还没落盘,重启后整个数据集直接归零。

2. 防抖队列:合并 N 次写为 1 次落盘

高频写入场景下,每次都刷盘太伤磁盘。我实现了一个简单的防抖队列:把写请求先放到队列里,如果 300 毫秒内又有新请求,就重置计时器;计时器到期后只执行最后一次真正的落盘。这样可以把连续多次变更合并为一次写入,既降低 I/O 又避免竞态。

3. writeFileSync 何时才该用

同步写的好处是调用完成后数据一定在缓冲区,但会阻塞事件循环。我现在的策略是:只在进程退出前的 SIGTERM 处理里用同步写,确保优雅关闭时不丢数据;日常写仍然走异步队列,保持接口响应速度。

4. 临时文件 + fs.rename 的原子性

核心改进是把数据先写到临时文件 data.json.tmp,等写入完成后再调用 fs.rename(tmp, 'data.json')。POSIX 规范保证 rename 在同一文件系统内是原子的,因此读进程要么看到旧文件,要么看到新文件,绝不会读到半成品。

5. 崩溃恢复与备份策略

每次成功替换后,我把旧版本复制到 data.json.bak。启动时先校验 JSON 能否解析,不能解析就自动从备份恢复。配合定期快照,系统在最近一次崩溃后最多丢失 5 分钟内的变更,这对于早期原型来说已经足够。

← 返回首页