---
title: "配置文件写到一半怎么办：临时文件与原子替换"
date: 2026-11-12
slug: python-atomic-file-replace
canonical: https://kernelranch.com/blog/2026/11/python-atomic-file-replace
markdown_url: https://kernelranch.com/blog/2026/11/python-atomic-file-replace.md
tags: ["python", "filesystem", "reliability"]
---直接打开配置文件写入会先截断旧内容。进程在中途失败，下一个读者可能只看到半份 JSON。把新内容写到目标目录里的临时文件，刷新后再替换目标，可以把发布动作集中到一次替换操作。

## 1. 在发布前故意抛出异常

保存为 atomic_file_lab.py，运行 python atomic_file_lab.py。输出目录由脚本临时创建。

```python
import json
import os
from pathlib import Path
from tempfile import NamedTemporaryFile, TemporaryDirectory

def publish_json(target, value, fail_before_publish=False):
    payload = json.dumps(value, ensure_ascii=False).encode("utf-8")
    staged = None
    try:
        with NamedTemporaryFile(dir=target.parent, prefix=".pending-", delete=False) as stream:
            staged = Path(stream.name)
            stream.write(payload)
            stream.flush()
            os.fsync(stream.fileno())
        if fail_before_publish:
            raise RuntimeError("injected failure")
        os.replace(staged, target)
    finally:
        if staged is not None:
            staged.unlink(missing_ok=True)

with TemporaryDirectory() as tmp:
    target = Path(tmp) / "config.json"
    publish_json(target, {"version": 1})
    try:
        publish_json(target, {"version": 2}, fail_before_publish=True)
    except RuntimeError:
        pass
    assert json.loads(target.read_text()) == {"version": 1}
    print("failed publish: version=1")
    publish_json(target, {"version": 2})
    assert json.loads(target.read_text()) == {"version": 2}
    assert sorted(p.name for p in target.parent.iterdir()) == ["config.json"]
    print("successful publish: version=2; temporary files=0")
```

## 2. 根据故障位置判断结果

| 故障位置 | 应观察的状态 |
|---|---|
| 序列化失败 | 目标文件仍是旧版本 |
| 临时文件写入或刷新失败 | 目标文件仍是旧版本，异常传给调用方 |
| 替换失败 | 本次发布失败，调用方检查权限、打开句柄和文件系统条件 |
| 替换成功 | 后续打开目标路径的读者可以读取新版本 |

示例实测发布前异常后版本仍为 1，正常发布后版本变成 2，临时文件清理完成。临时文件放在目标目录可以避免跨文件系统重命名的问题。Windows 上先关闭临时文件句柄，再调用替换。

## 3. 区分完整发布和断电持久性

os.fsync 用于刷新文件内容。Linux 等 POSIX 环境若要求目录项在断电后也持久，需要在替换后打开父目录并同步目录，具体保证取决于文件系统。示例验证的是正常运行期间的失败路径，断电恢复需要专门演练。

替换会发布临时文件的元数据。目标文件需要固定权限、ACL 或所有者时，应在发布前设置并验证。多个写入者同时发布时，最后一次成功替换会覆盖前一次结果。对配置版本有条件更新要求时，需要锁或数据库事务来协调。

读者每次读取应在一次打开期间读完文件。一次逻辑操作反复打开路径，可能跨越两个版本。应用层完成结构校验后再应用新配置，校验失败保留当前生效版本。

## 小结

先生成完整内容，再写入同目录临时文件，最后替换目标路径。把发布失败、权限变化和并发写入作为独立验收项。

## 数据来源与检索时间

检索时间 2026-11-12。

| 来源 | URL | 对应内容 |
|---|---|---|
| Python 3.11 官方文档 | https://docs.python.org/3.11/library/os.html#os.replace | replace、fsync 与文件系统约束 |
| Python tempfile 文档 | https://docs.python.org/3.11/library/tempfile.html | 临时文件位置、句柄与清理 |

## 核验说明

- 主示例在本地 Python 3.11.9 下执行，检查正文中的断言与输出。
- 实验范围是文中构造的输入和本地运行条件，部署时需按实际数据规模与运行环境核验。
