最近在给一个个人工具接本地文件存储。核心场景很朴素:用户在浏览器里改动数据,落到硬盘上一个用户自选的文件里,下次打开时读回来。
写第一版的时候,我在自测里发现一个不算太罕见的问题:中途写入失败,原来的文件会被清零。
我当时的第一反应是很本能的那种——「那我自建一层安全机制吧」。脑子里已经开始设计方案:
- 先写到一个
.tmp临时文件 - 完全写完之后 rename 覆盖原文件
- 这样中途挂了,原文件还在,最多丢这一次的改动
这个模式对老程序员来说太熟了,Unix 世界里管这个叫「原子替换」(atomic replace),几十年的老套路。
停下来查了一下 API 文档
真正动手前我留了个心眼,先去查了下我正在用的这个 Web API(File System Access API)到底是怎么处理写入的。
结果——它自己就是原子的。
规范里写得很清楚:调用 createWritable() 时,浏览器会在内部开一个 swap 文件,你所有的写操作都落在这个 swap 上;只有当你调用 close() 的那一刻,浏览器才会把 swap 提交成正式文件。
换句话说:
- 中途出任何事(用户拔盘、进程崩溃、你自己
abort()),只要没走到close(),原文件一个字节都不会动 - 我根本不需要自己造
.tmp+ rename 那一层
更讽刺的是,就算我想自己造,也造不干净——这个 API 只给我文件句柄(file handle),拿不到目录句柄(directory handle)。我根本没有权限在同目录下创建一个我自己的 .tmp。
底层已经替我把这件事做完了,做得比我能做的更好。
这不是第一次踩这个模式
回想起来,这类「我以为要自己造轮子,其实底层已经内置了」的场景,工程里到处都是。随便举几个:
- 想给 HTTP 请求加重试——很多 SDK 已经内置指数退避重试
- 想给数据库连接加连接池——ORM 层多半已经在管
- 想给多个并发写加串行锁——单文件句柄本身可能就是串行的
- 想给幂等操作自建去重——上游可能已经有 idempotency-key 机制
- 想给敏感字段自建加密存储——平台的 keychain / secure enclave 一直在等你调用
这些例子有个共同点:当我处于「解决方案兴奋」状态时,很难主动停下来去查一下「这件事是不是已经被解决过了」。
一个可以粗暴执行的习惯
我给自己加了一个简单的习惯,一句话就能背下来:
凡是准备自建”安全机制”、“补偿机制”、“事务边界”这类东西之前,先花 10 分钟查一下底层 API / SDK / 平台,是不是已经保证了。
不用查多,10 分钟基本能定性——要么找到明确的原子性/幂等/重试保证,要么找到明确说「这里不保证」的文档,你就知道该不该自己上了。
10 分钟的成本,换掉的是:
- 一层你不需要写的代码
- 一层你以后要维护的抽象
- 更重要的——一层可能跟底层重复、甚至互相冲突的机制
因为你自己造的那层如果跟底层”冲突”了,后果会很难查。举个具体的:你以为你的 .tmp + rename 是在保护数据,但如果底层已经在 swap,你的 rename 反而可能把它还没提交的中间状态给暴露出来。多做未必是加固,可能是引入新的失效模式。
收尾
这次排查花掉的时间加起来不到 20 分钟:10 分钟查 MDN,10 分钟改代码。省下的是我原本打算写的一整套自建原子替换逻辑、以及后续跟它有关的测试和维护。
投入产出比这么好的习惯,值得死死记住。
底层不是黑盒。查一下再动手。
—— 马启航Marvis