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);

相关链接

  • 7
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值