直接用 Path.write_text() 覆盖状态文件时,进程若在写入中途退出,目标文件可能只剩半段内容。更稳妥的做法是先在目标文件所在目录写完临时文件,再用 os.replace() 一次替换。
环境:Python 3.11+;Linux、macOS 或 Windows;仅使用标准库。
创建 atomic_json.py:
import json
import os
import tempfile
from pathlib import Path
from typing import Any
def write_json_atomic(path: Path, data: Any) -> None:
path = path.resolve()
path.parent.mkdir(parents=True, exist_ok=True)
fd, temp_name = tempfile.mkstemp(
dir=path.parent,
prefix=f".{path.name}.",
suffix=".tmp",
)
temp_path = Path(temp_name)
try:
with os.fdopen(fd, "w", encoding="utf-8", newline="\n") as file:
json.dump(data, file, ensure_ascii=False, indent=2)
file.write("\n")
file.flush()
os.fsync(file.fileno())
os.replace(temp_path, path)
except BaseException:
temp_path.unlink(missing_ok=True)
raise
if __name__ == "__main__":
target = Path("state.json")
write_json_atomic(target, {"version": 1, "ready": True})
print(json.loads(target.read_text(encoding="utf-8")))
运行:
python atomic_json.py
预期输出:
{'version': 1, 'ready': True}
临时文件必须放在目标文件同一目录:这样通常能保证两者位于同一文件系统,而 os.replace() 的替换才具有原子性。先 flush() 再 os.fsync(),可以把 Python 缓冲区和操作系统缓存中的文件内容提交给存储层;替换成功前,读取者看到旧文件,成功后看到完整的新文件。异常分支则清理未完成的临时文件。
这解决的是读取到半文件的问题,不解决多个写入者之间的更新竞争:并发调用仍然是最后一次替换生效。需要“读取、修改、写回”不丢更新时,还应在更高层增加文件锁、版本号比较,或改用支持事务的存储。对断电后的目录项持久性有严格要求时,POSIX 系统还需结合目标文件系统语义评估是否同步父目录。