You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

PostgreSQL+EF Core 8事务并发锁问题求解决方案

并发场景下的事务条件验证问题

场景示例

存在三层实体类A、B、C,层级关系为A包含多个B,B包含多个C,实体定义如下:

public class A
{
    public Guid Id { get; set; }
    public List<B> Bs { get; set; } = [];
    public string Name { get; set; }
}

public class B
{
    public Guid Id { get; set; }
    public Guid AId { get; set; }
    public A A { get; set; }
    public List<C> Cs { get; set; } = [];
}

public class C
{
    public Guid Id { get; set; }
    public Guid BId { get; set; }
    public B B { get; set; }
    public string Name { get; set; }
}

业务需求

修改A的请求需满足以下条件:

  • A必须包含3个B实例
  • 每个B实例都包含1个名称以小写字母开头的C实例

当前实现(PostgreSQL + EF Core 8)

当前实现逻辑:

MainDbContext db1 = ...;
A a = db1.A.First(...);

await using var t1 = await db1.Database.BeginTransactionAsync(IsolationLevel.ReadUncommitted);
a.Name = "Anew";

await db1.SaveChangesAsync();

if (db1.As.Any(m => m.Id == aId && m.Bs.Count() == 3 && m.Bs.All(n => n.Cs.Count == 1 && n.Cs.First().Name == "c")))
    await t1.CommitAsync();
else
    await t1.RollbackAsync();

问题描述

事务提交/回滚前存在时间间隙,期间其他进程可能对B或C执行删除、添加或修改操作,导致条件判断失效,做出错误的提交/回滚决策。

尝试过的方案

曾尝试使用Serializable隔离级,但该级别要求所有请求都使用才能生效,无法满足当前场景需求。

完整测试代码

using System.Data;
using System.Data.Common;
using System.Text.RegularExpressions;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Diagnostics;

var builder = WebApplication.CreateBuilder(args);

// Add services to the container.
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();
builder.Services.AddDbContext<MainDbContext>(m => m.UseNpgsql(builder.Configuration.GetConnectionString("DB")));
var app = builder.Build();
var db1 = app.Services.CreateScope().ServiceProvider.GetRequiredService<MainDbContext>();
var db2 = app.Services.CreateScope().ServiceProvider.GetRequiredService<MainDbContext>();
var db3 = app.Services.CreateScope().ServiceProvider.GetRequiredService<MainDbContext>();
await db3.Database.EnsureCreatedAsync();
await db3.Database.EnsureDeletedAsync();
await db3.Database.MigrateAsync();
var aId = Guid.NewGuid();
db3.As.Add(new A()
{
    Id = aId,
    Name = "A",
    Bs =
    [
        new B()
        {
            Cs =
            [
                new C()
                {
                    Name = "c"
                }
            ]
        },
        new B()
        {
            Cs =
            [
                new C()
                {
                    Name = "c"
                }
            ]
        },
        new B()
        {
            Cs =
            [
                new C()
                {
                    Name = "c"
                }
            ]
        }
    ]

});
await db3.SaveChangesAsync();

if (Equals(db1, db2))
    throw new InvalidOperationException();
await using var t1 = await db1.Database.BeginTransactionAsync(IsolationLevel.ReadUncommitted);
await using var t2 = await db2.Database.BeginTransactionAsync(IsolationLevel.ReadUncommitted);
var aFromDb1 = await db1.As.Include(m => m.Bs).FirstAsync();
var aFromDb2 = await db2.As.Include(m => m.Bs).FirstAsync();

aFromDb1.Name = "Anew";
await db1.SaveChangesAsync();

db2.Cs.Where(m=> m.BId == db2.Bs.First().Id).ExecuteDelete();
await t2.CommitAsync();

if (db1.As.Any(m => m.Id == aId && m.Bs.Count() == 3 && m.Bs.All(n => n.Cs.Count == 1 && n.Cs.First().Name == "c")))
    await t1.CommitAsync();
else
    await t1.RollbackAsync();

return;


public class MainDbContext : DbContext
{
    public DbSet<A> As { get; set; }
    public DbSet<B> Bs { get; set; }
    public DbSet<C> Cs { get; set; }

    public MainDbContext(DbContextOptions<MainDbContext> dbContextOptions) : base(dbContextOptions)
    {
    }
}

