EF Core 并发冲突实战:乐观锁、RowVersion 与 DbUpdateConcurrencyException 怎么处理
并发冲突是 EF Core 里最容易被忽视、出了事又最难排查的问题之一。这篇文章聊聊它的机制、怎么配置乐观锁、冲突异常怎么处理。问题背景真实场景电商平台秒杀活动同一件商品被多个请求并发扣减库存。业务日志里一切正常但库存对不上——扣了 100 件实际库存只减少了 60 件。排查后发现多个请求几乎同时读取了库存为 200 的记录各自在内存里把数量减掉后写回数据库里最后一个写入覆盖了前面所有写入的结果EF Core 没有报任何错误因为没有配置并发控制这就是典型的丢失更新Lost Update。原理解析EF Core 的并发控制模型EF Core 支持乐观并发不在读取时加锁而是在写入时检测提交更新时把当前数据库中读取到的令牌值放进 WHERE 条件如果这行在读取之后被别人改动过令牌不匹配受影响行数为 0EF Core 就会抛出DbUpdateConcurrencyException。生成的 SQL 大概长这样UPDATEProductsSETStocknewStockWHEREIdidANDRowVersionoriginalRowVersion如果RowVersion被别人改过WHERE 匹配不到更新语句影响 0 行EF Core 检测到后抛异常。两种并发令牌配置方式方式一[Timestamp]/IsRowVersion()推荐数据库自动维护每次行更新时自增不需要应用层干预publicsealedclassProduct{publicintId{get;set;}publicstringName{get;set;}string.Empty;publicintStock{get;set;}[Timestamp]publicbyte[]RowVersion{get;set;}[];}或者在OnModelCreating里配置modelBuilder.EntityProduct().Property(pp.RowVersion).IsRowVersion();方式二[ConcurrencyCheck]字段级令牌不需要专门的版本字段直接把某个业务字段标为并发令牌publicsealedclassSeat{publicintId{get;set;}[ConcurrencyCheck]publicstring?OccupiedBy{get;set;}}适合只要这个字段没被改就允许写的场景但精度不如RowVersion——其他字段改了不会触发冲突检测。DbUpdateConcurrencyException 的结构冲突发生时异常里携带了足够的信息用于决策catch(DbUpdateConcurrencyExceptionex){foreach(varentryinex.Entries){varproposedValuesentry.CurrentValues;// 应用层想写入的值varoriginalValuesentry.OriginalValues;// 应用层读取时的快照vardatabaseValuesawaitentry.GetDatabaseValuesAsync();// 数据库现在的值}}三组值拿到手才能做出合理的冲突解决策略。示例代码基础配置与迁移publicsealedclassAppDbContext:DbContext{publicDbSetProductProductsSetProduct();protectedoverridevoidOnModelCreating(ModelBuildermodelBuilder){modelBuilder.EntityProduct().Property(pp.RowVersion).IsRowVersion();}}生成迁移后SQL Server 会把RowVersion字段类型映射为rowversionPostgreSQL 对应xmin系统列用法略有差异。冲突重试客户端值优先最常见的处理策略——应用层值直接覆盖数据库值适合最后写入者获胜的运营后台场景publicasyncTaskUpdateStockAsync(intproductId,intnewStock,CancellationTokenct){constintmaxRetries3;for(varattempt0;attemptmaxRetries;attempt){try{varproductawaitdb.Products.FindAsync([productId],ct)??thrownewInvalidOperationException($Product{productId}not found.);product.StocknewStock;awaitdb.SaveChangesAsync(ct);return;}catch(DbUpdateConcurrencyExceptionex){if(attemptmaxRetries-1)throw;// 刷新原始值快照下一轮用新的 RowVersion 重试foreach(varentryinex.Entries)awaitentry.ReloadAsync(ct);}}}冲突拒绝数据库值优先读取到冲突直接告知调用方让用户重新决策适合需要用户确认的场景publicasyncTaskConflictResult?TryDeductStockAsync(intproductId,intquantity,byte[]expectedRowVersion,CancellationTokenct){varproductawaitdb.Products.FindAsync([productId],ct)??thrownewInvalidOperationException($Product{productId}not found.);if(!product.RowVersion.SequenceEqual(expectedRowVersion))returnnewConflictResult(product.Stock,product.RowVersion);if(product.Stockquantity)thrownewInvalidOperationException(库存不足);product.Stock-quantity;try{awaitdb.SaveChangesAsync(ct);returnnull;// 成功}catch(DbUpdateConcurrencyException){vardbValuesawaitdb.Products.AsNoTracking().FirstAsync(pp.IdproductId,ct);returnnewConflictResult(dbValues.Stock,dbValues.RowVersion);}}publicrecordConflictResult(intCurrentStock,byte[]CurrentRowVersion);冲突合并自定义字段级融合拿到三组值后按业务规则逐字段决策catch(DbUpdateConcurrencyExceptionex){foreach(varentryinex.Entries){varproposedentry.CurrentValues;vardatabaseawaitentry.GetDatabaseValuesAsync();if(databaseisnull)thrownewInvalidOperationException(记录已被删除无法合并。);// 用数据库里最新的 Stock保留应用层改动的 Nameproposed[Stock]database[Stock];// 把原始值快照更新为数据库当前值下次提交时令牌匹配entry.OriginalValues.SetValues(database);}awaitdb.SaveChangesAsync(ct);}总结EF Core 的乐观并发基于令牌比对——写入时把读取时的快照值塞进 WHERE 条件数据库决定这次更新是否有效。认识这个机制的关键点有三个RowVersion是最省心的配置方式让数据库自动维护版本DbUpdateConcurrencyException里有三组值决定了你能做什么样的冲突处理冲突策略重试、拒绝、合并要结合业务场景选择没有通用最优解并发冲突不是 EF Core 的问题是分布式写入的本质问题。搞清楚它才能在出事时不慌。
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2409482.html
如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!