EF Core 乐观、悲观并发控制

目录

并发控制的概念

悲观并发控制

实现

问题

乐观并发控制

实现

RowVersion

实体类及配置

概念

总结


并发控制的概念

  1. 并发控制:避免多个用户同时操作资源造成的并发冲突问题。举例:统计点击量。
  2. 最好的解决方案:非数据库解决方案。
  3. 数据库层面的两种策略:悲观、乐观。

悲观并发控制

悲观并发控制一般采用行锁、表锁等排他锁对资源进行锁定,确保同时只有一个使用者操作被锁定的资源。

EF Core没有封装悲观并发控制的使用,需要开发人员编写原生SQL语句来使用悲观并发控制。不同数据库的语法不一样。

实现

SQL Server:select * from T_Houses with(updlock) where id=1

如果有其他的查询操作也使用行级锁来查询Id=1的这条数据的话,那些查询就会被挂起,一直到针对这条数据的更新操作完成从而释放这个行锁,代码才会继续执行。

锁是和事务相关的,因此通过BeginTransactionAsync()创建一个事务,并且在所有操作完成后调用CommitAsync()提交事务。

cs 复制代码
public class Program
{
    class House
    {
        public long Id { get; set; }
        public string Name { get; set; }
        public string? Owner { get; set; }
    }

    static async Task Main(string[] args)
    {
        //方法1
        //var tom = ChooseHouseAsync("Tom");
        //var lucy = ChooseHouseAsync("Lucy");
        //await Task.WhenAll(tom, lucy);

        //方法2
        ChooseHouseAsync("Tom");
        await ChooseHouseAsync("Lucy");
    }

    public static async Task ChooseHouseAsync(string name)
    {
        using (MyDbContext ctx = new MyDbContext())
        using (var tx = await ctx.Database.BeginTransactionAsync())
        {
            Console.WriteLine($"{name}:准备开启查询锁{DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff")}");
            var h = await ctx.Houses.FromSqlInterpolated($"select * from T_Houses with (updlock) where id=1").FirstOrDefaultAsync();
            Console.WriteLine($"{name}:查询锁开启成功{DateTime.Now}.ToString(\"yyyy-MM-dd HH:mm:ss.fff\")");
            if (!string.IsNullOrEmpty(h.Owner))
            {
                if (h.Owner == name)
                {
                    Console.WriteLine($"{name}:房子已经被你抢到了");
                }
                else
                {
                    Console.WriteLine($"{name}:房子已经被{h.Owner}占了");
                }
                return;
            }
            await Task.Delay(5000);
            h.Owner = name;
            Console.WriteLine($"{name}:恭喜你抢到了");
            await ctx.SaveChangesAsync();//执行完释放锁
            Console.WriteLine($"{name}:释放锁{DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff")}");
            await tx.CommitAsync();
        }
    }
}

问题

  1. 悲观并发控制的使用比较简单;
  2. 锁是独占、排他的,如果系统并发量很大的话,会严重影响性能,如果使用不当的话,甚至会导致死锁。
  3. 不同数据库的语法不一样。

乐观并发控制

Update T_Houses set Owner=新值 where Id=1 and Owner=旧值

当Update的时候,如果数据库中的Owner值已经被其他操作者更新为其他值了,那么where语句的值就会为false,因此这个Update语句影响的行数就是0,EF Core就知道"发生并发冲突"了,因此SaveChanges()方法就会抛出DbUpdateConcurrencyException异常。

把被并发修改的属性使用IsConcurrencyToken()设置为并发令牌。

cs 复制代码
builder.Property(h => h.Owner).IsConcurrencyToken();

实现

