40 行原子写入:为什么临时文件必须写在同一个目录

我的日报系统里有一个 JSON 文件,存的东西不多,但丢不起:订阅者名单、健康检查状态、LLM 调用统计,全在里面。

有一阵子我一直在担心一件事:如果进程在写这个文件的时候挂掉------断电、被 kill、磁盘满------会发生什么?

答案很不好看。用 open(path, "w") 直接覆盖写,写了一半挂掉,磁盘上留下的就是半截 JSON。文件存在,但 json.load 解不开。第二天定时任务一起来读它,整个流程起不来。数据丢一点可以忍,系统起不来不行。

所以后来我把所有落盘收敛成了一个 40 行的函数,全项目只允许这一个实现。今天把它拆开讲,里面有一个反直觉的取舍。

先看代码

python 复制代码
import json, os, tempfile

def atomic_write_json(path, data, *, indent=2):
    d = os.path.dirname(path) or "."
    os.makedirs(d, exist_ok=True)
    fd, tmp = tempfile.mkstemp(
        prefix=os.path.basename(path) + ".",
        suffix=".tmp", dir=d)          # 注意:dir=d
    try:
        with os.fdopen(fd, "w", encoding="utf-8") as f:
            json.dump(data, f, ensure_ascii=False, indent=indent)
        os.replace(tmp, path)
    finally:
        if os.path.exists(tmp):
            os.remove(tmp)

思路就一句话:不直接覆盖目标文件,先写一个临时文件,写成功了再一步替换过去。

os.replace 在同一个文件系统里是原子的------目标文件要么是完整的旧版本,要么是完整的新版本,不存在中间态。写一半挂掉?挂掉的只是那个 .tmp,正式文件一根毫毛都没动。

坑一:临时文件必须写在目标同一个目录

最常见的写法是 tempfile.mkstemp() 不传 dir,临时文件落在系统临时目录。Linux 上多数时候能跑,一换机器就炸。

原因是 os.replace 底层是 rename,rename 不允许跨文件系统 。你的数据在 D:\data\,系统临时目录在 C:\------跨盘,直接 OSError。

我在注释里专门写了一句:临时文件写在目标同目录(保证 os.replace 同盘,Windows 跨盘会失败)。这不是习惯问题,是硬约束。

顺带的好处:临时文件和正式文件同目录,权限、归属也是一致的,不会出现"临时目录里权限正常、搬过去就不对"的怪事。

坑二:一个故意的"不优化"------不做 fsync

看到这段代码的人,十有八九会想"顺手"补一句 f.flush(); os.fsync(f.fileno())。我的注释里专门堵了这条路:

故意不调用 os.fsync ------ 统计文件一轮会写 41 次,每次 fsync 会显著拖慢主流程。这是有意的取舍,不要"顺手优化"补上。

账是这么算的:fsync 强制把数据刷到物理盘,一次几毫秒到几十毫秒。这个统计文件一轮流程写 41 次,每次都 fsync,等于平白多出几百毫秒------而它保护的东西,最多丢一轮统计数字,下一次运行就补回来了。

fsync 买的是"断电也不丢",我的场景只要求"进程挂了不坏"。 os.replace 的原子性已经满足后者。为不需要的保证付 41 次的代价,不划算。

这个取舍我写进了 docstring。因为三个月后我自己都可能忘了为什么没有 fsync,然后"好心"补上。取舍不写下来,就等于没做过。

坑三:finally 里的清理要区分两种情况

python 复制代码
finally:
    if os.path.exists(tmp):
        os.remove(tmp)

注意 os.replace 成功之后,tmp 这个路径已经不存在了(它变成了正式文件),所以 exists 是 False,remove 是空操作。真正会走到 remove 的只有一条路:写临时文件失败。

不清理的话,同目录会慢慢堆一堆 xxx.json.abc123.tmp 垃圾。不致命,但很难看,而且会让排障的人困惑。

最后

文本文件同理,换成 f.write(text) 就是,我项目里另一个 atomic_write_text 就是这么来的。

总结成三句话:

  1. 直接覆盖写 = 给进程崩溃留后门。写一半挂掉,留下的半截文件比没有文件更糟------它会让"文件存在"的检查通过,然后炸在解析上。
  2. 临时文件 + 同目录 + os.replace,40 行,解决九成场景。
  3. 取舍要写进注释。尤其是"故意没做"的事情------没人会追问一个不存在的 fsync,但一定会有人好心补上它。
相关推荐
vinijack1 小时前
1天干穿1个周额度!手握17个 opencode-go 账号的我,开源了 Claude Code 无感切号代理 OpenSelect
人工智能
zhidian_AI1 小时前
Claude Code 装到一半总超时?其实是下载了两次
人工智能
国科安芯1 小时前
长期在轨的计算底座:商业空间站与在轨服务飞行器健康管理中的抗辐射MCU—— 面向可维护空间平台的分层容错与安全通信综述
人工智能·单片机·嵌入式硬件·安全·低轨卫星·抗辐射
科技新资讯2 小时前
AI如何批量处理Excel文件? 从数据合并、清洗到自动生成报表,看看 AI Agent 怎么完成任务
大数据·人工智能·excel
郝学胜-神的一滴2 小时前
AI 编程智能体 22:解析器赋能与实战落地全指南
人工智能·程序人生·游戏·unity·游戏引擎
Sayai2 小时前
让动漫角色复刻真人动作!MiniMax H3 动作控制实战:一段视频驱动角色动画(12G 显存,附模型下载)
人工智能·aigc·音视频·comfyui·动作捕捉·minimax
绿算技术2 小时前
从单点性能到体系效能:存算网创新联合体启航
大数据·人工智能
衡石科技2 小时前
让多轮问数保持口径一致:Data Agent 的上下文、偏好与学习开关
人工智能·学习·安全