直接 json.dump() 到原文件不安全,因写入非原子:先清空再写入,崩溃会导致文件为空或截断而数据不可恢复;应改用临时文件+os.replace()原子替换,并确保同文件系统、落盘同步。

为什么直接 json.dump() 到原文件不安全?
因为写入过程不是原子的:先清空旧文件,再逐块写入新内容。若中途崩溃(如断电、程序被杀),文件会变成空或截断状态,原始数据彻底丢失。这不是“部分写入”,而是“不可恢复损坏”。
常见错误现象:OSError: [Errno 2] No such file or directory 或读取时 json.decoder.JSONDecodeError,尤其在多进程/多线程频繁写入时更明显。
核心思路是:先写到临时文件,再用 os.replace() 原子替换——该系统调用在 Linux/macOS 和 Windows(Python 3.3+)上均保证“要么全成功,要么原文件不变”。
推荐做法:用 tempfile.NamedTemporaryFile + os.replace
这是目前最稳妥、跨平台兼容性最好的方案。关键点在于:
立即学习“Python免费学习笔记(深入)”;
-
delete=False必须设为False,否则临时文件会被自动删掉,后续无法替换 - 临时文件必须和目标文件在同一个文件系统(同一磁盘分区),否则
os.replace()会退化为复制+删除,失去原子性 - 写入后要显式调用
.flush()和os.fsync(),确保数据真正落盘,避免缓存导致替换后仍读到旧内容
示例代码:
import json
import os
import tempfile
<p>def save_dict_as_json_atomic(data, filepath):
dirname = os.path.dirname(filepath) or '.'</p><h1>创建同目录下的临时文件,确保同文件系统</h1><pre class='brush:python;toolbar:false;'>with tempfile.NamedTemporaryFile(
mode='w',
dir=dirname,
delete=False,
suffix='.tmp'
) as f:
json.dump(data, f, ensure_ascii=False, indent=2)
f.flush()
os.fsync(f.fileno()) # 强制写入磁盘
temp_name = f.name
# 原子替换
os.replace(temp_name, filepath)注意 pathlib.Path.write_text() 不适合原子保存
虽然简洁,但 Path.write_text() 内部就是打开+写入+关闭,没有临时文件和原子替换逻辑,本质等价于直接 open(...).write(),完全不具备容错能力。
如果你看到有人用它保存配置或状态数据,且运行在不稳定环境(如树莓派、边缘设备、CI 临时机器),大概率已经丢过数据——只是还没被发现。
另外,json.dump() 的参数也要留心:
-
ensure_ascii=False:避免中文变\u4f60,除非你明确需要 ASCII-only 输出 -
indent=2:方便人工查看,但会增大文件体积;生产环境高频写入可考虑indent=None(默认) - 不要用
sort_keys=True除非真需要确定性输出,它会额外排序开销,且可能掩盖键顺序敏感的业务逻辑
并发写入时还要加锁吗?
如果多个进程/线程都调用上述原子函数写**同一个文件**,os.replace() 本身是原子的,但无法防止“后写入者覆盖前写入者”的逻辑冲突——比如 A 读旧数据→改→写,B 同时做同样操作,结果 B 的修改完全覆盖 A 的,A 的变更丢失。
这时候原子性只保“文件不损坏”,不保“业务一致性”。真正需要并发安全,得在应用层加逻辑:
- 用
threading.Lock(单进程内)或filelock.FileLock(跨进程)包裹整个“读→改→写”流程 - 或者改用带版本号或时间戳的乐观锁机制,写入前校验文件未被更新
- 更彻底的做法:别用文件存状态,换用 SQLite(支持 WAL 模式)或 Redis
文件系统的原子写只是基础防线,不是万能锁。很多人卡在这一步,以为用了 os.replace 就高枕无忧,结果上线后数据对不上,排查半天才发现是竞态没处理。


















