【EF Core】并发(悲观锁、乐观锁)
悲观锁
EF Core没有封装悲观并发控制的使用,需要开发人员编写原生SQL语句来使用悲观并发控制。不同数据库的语法不一样。
Console.WriteLine("请输入您的姓名");
string name = Console.ReadLine();
using MyDbContext ctx = new MyDbContext();
using var tx = await ctx.Database.BeginTransactionAsync();
Console.WriteLine("准备Select " + DateTime.Now.TimeOfDay);
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();
乐观锁
两种方案:并发令牌、RowVersion
如果有一个确定的字段要被进行并发控制,那么使用IsConcurrencyToken()
把这个字段设置为并发令牌即可;
如果无法确定一个唯一的并发令牌列,那么就可以引入一个额外的属性设置为并发令牌,并且在每次更新数据的时候,手动更新这一列的值。如果用的是SQLServer数据库,那么也可以采用RowVersion列,这样就不用开发者手动来在每次更新数据的时候,手动更新并发令牌的值了。
并发令牌只能监控一个字段,RowVersion可监控整条记录
并发令牌
举例子。当Update的时候,如果数据库中的Owner值已经被其他操作者更新为其他值了,那么where语句的值就会为false,因此这个Update语句影响的行数就是0,EF Core就知道“发生并发冲突”了,因此SaveChanges()方法就会抛出DbUpdateConcurrencyException异常。
1、把被并发修改的属性使用IsConcurrencyToken()设置为并发令牌。
builder.Property(h => h.Owner).IsConcurrencyToken();
2、
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();
RowVersion
SQLServer数据库可以用一个byte[]
类型的属性做并发令牌属性,然后使用IsRowVersion()
把这个属性设置为RowVersion
类型,这样这个属性对应的数据库列就会被设置为ROWVERSION
类型。对于ROWVERSION
类型的列,在每次插入或更新行时,数据库会自动为这一行的ROWVERSION
类型的列其生成新值。
在SQLServer中,timestamp
和rowversion
是同一种类型的不同别名而已。
class House
{
public long Id { get; set; }
public string Name { get; set; }
public string Owner { get; set; }
public byte[] RowVer { get; set; }
}
builder.Property(h => h.RowVer).IsRowVersion();
在MySQL等数据库中虽然也有类似的timestamp类型,但是由于timestamp类型的精度不够,并不适合在高并发的系统。
非SQLServer中,可以将并发令牌列的值更新为Guid的值。
修改其他属性值的同时,使用h1.RowVer = Guid.NewGuid()手动更新并发令牌属性的值。