Python 更新 JSON 配置时,怎样避免留下半份文件
一个服务每隔几分钟读取 config.json。更新脚本以 w 模式打开文件再执行 json.dump(),此时旧文件已经被截断。若序列化到一半遇到不支持的数据类型,读配置的服务就可能看到无法解析的 JSON。即使写入成功,并发读取也可能刚好撞上写入过程。
可以先在同一个目录写临时文件,写完并关闭后,再用 os.replace() 更新目标路径。目标目录相同可以避免跨文件系统替换。下面的例子在临时目录中执行,既验证正常更新,也验证异常时原配置仍可读取。
import json
import os
from pathlib import Path
from tempfile import NamedTemporaryFile, TemporaryDirectory
def save_json(path, data):
temp_path = None
try:
with NamedTemporaryFile(
mode="w", encoding="utf-8", dir=path.parent,
prefix=f".{path.name}.", suffix=".tmp", delete=False,
) as file:
temp_path = Path(file.name)
json.dump(data, file, ensure_ascii=False, allow_nan=False)
file.write("\n")
file.flush()
os.fsync(file.fileno())
os.replace(temp_path, path)
finally:
if temp_path is not None:
temp_path.unlink(missing_ok=True)
with TemporaryDirectory() as folder:
config = Path(folder) / "config.json"
save_json(config, {"version": 1, "name": "备份任务"})
try:
save_json(config, {"version": 2, "unsupported": {1, 2}})
except TypeError:
print("序列化失败后", json.loads(config.read_text(encoding="utf-8")))
save_json(config, {"version": 2, "name": "备份任务"})
print("正常更新后", json.loads(config.read_text(encoding="utf-8")))
print("残留临时文件", list(config.parent.glob("*.tmp")))
序列化失败后 {'version': 1, 'name': '备份任务'}
正常更新后 {'version': 2, 'name': '备份任务'}
残留临时文件 []
这个流程把可能失败的序列化和写入都放在临时文件上。成功替换时,按路径新打开文件的读者会读到完整文件。已经持有旧文件句柄的读者可能继续读取旧内容,具体取决于系统的文件共享方式。替换前关闭临时文件也有助于在 Windows 上避免自己持有的句柄阻碍替换。
flush() 先把 Python 的缓冲交给系统,fsync() 再请求同步文件数据。若需要覆盖突然断电的恢复要求,还要考虑目录项同步、存储系统和备份恢复。原子替换也没有解决多个写者的更新冲突。如果两个进程都从版本 1 出发修改配置,最后替换的进程仍可能覆盖另一个人的修改。这时可以把更新交给单一写进程,或者在读、改、写整个过程加锁。
替换的是文件本身,原文件的权限和所有权也需要按部署要求检查。最容易验证的起点是先在临时目录跑上面的正常和异常路径,再把同样的检查加入配置更新流程。
参考资料为 Python 标准库文档 os.replace()、os.fsync() 和 tempfile.NamedTemporaryFile()。
- 点赞
- 收藏
- 关注作者
评论(0)