当前位置: 首页 > news >正文

EF Core 并发冲突实战:乐观锁、RowVersion 与 DbUpdateConcurrencyException 怎么处理 - ryan

并发冲突是 EF Core 里最容易被忽视、出了事又最难排查的问题之一。这篇文章聊聊它的机制、怎么配置乐观锁、冲突异常怎么处理。

问题背景

真实场景:电商平台秒杀活动,同一件商品被多个请求并发扣减库存。业务日志里一切正常,但库存对不上——扣了 100 件,实际库存只减少了 60 件。

排查后发现:

  • 多个请求几乎同时读取了库存为 200 的记录
  • 各自在内存里把数量减掉后写回
  • 数据库里最后一个写入覆盖了前面所有写入的结果
  • EF Core 没有报任何错误,因为没有配置并发控制

这就是典型的"丢失更新"(Lost Update)。

原理解析

EF Core 的并发控制模型

EF Core 支持乐观并发,不在读取时加锁,而是在写入时检测:

提交更新时,把当前数据库中读取到的"令牌值"放进 WHERE 条件,如果这行在读取之后被别人改动过,令牌不匹配,受影响行数为 0,EF Core 就会抛出 DbUpdateConcurrencyException

生成的 SQL 大概长这样:

UPDATE Products
SET Stock = @newStock
WHERE Id = @id AND RowVersion = @originalRowVersion

如果 RowVersion 被别人改过,WHERE 匹配不到,更新语句影响 0 行,EF Core 检测到后抛异常。

两种并发令牌配置方式

方式一:[Timestamp] / IsRowVersion()(推荐)

数据库自动维护,每次行更新时自增,不需要应用层干预:

public sealed class Product
{public int Id { get; set; }public string Name { get; set; } = string.Empty;public int Stock { get; set; }[Timestamp]public byte[] RowVersion { get; set; } = [];
}

或者在 OnModelCreating 里配置:

modelBuilder.Entity<Product>().Property(p => p.RowVersion).IsRowVersion();

方式二:[ConcurrencyCheck](字段级令牌)

不需要专门的版本字段,直接把某个业务字段标为并发令牌:

public sealed class Seat
{public int Id { get; set; }[ConcurrencyCheck]public string? OccupiedBy { get; set; }
}

适合"只要这个字段没被改就允许写"的场景,但精度不如 RowVersion——其他字段改了不会触发冲突检测。

DbUpdateConcurrencyException 的结构

冲突发生时,异常里携带了足够的信息用于决策:

