如何实现EF Core数据库与Azure Blob存储操作失败后的双向回滚?
问题答复
1. 你朋友给出的参考方案不可行,存在明显缺陷
Task.WhenAll执行多个异步任务时,如果多个任务同时抛出异常,最终会返回包含所有异常的AggregateException,但你写的单类型异常捕获逻辑只能触发第一个匹配到的异常的回滚分支,另一个操作的残留资源不会被清理,比如数据库和Blob同时报错时,只会回滚Blob,数据库未提交的变更不会处理,反之亦然。- 没有考虑回滚操作本身失败的场景,也没有处理程序中途崩溃导致两个操作只完成一半、没有触发异常捕获的极端情况。
- 如果是更新旧数据的场景,回滚逻辑需要用到操作前的旧数据快照,参考方案完全没有涉及快照留存的逻辑,无法实现真正的回滚。
2. 无法直接使用统一事务回滚能力,必须手动实现回滚逻辑
Azure Blob存储和你的业务数据库属于完全异构的独立资源,没有兼容的分布式事务管理器(比如DTC)支持跨系统的事务协调,所以不可能靠框架自带的事务能力直接实现两者的统一回滚,必须手动编写对应的回滚方法。
可行实现建议
推荐优先用先后执行的补偿逻辑(比并行实现更稳妥,出问题概率更低)
根据你是新增还是更新数据,可选择对应的流程:
新增场景(上传新文件+插入新数据库记录)
public async Task<ActionResult> Upload(<parameter-lists>) { // 原有鉴权、文件校验逻辑 string tempBlobName = Guid.NewGuid().ToString(); BlobClient tempBlobClient = containerClient.GetBlobClient(tempBlobName); try { // 第一步:先上传文件到临时Blob位置 await tempBlobClient.UploadAsync(/* 文件流 */); // 第二步:开启EF Core本地事务 using var transaction = await dbContext.Database.BeginTransactionAsync(); try { // 插入数据库记录,记录正式Blob路径 dbContext.Add(/* 你的实体 */); await dbContext.SaveChangesAsync(); // 第三步:移动临时Blob到正式位置(或重命名为正式名称) var formalBlobClient = containerClient.GetBlobClient(/* 正式Blob名 */); await formalBlobClient.StartCopyFromUriAsync(tempBlobClient.Uri); await tempBlobClient.DeleteAsync(); // 第四步:提交数据库事务 await transaction.CommitAsync(); } catch { // 数据库操作/Blob移动失败,回滚事务+删除临时Blob await transaction.RollbackAsync(); await tempBlobClient.DeleteIfExistsAsync(); throw; } } catch { // 临时Blob上传失败直接清理 await tempBlobClient.DeleteIfExistsAsync(); throw; } // 返回成功响应 }
更新场景(更新已存文件+更新已存数据库记录)
- 操作前先留存旧数据库记录快照、旧Blob内容快照
- 先执行数据库更新(不提交事务),再执行Blob更新
- 两者都成功则提交数据库事务,删除旧快照
- 任意一步失败则回滚数据库事务,用留存的快照恢复旧Blob内容
额外兜底方案
添加定时后台任务,定期扫描:
- 存在超过24小时的临时Blob直接删除
- 数据库里不存在对应记录的孤立Blob直接删除
内容的提问来源于stack exchange,提问作者NNguyen
相关产品推荐
相关产品推荐

