惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

Threat Intelligence Blog | Flashpoint
Threat Intelligence Blog | Flashpoint
V
V2EX
大猫的无限游戏
大猫的无限游戏
腾讯CDC
博客园 - Franky
WordPress大学
WordPress大学
Jina AI
Jina AI
GbyAI
GbyAI
云风的 BLOG
云风的 BLOG
B
Blog RSS Feed
Last Week in AI
Last Week in AI
The Cloudflare Blog
V
Visual Studio Blog
P
Proofpoint News Feed
博客园 - 叶小钗
L
LangChain Blog
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
Recorded Future
Recorded Future
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
T
The Blog of Author Tim Ferriss
人人都是产品经理
人人都是产品经理
Y
Y Combinator Blog
罗磊的独立博客
雷峰网
雷峰网
博客园 - 【当耐特】
Microsoft Security Blog
Microsoft Security Blog
L
LINUX DO - 热门话题
Cisco Talos Blog
Cisco Talos Blog
L
Lohrmann on Cybersecurity
Martin Fowler
Martin Fowler
Spread Privacy
Spread Privacy
MongoDB | Blog
MongoDB | Blog
Engineering at Meta
Engineering at Meta
C
Cybersecurity and Infrastructure Security Agency CISA
小众软件
小众软件
CTFtime.org: upcoming CTF events
CTFtime.org: upcoming CTF events
cs.CV updates on arXiv.org
cs.CV updates on arXiv.org
Recent Announcements
Recent Announcements
T
Threat Research - Cisco Blogs
Security Archives - TechRepublic
Security Archives - TechRepublic
量子位
cs.AI updates on arXiv.org
cs.AI updates on arXiv.org
宝玉的分享
宝玉的分享
D
DataBreaches.Net
T
The Exploit Database - CXSecurity.com
Vercel News
Vercel News
IT之家
IT之家
Exploit-DB.com RSS Feed
Exploit-DB.com RSS Feed
T
Troy Hunt's Blog
aimingoo的专栏
aimingoo的专栏

博客园 - 邓磊Lei

为什么 Go 一个 HTTP 服务可以同时处理数万连接 gin怎么处理一次http请求的 我的经验:git提交信息分别用什么icon C# 也能像 Python 一样写脚本 | .NET 10 构建基于文件的应用 .NET 10 使用 Microsoft.AspNetCore.OpenApi 实现 API 版本管理 ASP.NET Core 内存缓存实战:一篇搞懂该怎么配、怎么避坑 Microsoft Agent Framework + Kimi API 实战:控制台应用跑通单次与多轮 Agent 对话 开发实战:asp.net core + ef core 实现动态可扩展的分页方案 聊聊 ASP.NET Core 中间件和过滤器的区别 Python 入门:从“其他语言”到 Pythonic 思维的完整迁移手册 .NET 进阶之路:异步、并发与内存管理的系统性认知 Redis:延迟双删的适用边界与落地细节 Serilog:从结构化日志认知到 .NET 工程落地 EF Core 原生 SQL 实战:FromSql、SqlQuery 与对象映射边界 EF Core 拦截器实战:SaveChangesInterceptor、CommandInterceptor 与审计落地 ASP.NET Core 外部依赖调用治理实战:HttpClientFactory、Polly 与幂等边界 .NET .Result 避坑指南:不同框架下的死锁与线程池饥饿 EF Core 慢查询排查实战:TagWith、OpenTelemetry、执行计划,30 分钟定位性能瓶颈 EF Core 写入链路深拆:从 ChangeTracker 到 SQL Batch 的性能诊断与优化 ASP.NET Core 认证鉴权实战:JWT、Policy 与权限边界怎么落地 C# 异步编程深水区:Task、ValueTask、线程池饥饿与背压设计 EF Core 查询性能黑洞:Include、投影与跟踪策略的边界 从 IApplicationBuilder 到 RequestDelegate:ASP.NET Core 请求管线的性能与可观测性实战 如何在 rider 中配置 SonarQube
EF Core 并发冲突实战:乐观锁、RowVersion 与 DbUpdateConcurrencyException 怎么处理
邓磊Lei · 2026-03-13 · via 博客园 - 邓磊Lei

并发冲突是 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,保留应用层改动的 Name
        proposed["Stock"] = database["Stock"];

        // 把原始值快照更新为数据库当前值,下次提交时令牌匹配
        entry.OriginalValues.SetValues(database);
    }

    await db.SaveChangesAsync(ct);
}

总结

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

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

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

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