【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中,timestamprowversion是同一种类型的不同别名而已。

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()手动更新并发令牌属性的值。

posted @ 2022-04-22 01:13  .Neterr  阅读(1224)  评论(0编辑  收藏  举报