EF Core 悲观与乐观并发控制、并发令牌
EF Core 悲观并发控制(不推荐使用,EF Core也没有对其进行封装)
并发控制的概念
1.并发控制:避免多个用户同时操作资源造成的并发冲突问题。例如,统计点击量,秒杀,抢票
2.最好的解决方案:非数据库解决方案。
3.数据库层面的两种策略,悲观,乐观。
1.悲观并发控制一般采用行锁、表锁等排他锁对资源进行锁定,确保同时只有一个使用者操作被锁定的资源
2.EF Core没有封装悲观并发控制的使用,需要开发人员编写原生sql语句来使用悲观并发控制,不同数据库的语法不一样。
House与HouseConfig
class House
{
public long Id { get; set; }
public string Name { get; set; }
public string? Owner { get; set; }
}
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
class HouseConfig : IEntityTypeConfiguration<House>
{
public void Configure(EntityTypeBuilder<House> builder)
{
builder.ToTable("T_Houses");
builder.Property(h => h.Name).IsUnicode();
}
}
Program
using Microsoft.EntityFrameworkCore;
Console.WriteLine("请输入您的姓名");
string name = Console.ReadLine();
using MyDbContext ctx = new MyDbContext();
//开启事务
using var tx = await ctx.Database.BeginTransactionAsync();
Console.WriteLine("准备Select " + DateTime.Now.TimeOfDay);
//select xxx for update 是MySQL的 其他数据库不一样需要自己单独了解
var h1 = await ctx.Houses.FromSqlInterpolated($"select * from T_Houses where Id=1 for update")
.SingleAsync();
Console.WriteLine("完成Select " + DateTime.Now.TimeOfDay);
if (string.IsNullOrEmpty(h1.Owner))
{//房子没有所属 则可以抢购
//为了模拟并发 等待时间
await Task.Delay(5000);
h1.Owner = name;
await ctx.SaveChangesAsync();
Console.WriteLine("抢到手了");
}
else
{
if (h1.Owner == name)
{
Console.WriteLine("这个房子已经是你的了,不用抢");
}
else
{
Console.WriteLine($"这个房子已经被{h1.Owner}抢走了");
}
}
//提交事务
await tx.CommitAsync();
Console.ReadKey();
测试:编译完成后去对应的目录下找到对应的exe文件,运行2个,分别输入2个名字,例如Tom,Jerry
总结:
1.悲观并发控制的使用比较简单;
2.锁是独占、排他的,如果系统并发量很大的话,会严重影响性能,如果使用不当的话,甚至会导致死锁
3.不同的数据库的语法不一样
EF Core 乐观并发控制、并发令牌
并发令牌就是对当前一组数据(简单理解一行)的一个版本标记器(类似于软件版本的意思)
例如我要对这个House的所有者Owner进行修改,当这个房子进行预售的时候,Tom,Jerry进行抢购的时候,如果Owner不为空的时候,是应该修改失败的
--此时Owner是空的情况下 (Tom)
select * from T_Houses where id=1
Update T_Houses set Owner='Tom' where id=1 and Owner='空'
--Tom已经抢到 Owner=Tom
--程序同时进行 (Jerry也在抢购) Owner为空
select * from T_Houses where id=1
--此时修改的时候 Owner=Tom 更新失败 EF Core抛出异常DbUpdateConcurrencyException对它进行catch即可
Update T_Houses set Owner='Jerry' where id=1 and Owner='空'
--此时Owner就是 并发令牌
House与HouseConfig
class House
{
public long Id { get; set; }
public string Name { get; set; }
public string? Owner { get; set; }
}
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
class HouseConfig : IEntityTypeConfiguration<House>
{
public void Configure(EntityTypeBuilder<House> builder)
{
builder.ToTable("T_Houses");
builder.Property(h => h.Name).IsUnicode();
builder.Property(h => h.Owner).IsConcurrencyToken();
}
}
Program
using Microsoft.EntityFrameworkCore;
Console.WriteLine("请输入您的姓名");
string name = Console.ReadLine();
using MyDbContext ctx = new MyDbContext();
var h1 = await ctx.Houses.SingleAsync(h => h.Id == 1);
if (string.IsNullOrEmpty(h1.Owner))
{
await Task.Delay(5000);
h1.Owner = name;
try
{
await ctx.SaveChangesAsync();
Console.WriteLine("抢到手了");
}
catch (DbUpdateConcurrencyException ex)
{
var entry = ex.Entries.First();
var dbValues = await entry.GetDatabaseValuesAsync();
string newOwner = dbValues.GetValue<string>(nameof(House.Owner));
Console.WriteLine($"并发冲突,被{newOwner}提前抢走了");
}
}
else
{
if (h1.Owner == name)
{
Console.WriteLine("这个房子已经是你的了,不用抢");
}
else
{
Console.WriteLine($"这个房子已经被{h1.Owner}抢走了");
}
}
Console.ReadLine();
ABA 问题
空–>Tom–>空–>Jerry
Guid->Tom–>Guid–>Jerry(更新失败) 不会产生这种ABA问题,因为并发令牌的每次生成的是唯一的
当Tom抢购之后,Tom又卖给了开发商 此时Owner=空 如果此时 Jerry更新数据,这种情况下是可以更新成功的,如果自己的系统不介意这种,可以不处理。如果介意,SQL server可以用RowVersion
//实体类属性加上
public byte[] RowVersion { get; set; }
//在对应的HouseConfig里面加上
builder.Property(h => h.RowVersion).IsRowVersion();
总结:
1.乐观并发控制能够避免悲观锁带来的性能、死锁等问题,因此推荐使用乐观锁并发控制而不是悲观锁
2.如果有一个确定的字段要被进行并发控制,那么使用IsConcurrencyToken()把这个字段设置为并发令牌即可;
3.如果无法确定一个唯一的并发令牌列,那么就可以引入一个额外的属性设置为并发令牌,并且每次更新数据的时候,手动更新这一列如果用的是SQL server数据库,那么也可以采用IsRowVersion列,这样就不用开发者手动来每次更新数据的时候,手动更新并发令牌的值了