catch (DbUpdateConcurrencyException ex)
{foreach (var entry in ex.Entries){var proposedValues = entry.CurrentValues;   // 应用层想写入的值var originalValues = entry.OriginalValues;  // 应用层读取时的快照var databaseValues = await entry.GetDatabaseValuesAsync(); // 数据库现在的值}
}

三组值拿到手,才能做出合理的冲突解决策略。

示例代码

基础配置与迁移

public sealed class AppDbContext : DbContext
{public DbSet<Product> Products => Set<Product>();protected override void OnModelCreating(ModelBuilder modelBuilder){modelBuilder.Entity<Product>().Property(p => p.RowVersion).IsRowVersion();}
}

生成迁移后,SQL Server 会把 RowVersion 字段类型映射为 rowversion,PostgreSQL 对应 xmin 系统列(用法略有差异)。

冲突重试:客户端值优先

最常见的处理策略——应用层值直接覆盖数据库值,适合"最后写入者获胜"的运营后台场景:

public async Task UpdateStockAsync(int productId, int newStock, CancellationToken ct)
{const int maxRetries = 3;for (var attempt = 0; attempt < maxRetries; attempt++){try{var product = await db.Products.FindAsync([productId], ct)?? throw new InvalidOperationException($"Product {productId} not found.");product.Stock = newStock;await db.SaveChangesAsync(ct);return;}catch (DbUpdateConcurrencyException ex){if (attempt == maxRetries - 1) throw;// 刷新原始值快照,下一轮用新的 RowVersion 重试foreach (var entry in ex.Entries)await entry.ReloadAsync(ct);}}
}

冲突拒绝:数据库值优先

读取到冲突直接告知调用方,让用户重新决策,适合需要用户确认的场景:

public async Task<ConflictResult?> TryDeductStockAsync(int productId, int quantity, byte[] expectedRowVersion, CancellationToken ct)
{var product = await db.Products.FindAsync([productId], ct)?? throw new InvalidOperationException($"Product {productId} not found.");if (!product.RowVersion.SequenceEqual(expectedRowVersion))return new ConflictResult(product.Stock, product.RowVersion);if (product.Stock < quantity)throw new InvalidOperationException("库存不足");product.Stock -= quantity;try{await db.SaveChangesAsync(ct);return null; // 成功}catch (DbUpdateConcurrencyException){var dbValues = await db.Products.AsNoTracking().FirstAsync(p => p.Id == productId, ct);return new ConflictResult(dbValues.Stock, dbValues.RowVersion);}
}public record ConflictResult(int CurrentStock, byte[] CurrentRowVersion);

冲突合并:自定义字段级融合

拿到三组值后,按业务规则逐字段决策:

catch (DbUpdateConcurrencyException ex)
{foreach (var entry in ex.Entries){var proposed = entry.CurrentValues;var database = await entry.GetDatabaseValuesAsync();if (database is null)throw new InvalidOperationException("记录已被删除,无法合并。");// 用数据库里最新的 Stock,保留应用层改动的 Nameproposed["Stock"] = database["Stock"];// 把原始值快照更新为数据库当前值,下次提交时令牌匹配entry.OriginalValues.SetValues(database);}await db.SaveChangesAsync(ct);
}

总结

EF Core 的乐观并发基于"令牌比对"——写入时把读取时的快照值塞进 WHERE 条件,数据库决定这次更新是否有效。

认识这个机制的关键点有三个:

  1. RowVersion 是最省心的配置方式,让数据库自动维护版本
  2. DbUpdateConcurrencyException 里有三组值,决定了你能做什么样的冲突处理
  3. 冲突策略(重试、拒绝、合并)要结合业务场景选择,没有通用最优解

并发冲突不是 EF Core 的问题,是分布式写入的本质问题。搞清楚它,才能在出事时不慌。

http://www.jsqmd.com/news/472102/

相关文章:

  • IPED与AI模型部署:将模型集成到取证流程的5个关键步骤
  • favicons-webpack-plugin零配置使用:一行代码搞定全平台图标
  • SSDTTime与其他ACPI工具对比:为什么它能脱颖而出?
  • IPED时间线过滤预设:保存常用过滤条件的完整指南
  • 更改表的字符集,支持标签
  • 不止于昔日“核弹”:2026年Log4j漏洞的持久战与新战线
  • 从0到1使用React-Bulma-Components构建一个完整的React应用
  • IPED工作流导出导入:分享与复用流程配置的功能
  • 2026制造业短视频营销TOP5名单公布,无锡现状与趋势数据出炉。 - 精选优质企业推荐榜
  • 100元以内的香港云服务器,能支撑日均1万IP的电商站吗?
  • TIS插件文档生成:使用Swagger自动生成API文档
  • 【2026年制造业短视频营销TOP5趋势发布】 - 精选优质企业推荐榜
  • 嵌入式设备性能优化:基于RPi-Monitor的系统资源监控与调优策略
  • Sharry数据库设计与文件存储机制:深入理解数据持久化方案
  • 分期乐微信立减金如何快捷回收,教你三步解决! - 猎卡回收公众号
  • 为什么选择Bochs?跨平台x86模拟的核心优势解析
  • 最终最佳实践操作文档:统信UOS VSCode 全栈开发环境配置(基于 Chromium 浏览器)
  • 2026四川电缆回收哪家强?区域再生资源回收企业专业测评TOP榜 - 深度智识库
  • animatediff-cli-prompt-travel:AI动画创作新革命,让文字轻松变为流畅视频
  • ExAdmin关联关系处理:has_many与many_to_many的最佳实践
  • 2026年全国小型电动环卫车哪家好?可靠优质 实力强值得信赖 口碑佳适配各类场景 - 深度智识库
  • Vimperator新手入门:5分钟学会用Vim命令提升浏览器操作效率
  • yolo-tensorrt核心API解析:Detector类与Config结构体的使用技巧
  • testfixtures并行测试策略:4种方案助你大幅缩短测试时间
  • 淬炼数字内核,锻造智造未来:无锡哲讯以ERP解决方案赋能金属加工企业转型升级
  • matrixmultiplication.xyz部署教程:本地搭建交互式矩阵乘法学习环境
  • 为什么选择Xorbits?5大核心优势彻底解决Python数据科学扩展性难题
  • 2026年大健康礼盒包装厂家推荐:养生保健/滋补品/高档保健品礼盒专业供应商 - 品牌推荐官
  • 2026香港求职机构哪家靠谱实力榜:投行四大内推资源深度对比(真实案例/防坑) - Matthewmx
  • ALHacking v3新特性详解:VirusCrafter与N-ANOM功能体验