我们在项目当中,经常因为业务要求,需要对数据一致性进行控制,常用设计当中,一般需要使用悲观锁或乐观锁。
一. 悲观锁:
所谓悲观锁,就是在进行操作时针对记录加上排他锁,这样其他事务如果想操作该记录,需要等待锁的释放。
悲观锁在处理并发量和频繁访问时,等待时间比较长,冲突概率高,并发性能不好。
二. 乐观锁
乐观锁,是在提交对记录的更改时才将对象锁住,提交前需要检查数据的完整性。
据此,我们了解一下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);
相关链接: