3 次浏览0 条回复

直接用 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 系统还需结合目标文件系统语义评估是否同步父目录。