如何处理EF Core中乐观锁RowVersion引发的DbUpdateConcurrencyException并发冲突?
摘要:并发冲突是 EF Core 里最容易被忽视、出了事又最难排查的问题之一。这篇文章聊聊它的机制、怎么配置乐观锁、冲突异常怎么处理。 问题背景 真实场景:电商平台秒杀活动,同一件商品被多个请求并发扣减库存。业务日志里一切正常,但库存对不上——扣
并发冲突是 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 检测到后抛异常。
两种并发令牌配置方式
方式一:1773447150 / IsRowVersion()(推荐)
数据库自动维护,每次行更新时自增,不需要应用层干预:
public sealed class Product
{
public int Id { get; set; }
public string Name { get; set; } = string.Empty;
public int Stock { get; set; }
1773447150
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(); // 数据库现在的值
}
}
三组值拿到手,才能做出合理的冲突解决策略。