public class A
{
    public Guid Id { get; set; }
    public List<B> Bs { get; set; } = [];
    
    public string Name { get; set; }
}

public class B
{
    public Guid Id { get; set; }
    public Guid AId { get; set; }
    public A A { get; set; }

    public List<C> Cs { get; set; } = [];
}

public class C
{
    public Guid Id { get; set; }

    public Guid BId { get; set; }
    public B B { get; set; }

    public string Name { get; set; }
}

解决方案

方案1:使用乐观锁(版本号)

在实体A、B、C中添加版本号字段,EF Core会自动处理并发冲突检测:

public class A
{
    public Guid Id { get; set; }
    public List<B> Bs { get; set; } = [];
    public string Name { get; set; }
    [Timestamp]
    public byte[] Version { get; set; }
}

// B和C类同样添加[Timestamp]属性的Version字段

修改逻辑时,先加载包含关联数据的A,验证条件后再执行更新,若期间有其他进程修改相关数据,SaveChangesAsync会抛出DbUpdateConcurrencyException,此时捕获异常并回滚:

await using var t1 = await db1.Database.BeginTransactionAsync();
var a = await db1.As.Include(m => m.Bs).ThenInclude(n => n.Cs)
                  .FirstAsync(m => m.Id == aId);

// 先验证条件
if (a.Bs.Count != 3 || !a.Bs.All(n => n.Cs.Count == 1 && char.IsLower(n.Cs.First().Name[0])))
{
    await t1.RollbackAsync();
    return;
}

a.Name = "Anew";
try
{
    await db1.SaveChangesAsync();
    await t1.CommitAsync();
}
catch (DbUpdateConcurrencyException)
{
    await t1.RollbackAsync();
    // 处理并发冲突,比如重试或返回错误
}

方案2:在事务中锁定相关资源

使用PostgreSQL的SELECT ... FOR UPDATE语句锁定A及其关联的B、C数据,确保事务期间其他进程无法修改这些数据。EF Core中可通过FromSqlRaw实现:

await using var t1 = await db1.Database.BeginTransactionAsync(IsolationLevel.RepeatableRead);

// 锁定A、关联的B和C
var a = await db1.As.FromSqlRaw(@"
    SELECT * FROM ""As"" 
    WHERE ""Id"" = {0} 
    FOR UPDATE
", aId)
.Include(m => m.Bs)
.ThenInclude(n => n.Cs)
.FirstAsync();

// 验证条件
if (a.Bs.Count != 3 || !a.Bs.All(n => n.Cs.Count == 1 && char.IsLower(n.Cs.First().Name[0])))
{
    await t1.RollbackAsync();
    return;
}

a.Name = "Anew";
await db1.SaveChangesAsync();
await t1.CommitAsync();

FOR UPDATE会锁定查询到的行,直到事务结束,其他进程修改这些行时会被阻塞,直到当前事务提交或回滚。

方案3:将更新与条件判断合并为原子操作

直接编写SQL语句,将条件判断和更新合并为一个原子操作,避免中间间隙:

await using var t1 = await db1.Database.BeginTransactionAsync();

var affectedRows = await db1.Database.ExecuteSqlAsync(@"
    UPDATE ""As"" 
    SET ""Name"" = 'Anew'
    WHERE ""Id"" = {0}
    AND (
        SELECT COUNT(*) FROM ""Bs"" WHERE ""AId"" = {0}
    ) = 3
    AND NOT EXISTS (
        SELECT 1 FROM ""Bs"" 
        WHERE ""AId"" = {0}
        AND (
            SELECT COUNT(*) FROM ""Cs"" WHERE ""BId"" = ""Bs"".""Id""
        ) != 1
        OR (
            SELECT ""Name"" FROM ""Cs"" WHERE ""BId"" = ""Bs"".""Id"" LIMIT 1
        ) !~ '^[a-z]'
    )
", aId);

if (affectedRows > 0)
{
    await t1.CommitAsync();
}
else
{
    await t1.RollbackAsync();
}

这个SQL会在同一个原子操作中完成条件验证和更新,不存在中间间隙,只有当所有条件满足时才会更新A的名称。


内容的提问来源于stack exchange,提问作者Иван Иван

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.20 08:05:54