EFCore 数据一致性保证-乐观锁和悲观锁

我们在项目当中,经常因为业务要求,需要对数据一致性进行控制,常用设计当中,一般需要使用悲观锁乐观锁

一. 悲观锁:

所谓悲观锁,就是在进行操作时针对记录加上排他锁,这样其他事务如果想操作该记录,需要等待锁的释放。

悲观锁在处理并发量和频繁访问时,等待时间比较长,冲突概率高,并发性能不好。

二. 乐观锁

乐观锁,是在提交对记录的更改时才将对象锁住,提交前需要检查数据的完整性。

据此,我们了解一下EFCore在并发控制上的实践策略。

EFCore本身实现了乐观锁控制,在保存数据时会进行一致性校验,分为两种模式:

2.1 [ConcurrencyCheck]

通过标识 [ConcurrencyCheck] 来检查数据一致性。

假如,我们有以下库存表:

// 库存表
public class Stock {
    
    
	// 商品ID
   [ConcurrencyCheck]
	public Guid GoodsId {
    
     get; set; }
	// 商品数量
	[ConcurrencyCheck]
	public int Num {
    
    set; get;}
}
  • 我们使用[ConcurrencyCheck]标识了商品Id和库存数量为一组乐观锁。

  • 首先,张三查询到商品Id 001库存为10个

  • 然后,张三想要修改为9个

  • 那么张三在SaveChange的时候,会去检查商品的数量是不是一开始查出来的样子,就会执行下面的语句:

update Goods set Num=9 where Id=001 and Num = 10

这个时候,如果商品数量10已经被改过,就会保存失败,这样我们就可以保证在商品数量被其他人修改后,现有保存就能够得到检查,而不是以读取到的数据为依据去判断更新。

2.2 [Timestamp]

另外一种更常用的,就是使用rowversion,给每行加一个版本,每次更新行数据时会同步更新rowversion。

EFCore在 SaveChange 的时候,会去验证 rowversion 是否和查询出来的一致,与上述流程同效,只不过验证字段不一样。

具体做法是:在类中添加 [Timestamp] 特性 和 TimeStamp 字段,如下:

// 库存表
public class Stock {
    
    
	// 商品ID
	public Guid GoodsId {
    
     get; set; }
	// 商品数量
	public int Num {
    
    set; get;}
	// rowversion,添加Timestamp特性标识
	[Timestamp]
    public Byte[] TimeStamp {
    
     get; set; }
}

当验证发生异常时,会抛出 DbUpdateConcurrencyException 异常,我们可以通过catch DbUpdateConcurrencyException 来实现具体的处理逻辑。

比如:

  • 抛出异常,提示用户数据已更改。
  • 继续执行修改,合并最新数据到现有更改。
  • 重试

2.3 乐观锁异常处理
乐观锁的DbUpdateConcurrencyException处理,一般有以下几种方式:

  • 客户端数据优先原则:
    客户端数据为主,覆盖数据库中的数据。
// ...
	using dbContext = new TestContext();
	bool saveFailed;
    do
    {
    
    
        saveFailed = false;
        // 查询库存数据
        var stockData = dbContext.Stock.FirstOrDefault(c => c.Id == Id);
        // 如果有库存,扣库存出库
		if (stockData.Num > 0)
    	{
    
    
          	stockData.Num--;
    	}
        try
        {
    
    
            dbContext.SaveChanges();
        }
        catch (DbUpdateConcurrencyException ex)
        {
    
    
         	saveFailed = true;
            // 更新值为当前客户端值
            var entry = ex.Entries.Single();
            entry.OriginalValues.SetValues(entry.GetDatabaseValues());
        }
    } while (saveFailed);
  • 数据库数据优先原则:
    重新数据库数据,覆盖本地数据
 	using dbContext = new TestContext();
 	bool saveFailed;
    do
    {
    
    
        saveFailed = false;
        // 查询库存数据
        var stockData = dbContext.Stock.FirstOrDefault(c => c.Id == Id);
        // 如果有库存,扣库存出库
		if (stockData.Num > 0)
    	{
    
    
          	stockData.Num--;
    	}
        try
        {
    
    
            dbContext.SaveChanges();
        }
        catch (DbUpdateConcurrencyException ex)
        {
    
    
            saveFailed = true;
            // 从数据库更新实体最新值,不加就会重复读内存实体值
            ex.Entries.Single().Reload();
        }
    } while (saveFailed);
  • 根据实际情况判断性处理
    实体有可能已被删除,已被修改,需要具体判断决定处理方法
 	using dbContext = new TestContext();
 	do
    {
    
    
        saveFailed = false;
        // 查询库存数据
        var stockData = dbContext.Stock.FirstOrDefault(c => c.Id == Id);
        // 如果有库存,扣库存出库
		if (stockData.Num > 0)
    	{
    
    
          	stockData.Num--;
    	}
        try
        {
    
    
            dbContext.SaveChanges();
        }
        catch (DbUpdateConcurrencyException ex)
        {
    
    
            saveFailed = true;
            var entry = ex.Entries.Single();
            if (entry.State == EntityState.Deleted)
                //EF删除项目时,其状态设置为“Detached”
                entry.State = EntityState.Detached;
            else
            	//EF更新项目时,以客户端数据为主处理
                entry.OriginalValues.SetValues(entry.GetDatabaseValues());
        }
    } while (saveFailed);

相关链接

猜你喜欢

转载自blog.csdn.net/csdn102347501/article/details/120551215