cs 复制代码
public static async Task ChooseHouseAsync(string name)
{
    Console.WriteLine($"{name}开始执行");
    using (MyDbContext ctx = new MyDbContext())
    {
        Console.WriteLine($"{name}:准备查询{DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff")}");
        var h = await ctx.Houses.FromSqlInterpolated($"select * from T_Houses where id=1 and (Owner is null or Owner='')").FirstOrDefaultAsync();
        Console.WriteLine($"{name}:查询完成{DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff")}");
        if (!string.IsNullOrEmpty(h.Owner))
        {
            if (h.Owner == name)
            {
                Console.WriteLine($"{name}:房子已经被你抢到了");
            }
            else
            {
                Console.WriteLine($"{name}:房子已经被{h.Owner}占了");
            }
            return;
        }
        await Task.Delay(5000);
        h.Owner = name;
        try
        {
            await ctx.SaveChangesAsync();
        }
        catch (DbUpdateConcurrencyException ex)
        {
            var entry=ex.Entries.First();
            var dbValues=await entry.GetDatabaseValuesAsync();
            string newOwner=dbValues.GetValue<string>(nameof(House.Owner));
            Console.WriteLine($"{name}:并发访问冲突,被{newOwner}抢走了");
        }
    }
}

RowVersion

  1. SQLServer数据库可以用一个byte[]类型的属性做并发令牌属性,然后使用IsRowVersion()把这个属性设置为RowVersion类型,这样这个属性对应的数据库列就会被设置为ROWVERSION类型。对于ROWVERSION类型的列,在每次插入或更新行时,数据库会自动为这一行的ROWVERSION类型的列其生成新值。
  2. 在SQLServer中,timestamp和rowversion是同一种类型的不同别名而已。

实体类及配置

cs 复制代码
class House
{
    public long Id { get; set; }
    public string Name { get; set; }
    public string? Owner { get; set; }
    public byte[] RowVersion { get; set; }
}

builder.Property(o=>o.RowVersion).IsRowVersion();

概念

  1. 在MySQL等数据库中虽然也有类似的timestamp类型,但是由于timestamp类型的精度不够,并不适合在高并发的系统。
  2. 非SQLServer中,可以将并发令牌列的值更新为Guid的值。
  3. 修改其他属性值的同时,使用h1.RowVersion = Guid.NewGuid()手动更新并发令牌属性的值。

总结

  1. 乐观并发控制能够避免悲观锁带来的性能、死锁等问题,因此推荐使用乐观并发控制而不是悲观锁。
  2. 如果有一个确定的字段要被进行并发控制,那么使用IsConcurrencyToken()把这个字段设置为并发令牌即可;
  3. 如果无法确定一个唯一的并发令牌列,那么就可以引入一个额外的属性设置为并发令牌,并且在每次更新数据的时候,手动更新这一列的值。如果用的是SQLServer数据库,那么也可以采用RowVersion列,这样就不用开发者手动来在每次更新数据的时候,手动更新并发令牌的值了。
相关推荐
[email protected]2 天前
ASP.NET Core 反射动态发现类库服务
后端·asp.net·.netcore
江沉晚呤时2 天前
深入探析C#设计模式:访问者模式(Visitor Pattern)的原理与应用
java·服务器·开发语言·数据库·.netcore
盗理者2 天前
.net Core 和 .net freamwork 调用 deepseek api 使用流输出文本(对话补全)
.net·.netcore
江沉晚呤时4 天前
深入解析策略模式在C#中的应用与实现
java·服务器·开发语言·前端·.netcore
时光追逐者5 天前
一款基于 .NET 8 + Vue 开源的、企业级中后台权限管理系统
前端·vue.js·microsoft·开源·c#·.net·.netcore
时光追逐者6 天前
C#/.NET/.NET Core技术前沿周刊 | 第 33 期(2025年4.1-4.6)
c#·.net·.netcore
江沉晚呤时8 天前
如何深入理解C#中的备忘录模式(Memento Pattern)设计模式
运维·服务器·数据库·c#·.netcore
[email protected]8 天前
ASP.NET Core Web API 参数传递方式
后端·asp.net·.netcore
[email protected]8 天前
ASP.NET Core Web API 中 HTTP状态码的分类及对应的返回方法
http·asp.net·.netcore
全栈小512 天前
【C#】.net core 6.0 依赖注入常见问题之一,在构造函数使用的类,都需要注入到容器里,否则会提示如下报错,让DeepSeek找找原因,看看效果
c#·.netcore·依赖注入·deepseek