文件集成互斥控制基础知识 - 文件锁与原子 claim 的最佳实践
· 更新日期: · 小村 豪 · 文件集成, 互斥控制, 设计, Windows 开发
引用本文(DOI: 10.5281/zenodo.21615420)
本文保存于 Zenodo。以下同时提供始终指向最新版本的 DOI,以及固定于您正在阅读版本的 DOI。
小村 豪(2026)。《文件集成互斥控制基础知识 - 文件锁与原子 claim 的最佳实践》。小村软件有限公司。https://doi.org/10.5281/zenodo.21615420 https://comcomponent.com/zh-CN/blog/2026/03/07/001-file-integration-locking-best-practices-komurasoft-style/
- DOI(最新版本)
- 10.5281/zenodo.21615420
- DOI(此版本)
- 10.5281/zenodo.22281920
文件集成的互斥控制,在共享文件夹、夜间批处理、跨进程集成中几乎必然会成为问题。 搜索中常见的困惑主要有:光靠文件锁是否够用、如何避免多个 worker 抓到同一个文件、如何避开写入中途的文件等。
本文将以文件锁、原子 claim、temp -> rename、idempotency 为主线,来审视文件集成的互斥控制。
先统一术语
这个领域里有很多直接沿用英文的说法,如果含义一直含糊,读起来就会很吃力。这里先把它们在本文中的含义固定下来。
| 术语 | 本文中的含义 |
|---|---|
| 原子的(atomic) | 指中间状态不会被外部看到的操作。结果只有两种:要么成功,要么什么都没发生 |
| claim | 指宣告“这个文件由我来处理”,从而确保处理权。本文中主要指这样一种形式:只有成功把文件从 incoming rename 到 processing/<worker>/ 的一方才成为所有者 |
| 原子 claim | 指用一次操作完成上述 claim。如果确认与确保被拆成两步,其他进程就能挤进这个空隙(3.1) |
| lease | 指带有效期的所有权。在 lock file 中写明谁持有、有效到什么时候,一旦过期就让其他 worker 可以接管(4.4) |
| stale | 指持有者已经异常终止、但 lock 或 claim 仍然残留的状态。如果无法判定它是活的还是死的,所有人都会停下来(2.3) |
| manifest | 指与数据本体分开放置的内容说明文件。其中写入文件名、大小、哈希、记录数等信息,供接收方校验使用。done 文件是它的最小版本(4.2) |
| idempotency(幂等性) | 指同一份输入再处理一次,结果也不会改变的性质(4.5) |
| advisory lock | 只有在所有参与者都遵守该约定的前提下才生效的锁。操作系统并不强制,因此完全可以写出无视它照样读写的程序。Linux 的 flock 就属于这一类 |
| byte-range lock | 不以整个文件为对象,而只针对指定范围的锁。代表是 Windows 的 LockFileEx,这一类由操作系统强制执行。不过也存在例外(3.5) |
图中实线表示始终成立的关系,虚线表示带条件的关系(成立条件写在详情页各关系的说明中)。关系的完整列表(共 29 条,附依据与确信度)以及主要概念的定义,汇总在知识地图详情页(日文)。数据:JSON-LD / Turtle
目录
- 先说结论(一句话)
- 文件集成中出现的竞争模式(图)
- 2.1. 读到写入中途的文件
- 2.2. 多个 worker 同时抓到同一个文件
- 2.3. stale lock 导致所有人都卡住
- 反模式
- 3.1.
Exists -> Create的两段式检查 - 3.2. 直接写入最终文件名
- 3.3. 文件大小停止变化就视为完成
- 3.4. 大家一起更新共享文件
- 3.5. 以为锁 API 是万能的
- 3.1.
- 最佳实践
- 4.1. 以
temp -> close -> rename / replace方式公开 - 4.2. 用
done/ manifest 明确标示完整性 - 4.3. 接收方以原子方式获取 claim
- 4.4. 若依赖 lock file,就把它做成 lease
- 4.5. 以 idempotency 为前提
- 4.1. 以
- 伪代码(节选)
- 大致的选用方式
- 总结
- 参考资料
文件集成是一个“交接约定”比代码本身更容易出问题的领域。 单元测试都能通过,却唯独在生产环境的共享文件夹或夜间批处理中偶尔出问题,而且难以复现——这种情况相当常见。
大多数原因,与文件 I/O 的 API 本身无关,而在于以下三点含糊不清:
- 什么时候才可以读取
- 谁拥有处理权
- 失败时该如何恢复
本文不会把文件集成的互斥控制仅仅停留在操作系统锁的话题上,而是将其作为一种交接协议来加以整理。
另外,本文中出现的代码,也以一整套可构建、可运行的示例(库、演示两个 worker 之间 claim 竞争与 lease 交接的演示程序,以及重现竞争、损坏、stale lock 的单元测试)的形式发布在 GitHub 上。
file-integration-locking-best-practices-komurasoft-style - komurasoft-blog-samples (GitHub)
1. 先说结论(一句话)
- 文件集成中最重要的,是让“最终文件名一出现,就已经可以读取”这一状态成立
- 用文件名或目录来区分表达“生成中 / 已公开 / 处理中 / 已处理”
- 如果有多个 worker,读取之前要先以原子方式获取 claim
- lock file 或操作系统锁只作为辅助手段,最终以 idempotency 来兜底
说到底,在文件集成中,与其说主体是互斥控制,不如说是交接协议的设计。 调用一个锁函数就完事,这种情况是不存在的。
2. 文件集成中出现的竞争模式(图)
2.1. 读到写入中途的文件
一旦直接对最终文件名开始写入,就会出现这种问题。 JSON 会缺少闭合括号,CSV 行数不够,ZIP 则会直接损坏。
sequenceDiagram
participant 送信 as 发送方
participant 共有 as 共享文件夹
participant 受信 as 接收方
送信->>共有: 以最终文件名创建 orders.csv
送信->>共有: 正在写入第 1~5000 行
受信->>共有: 检测到 orders.csv
受信->>共有: 直接开始读取
Note over 受信: 还在写入中
送信->>共有: 写入剩余内容
Note over 受信: 行数不足 / 解析失败 / 只处理了部分内容
2.2. 多个 worker 同时抓到同一个文件
“查看列表,如果未处理就打开”这种流程,很容易让两个 worker 同时抓住同一个文件。 这正是重复计算或重复发送问题的开端。
sequenceDiagram
participant W1 as worker1
participant W2 as worker2
participant Dir as incoming
W1->>Dir: 找到 a.csv
W2->>Dir: 找到 a.csv
W1->>Dir: 开始读取
W2->>Dir: 开始读取
Note over W1,W2: 同一份输入被重复处理
2.3. stale lock 导致所有人都卡住
只是放置一个 lock file 的设计,很容易在异常终止时卡住。 如果不知道这是谁的 lock、它是否仍然有效、有效期到什么时候,后续的处理就会永远等待下去。
sequenceDiagram
participant A as workerA
participant Lock as lock 文件
participant B as workerB
A->>Lock: 创建 lock
Note over A: 这里发生异常终止
B->>Lock: 确认 lock 是否存在
B->>Lock: 暂缓开始处理
B->>Lock: 继续等待
Note over B,Lock: 无法判定是否 stale,全员停摆
3. 反模式
3.1. Exists -> Create 的两段式检查
这里的问题在于,“确认”与“确保”是两个独立的操作。 中间会有其他进程插入进来,因此无法实现互斥。
sequenceDiagram
participant A as 进程A
participant B as 进程B
participant FS as 文件系统
A->>FS: 确认 lock 是否不存在
B->>FS: 确认 lock 是否不存在
FS-->>A: 不存在
FS-->>B: 不存在
A->>FS: 创建 lock
B->>FS: 创建 lock
Note over A,B: 两边都被放行
典型的错误写法是这样的。
if (!File.Exists(lockPath))
{
File.WriteAllText(lockPath, Environment.ProcessId.ToString());
ProcessFile();
}
需要做的是,把“不存在就创建”变成一个单一操作。
在 .NET 中可以使用 FileMode.CreateNew 系列,POSIX 系统则使用 O_CREAT | O_EXCL 这类原子创建方式。
3.2. 直接写入最终文件名
如果接收方的理解是“看到那个名字就可以读取”,那么从直接对最终文件名开始写入的那一刻起,就已经输了。 基本原则是不要把“可见”与“可以读取”等同起来。
flowchart LR
A[final 名可见] --> B[接收方检测到]
B --> C[发送方仍在写入]
C --> D[读到不完整的数据]
using var writer = OpenForWrite(finalPath); // 这里 finalPath 就已经变得可见
foreach (var row in rows)
{
writer.WriteLine(row);
}
这种做法,等于自己主动招来了 2.1 中的问题。
3.3. 文件大小停止变化就视为完成
这看起来很方便,但相当危险。 跨网络的复制、发送方的临时暂停、缓冲、重试,都会让文件大小很自然地出现波动。
sequenceDiagram
participant 送信 as 发送方
participant 共有 as 共享文件夹
participant 受信 as 接收方
送信->>共有: 开始复制 data.zip
送信->>共有: 中途暂停
受信->>共有: 大小 10 秒未变化
Note over 受信: 误判为已完成
受信->>共有: 开始读取
送信->>共有: 复制恢复
if (currentLength == lastLength && stableSeconds >= 10)
{
return Ready;
}
靠推测来判断完成,在共享文件夹或大文件场景下很容易被绊倒。 完成状态用 manifest 或 done file 明确标示出来,才会更稳定。
3.4. 大家一起更新共享文件
让大家都去读取和更新同一个 status.csv 或 counter.json 的设计,基本上都是最后写入的人胜出。
一旦开始把文件集成当作简易数据库来用,就会在这里吃苦头。
sequenceDiagram
participant A as batchA
participant B as batchB
participant F as status.csv
A->>F: 读取 v1
B->>F: 读取 v1
A->>F: 写入 v2-A
B->>F: 写入 v2-B
Note over F: A 的更新丢失
也有人会退一步选择 append-only,但其含义会随文件系统或部署形态而变动。 如果确实需要共享更新,那么这一环节最好不要硬靠文件集成来撑。
3.5. 以为锁 API 是万能的
锁 API 很重要,但它只在所有参与者都遵循同一套约定时才有效。 在异构系统集成中,最好不要过度信赖它,这样更安全。
补充说明:
- Linux 的
flock是 advisory lock,不遵守约定的一方照样可以正常写入 - Windows 的 byte-range lock,在内存映射文件中会被忽略
- 也就是说,不应该让操作系统级别的锁单独承担完成通知或所有权设计的全部责任
第二点在 Windows 的规范中有明确记载。Microsoft Learn 的 Locking and Unlocking Byte Ranges in Files 先写明:其他进程访问已被锁定的范围时必定失败(也就是说 Windows 的范围锁不是 advisory,而是由系统强制执行的),紧接着又补上一条提醒——使用内存映射文件时,byte-range lock 会被忽略。也就是说,只要对方是经由 CreateFileMapping 操作同一个文件,我方的锁就会被直接绕过。
在 .NET 中获取范围锁,用的是 FileStream.Lock / Unlock(Windows 环境下)。
using var stream = new FileStream(
path, FileMode.Open, FileAccess.ReadWrite, FileShare.ReadWrite);
// 只把开头的 1 个字节当作“处理中”的标记加排他锁
stream.Lock(0, 1);
try
{
// 在这里读写数据本体
}
finally
{
// 关闭之前必须解除锁
stream.Unlock(0, 1);
}
这种写法在遵循同一套约定的应用之间是有效的。但正如上面所说,如果对方经由内存映射访问,它就不起作用;而且本来也无法保证其他系统会去查看这张标记。所以真正的主体是第 4 章的交接协议一侧。
4. 最佳实践
先把这些做法与第 3 章的反模式一一对应地列出来。如果你意识到自己正踩中其中某一条,直接从对应的小节读起也没问题。
| 反模式 | 会发生什么 | 对应的对策 |
|---|---|---|
3.1. Exists -> Create 的两段式检查 |
确认与确保之间的空隙被插入,两个进程同时向前推进 | 4.3 以原子方式获取 claim(用 rename 或 FileMode.CreateNew) |
| 3.2. 直接写入最终文件名 | 接收方读到正在写入中途的文件 | 4.1 以 temp -> close -> rename / replace 方式公开 |
| 3.3. 文件大小停止变化就视为完成 | 把复制的临时暂停误判为已完成 | 4.2 用 done / manifest 明确标示完成 |
| 3.4. 大家一起更新共享文件 | 被后写入的一方覆盖,更新丢失 | 用 4.3 把写入方收敛为一个,用 4.5 吸收重复处理。如果还不够,就参考第 6 章的撤退判断 |
| 3.5. 以为锁 API 是万能的 | 会被不遵守约定的一方或经由内存映射的访问打破 | 用 4.4 中作为 lease 的 lock file,以及 4.5 的 idempotency 来兜底 |
4.1. 以 temp -> close -> rename / replace 方式公开
这是王道做法。 把生成中的文件封闭在 temp 文件名下,close 之后再切换为 final 文件名。 接收方只查看 final 文件名。
flowchart LR
A[生成唯一的 temp 文件名] --> B[将全部内容写入 temp]
B --> C[flush / close]
C --> D[在同一目录中 rename / replace 为 final 文件名]
D --> E[接收方只监视 final 文件名]
要点:
- temp 与 final 要放在 同一目录,至少要在 同一个卷 / 文件系统 上
- 在 Windows / .NET 上,可以考虑使用
File.Replace系列方法 - 约定:final 文件名一出现,内容就已经完成
如果把 temp 放在另一个驱动器上,rename 就会相当于一次单纯的复制,或者 Replace 会失败。
这个前提看起来不起眼,但非常重要。
跨共享文件夹(SMB)时,还有下面 4 点会变得不确定。本文的主战场恰恰就在这里,所以单独列出来。
- 在同一个共享的同一目录内进行 rename,是在服务器端执行的。 因此“看不到中间文件名”这一性质本身仍然成立。反过来,如果像从
\\server\shareA到\\server\shareB这样跨越共享,就会被当作不同的卷来处理,而 Windows 的MoveFileEx在指定了MOVEFILE_COPY_ALLOWED时,会用复制加删除来代替移动。也就是说它不再是原子的,中间状态会被看到。把 temp 与 final、incoming与processing放在同一个共享之内这一前提,比在本地时更加重要 - 只要有人打开着,rename 就会失败。 共享文件夹上会有杀毒软件、搜索索引器、其他站点的客户端等我方并不掌握的访问方。对于 publish 与 claim 的 rename,把失败当作正常分支而不是异常来处理,中间插入一段短暂等待后重试,这才是现实的做法
- 时间戳不能作为判断依据。 Microsoft Learn 的 File Times 中写明,关于文件时间,唯一能保证的只有“在关闭做出修改的那个句柄时会正确写入”这一点。写入过程中的最后修改时间,在所有写入用句柄全部关闭之前不会被完整更新。精度也取决于文件系统:FAT 的最后修改时间以 2 秒为单位,NTFS 的最后访问时间最多会延迟 1 小时才更新。再加上跨 SMB 时,时间是按服务器端的时钟打上的,如果客户端与服务器的时钟存在偏差,“距最后更新已过 N 分钟就开始处理”这种判定也会跟着偏。这正是不用时间或大小、而用 4.2 的
done/ manifest 来判定完成的理由 - 变更通知也会漏掉。 对共享文件夹的监视不要只依赖事件通知,与定期的目录枚举配合使用会更稳定。这部分内容整理在 FileSystemWatcher 实务指南 - 漏报与重复的应对 中
4.2. 用 done / manifest 明确标示完整性
除了数据本体之外,用另一个文件明确标示“什么已经完成”,可以让接收方更稳定。 在异构系统集成中尤其有效。
flowchart TD
A[生成 data.tmp] --> B[公开为 data.csv]
B --> C[创建 data.done / manifest.json]
C --> D[接收方检测到 done / manifest]
D --> E[验证文件名、大小、哈希]
希望放入 manifest 中的字段大致有这些:
- 目标文件名
- 大小
- 哈希
- 记录数
- 集成 ID / idempotency key
- 生成时间
顺序也很重要。
如果比本体公开更早放置 done,那它就不是完成通知,而是事故预告。
4.3. 接收方以原子方式获取 claim
如果多个 worker 都在查看同一个 incoming,那么“读取之前先把它移到自己名下”是最容易理解的做法。
只有把文件从 incoming 重命名到 processing/<worker>/ 成功的 worker 才会进行处理。
sequenceDiagram
participant W1 as worker1
participant W2 as worker2
participant IN as incoming
participant PR as processing
W1->>IN: 找到 a.csv
W2->>IN: 找到 a.csv
W1->>PR: rename a.csv
W2->>PR: rename a.csv
Note over W1,W2: 先成功的一方获得所有权
在运维层面,把目录也分开,会更便于追踪。
flowchart LR
T[temp] -->|publish| I[incoming]
I -->|claim| P[processing]
P -->|成功| A[archive]
P -->|失败| E[error]
用于 claim 的 rename 操作,前提同样是要在同一文件系统上进行。
4.4. 若依赖 lock file,就把它做成 lease
如果要使用 lock file,就不要只做成一个空文件,而要做成 带有有效期的所有权信息。 不知道是谁获取的 lock,之后必然会引发纠纷。
flowchart TD
L[lock.json] --> A[ownerId]
L --> B[host]
L --> C[pid]
L --> D[acquiredAt]
L --> E[expiresAt]
L --> F[heartbeatAt]
要点:
- 创建要以原子方式进行
- 把停止更新作为 stale 判定的依据
- 删除原则上只由 创建者本人 进行
- 预先假定会有解锁遗漏的情况,制定好恢复流程
lock file 终究只是 用于协作的一张号牌。 想靠这一张号牌就保证完全的一致性,基本上都会很吃力。
4.5. 以 idempotency 为前提
互斥控制固然重要,但在实际运维中,“偶尔重复送达”“中途重新执行”这类情况是无法归零的。 最终起作用的,是 即使同一份输入再吃一次也不会坏掉 的设计。
flowchart LR
A[输入 + idempotency key] --> B{是否已处理}
B -- 是 --> C[不重复执行,视为成功]
B -- 否 --> D[执行处理]
D --> E[记录到已处理台账]
例如,为每个接收文件配上集成 ID,并记录到已处理台账中。 即使互斥控制曾被打破一次,只要保证结果不会被重复计算,运维就会轻松很多。
5. 伪代码(节选)
下面出现的 MakeTempPathSameDirectory、TryClaimBundleByRename,都是为了展示顺序而设的 虚构函数名。真正可以运行的实现,在开头介绍的那套示例中。
这段伪代码的实现(库、两个 worker 的 claim 竞争演示、单元测试) - komurasoft-blog-samples (GitHub)
5.1. 典型的失败模式
var lockPath = finalPath + ".lock";
if (!File.Exists(lockPath))
{
File.WriteAllText(lockPath, "");
using var writer = OpenForWrite(finalPath); // 直接写入最终文件名
WritePayload(writer);
File.Delete(lockPath);
}
存在三个问题:
Exists与WriteAllText是两个独立操作finalPath从写入过程中途就已经可见- 异常终止时
lock会残留
5.2. 正确方向的示例(粗略写法)
var tempPath = MakeTempPathSameDirectory(finalPath);
WritePayload(tempPath);
FlushAndClose(tempPath);
PublishByRenameOrReplace(tempPath, finalPath); // 前提:同一 FS / 同一 volume
PublishDoneFile(finalPath + ".done", new
{
FileName = Path.GetFileName(finalPath),
Size = GetFileSize(finalPath),
Hash = ComputeHash(finalPath),
IdempotencyKey = integrationId
});
if (!TryClaimBundleByRename(baseName, incomingDir, processingDir))
{
return; // 已被其他 worker 先行获取
}
var manifest = ReadDoneFile(Path.Combine(processingDir, baseName + ".done"));
VerifyPayload(Path.Combine(processingDir, baseName), manifest);
if (AlreadyProcessed(manifest.IdempotencyKey))
{
MoveBundle(processingDir, archiveDir, baseName);
return;
}
Process(Path.Combine(processingDir, baseName));
RecordProcessed(manifest.IdempotencyKey);
MoveBundle(processingDir, archiveDir, baseName);
这里重要的是 顺序,而不是实现细节。 不要把“写入”“公开”“获取所有权”“记录已处理”混在一起,这样会更不容易出问题。
6. 大致的选用方式
- 单一 writer / 单一 reader / 同一主机的情况下,光靠
temp -> rename就已经相当稳定 - 如果存在多个 consumer,就加入
incoming -> processing的 claim rename - 异构系统集成、NAS、共享文件夹的场景下,把 manifest / done 与 idempotency 都加进去会更安全
- 如果多个 writer 想更新同一个逻辑状态,不要在文件集成上硬撑,也可以考虑数据库或队列
- 操作系统级别的锁在同一应用群、同一前提下是有效的,但不能替代交接协议
最后一项其实也是一种撤退判断。 确实存在一些问题,用文件来处理会相当痛苦。
7. 总结
文件集成的互斥控制,重点不在于调用锁函数,而在于决定状态迁移——这就是本文的核心主张。用名称或目录来表达“生成中 / 已公开 / 处理中 / 已处理”,避免 Exists -> Create 的两段式检查、直接写入最终文件名、等待大小稳定、共享文件的相互更新,以及对锁 API 的过度信赖。在此基础上,再组合使用 temp -> close -> rename / replace、done / manifest、claim rename、lease 与 idempotency,就能相当有效地避免共享文件夹集成中的问题。
在文件集成中,诀窍在于不要把“可以读到”与“可以读取”混为一谈。只要把这两者区分开,那些只在深夜才会出现的故障就会大幅减少。
8. 参考资料
- 本文的整套示例代码(库、演示、单元测试) - komurasoft-blog-samples (GitHub)
- LockFileEx function (Win32)
- Locking and Unlocking Byte Ranges in Files (Win32)
- Moving and Replacing Files (Win32)
- MoveFileEx function (Win32)
- File Times (Win32)
- FileStream.Lock Method (.NET)
- File.Replace Method (.NET)
- rename — POSIX
- open — POSIX (
O_CREAT | O_EXCL) - flock(2) — Linux manual page
- open(2) — Linux manual page
相关文章
共享相同标签的最新文章。可以围绕相近的主题进一步加深理解。
ADR(Architecture Decision Record)入门 —— 在小规模开发中,用最简方法留住「为什么这样设计」
代码不会告诉你「为什么这样做」。本文讲解如何用 ADR(Architecture Decision Record),以「1 个决定 = 1 个 Markdown 文件」的方式记录设计判断的理由,并附带模板、写与不写的判断表,以及实际案例。
CSV 不是「纯文本」──C# 业务应用中 CSV 实务(字符编码・Excel 兼容性・注入攻击防范)
梳理业务应用 CSV 读写中常见的事故模式──用 Split(',') 自行解析、无 BOM 的 UTF-8 导致 Excel 乱码、前导零丢失、CSV 注入攻击──并从实务角度讲解 RFC 4180 规则、.NET 中 Shift_JIS 的处理方式(CodePagesE...
意外异常发生时,应用该终止还是继续的判断表
从状态破坏、外部副作用、线程、原生边界这几个角度,梳理发生意料之外的异常时,应该让应用终止还是继续运行。
Windows 应用程序开发的安全最低限度检查清单
面向 WPF / WinForms / WinUI / C++ / C# 的业务应用程序,以检查清单的形式梳理权限、签名、更新、机密信息、HTTPS、输入验证、DLL 加载、日志这些方面的基本要点。
FileSystemWatcher 实务指南:应对遗漏通知与重复通知
本文从遗漏通知、重复通知、完成判定的陷阱、重新扫描、原子式 claim、idempotency(幂等性)等角度,整理 FileSystemWatcher 的使用方法与注意事项。
相关主题
与本文相近的主题页面。以本文为起点,可以进一步了解相关服务和其他文章。
Windows 技术主题
汇整 KomuraSoft LLC 关于 Windows 开发、故障调查与既有资产活用文章的主题中心。
与本主题相关的服务
本文与以下服务页面相关联,欢迎从最接近的入口查看。
Windows 应用程序开发
在包含共享文件夹集成与夜间批处理的 Windows 应用开发中,互斥控制的设计会直接决定实现质量。
技术咨询 & 设计评审
如果希望先把锁、原子 claim、idempotency 之间的职责划分梳理清楚,可以按技术咨询与设计评审来处理。
常见问题
汇总了咨询这一主题时常见的问题。
- 文件集成的互斥控制只靠锁 API 就够了吗?
- 多数情况下并不够。Linux 的 flock 是 advisory lock,不遵守约定的一方照样可以写入;Windows 的 byte-range lock 在内存映射文件中会被忽略。操作系统级别的锁在同一应用群、同一前提下是有效的,但应当作为辅助手段来使用,基本做法是把 temp -> rename、done/manifest、原子 claim、idempotency 这类交接协议的设计作为主体。
- 如何避免文件被读取到写入中途的内容?
- 王道做法是通过 temp -> close -> rename/replace 来公开文件。把生成中的文件封闭在 temp 文件名下,close 之后再在同一目录中切换为 final 文件名,接收方只查看 final 文件名。前提是 temp 与 final 要放在同一目录,至少要在同一个卷 / 文件系统上,并约定:final 文件名一出现,内容就已经完成。
- 如何防止多个 worker 同时处理同一个文件?
- 在读取之前以原子方式获取 claim。具体做法是让只有把文件从 incoming 重命名到 processing/<worker>/ 成功的 worker 才进行处理。Exists -> Create 这种两段式检查,由于“确认”与“确保”是两个独立操作,中间会有其他进程插入,因此无法实现互斥。如果需要原子创建,可以使用 .NET 的 FileMode.CreateNew 系列或 POSIX 的 O_CREAT | O_EXCL。
- 使用 lock file 时有哪些注意事项?
- 不要只做成一个空文件,而应做成带有 ownerId、host、pid、acquiredAt、expiresAt、heartbeatAt 的、具有有效期的 lease(所有权信息)。创建要以原子方式进行,把停止更新作为 stale 判定的依据,删除原则上只由创建者进行,并预先假定会有解锁遗漏的情况,制定好恢复流程。不要指望靠一个 lock file 就能保证完全的一致性,最终以 idempotency 来兜底的设计在实务中更为可靠。