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列,这样就不用开发者手动来每次更新数据的时候,手动更新并发令牌的值了