
为什么直接用 Polly.Wrap 会失败
文件 IO 操作(比如 File.ReadAllText、FileStream.Write)通常抛出的是 IOException、UnauthorizedAccessException 或 DirectoryNotFoundException,但 Polly 默认策略只捕获 Exception 基类——这本身没问题;真正踩坑的是:**某些文件异常(如被其他进程锁定)在重试瞬间仍会立即复现,且没有“冷却时间”或“退避逻辑”,导致重试形同虚设**。
更隐蔽的问题是:Polly 的 PolicyWrap 如果嵌套了 Retry 和 CircuitBreaker,而你没在重试前释放文件句柄(比如忘了 using 或 Dispose),下次重试时可能因句柄未释放继续报 IOException: The process cannot access the file...。
实操建议:
- 必须用
Policy.Handle<ioexception>().Or<unauthorizedaccessexception>()</unauthorizedaccessexception></ioexception>显式声明要捕获的异常类型,避免漏掉常见文件锁异常 - 重试策略必须搭配
WaitAndRetryAsync+ 指数退避(如Backoff.DecorrelatedJitterBackoffV2),不能只用固定间隔 - 所有文件操作必须包裹在
using或确保Dispose被调用,否则重试只会让问题恶化
如何写一个安全的重试型 File.ReadAllText
直接对 File.ReadAllText(path) 套 Polly 是危险的——它内部可能已打开文件但没暴露流供你控制生命周期。正确做法是自己构造可重试的流读取逻辑。
示例(使用 FileStream + StreamReader):
var retryPolicy = Policy
.Handle<IOException>()
.Or<UnauthorizedAccessException>()
.WaitAndRetryAsync(
retryCount: 3,
sleepDurationProvider: (retryAttempt) =>
TimeSpan.FromMilliseconds(100 * Math.Pow(2, retryAttempt))
);
<p>string content = await retryPolicy.ExecuteAsync(async () =>
{
using var fs = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.Read, 4096, FileOptions.Asynchronous);
using var sr = new StreamReader(fs);
return await sr.ReadToEndAsync();
});
关键点:
- 把
FileStream创建和StreamReader初始化都放在ExecuteAsync内部,确保每次重试都是全新句柄 - 显式指定
FileOptions.Asynchronous,避免同步阻塞干扰重试调度 - 缓冲区大小(
4096)建议设为 4KB 或 8KB,太小会放大 I/O 开销,太大无益
Directory.CreateDirectory 重试要注意什么
Directory.CreateDirectory 看似幂等,但在网络路径或权限瞬变场景下仍可能失败。它的典型错误是 UnauthorizedAccessException 或 IOException(如父目录不存在且无法创建)。
但它有个隐藏特性:**即使抛异常,部分中间目录可能已被创建成功**。所以重试前不能简单“再试一次”,得先检查目标路径是否存在。
实操建议:
- 重试策略应包含
.CanRetryImmediately((ex, ct) => !Directory.Exists(path))判断(需自定义策略逻辑,或手动加前置检查) - 不要对整个路径链盲目重试;优先确认最深层缺失的父目录,再逐级创建
- 若用于 UNC 路径(如
\servershareolder),必须确保运行账户有网络访问权限,Polly 无法绕过这个限制
异步文件写入(WriteAllTextAsync)怎么加重试
File.WriteAllTextAsync 底层会创建新文件并覆盖,但如果目标文件正被记事本、Excel 或杀毒软件占用,就会抛 IOException。注意:它不会自动处理“文件存在但只读”的情况——那会抛 UnauthorizedAccessException,需额外捕获。
推荐写法(带存在性与只读处理):
var policy = Policy
.Handle<IOException>()
.Or<UnauthorizedAccessException>()
.WaitAndRetryAsync(2, _ => TimeSpan.FromMilliseconds(50));
<p>await policy.ExecuteAsync(async () =>
{
if (File.Exists(path) && (File.GetAttributes(path) & FileAttributes.ReadOnly) == FileAttributes.ReadOnly)
{
File.SetAttributes(path, FileAttributes.Normal);
}
await File.WriteAllTextAsync(path, content);
});
容易忽略的点:
-
File.SetAttributes本身也可能失败(如权限不足),所以它应该放在重试范围内,而不是前置一次性操作 - 如果写入内容很大,考虑改用
FileStream分块写 +FlushAsync,避免单次大写入失败后全部重放 - 不要在重试中重复生成临时文件名——比如用
Path.GetTempFileName(),否则每次重试都会留垃圾文件
重试不是万能胶水,文件 IO 的不确定性主要来自外部系统(OS 锁、防病毒扫描、网络延迟)。最可靠的策略永远是:先检查再操作,失败后等一会儿再检查,而不是无脑重试。










