在Entity Framework的文档中写道,它没有默认支持悲观并发控制。因此,使用Linq to Entities语法从表中读取实体时,没有锁定表的方法。
假设我们有以下任务:多个线程共享一个数据库中的一张表。该表的结构如下:
Table Counter
Name : varchar[10]
Value : bigint
每个线程都希望获取名称为"some name"的计数器,获得它的计数值,然后增加它并保存回数据库。
这里我们可以看到一个问题,就是在同一时间内,所有这些线程都可以从数据库中读取相同的值并增加它。假设我们有5个线程。初始计数器值为1。如果所有线程都读取计数器,则它们都会得到值1。然后它们增加它的值并将其保存回数据库。最终我们得到的计数器值等于2。但期望值应该是6,因为我们希望每个线程都从数据库中获得真实值。
我认为可以使用高隔离级别的事务范围来解决此问题,如下所示:
using (var transactionScope = new TransactionScope(TransactionScopeOption.Required, new TransactionOptions{IsolationLevel = System.Transactions.IsolationLevel.Serializable}))
{
using (var context = new TestDbContext())
{
var counter = context.Counters.First();
Thread.Sleep(2000);
counter.Value ++;
Console.WriteLine("Value={0}", counter.Value);
context.SaveChanges();
}
transactionScope.Complete();
}
所有的隔离级别都不允许我们在从表中读取记录后对其进行锁定。在Transact SQL中,我们有UPDLOCK和HOLDLOCK关键字,可以使我们锁定表直至事务完成。
因此,我创建了一个DbContext的扩展方法来解决这个问题:
public static class DataContextExtensions
{
public static void LockTable<TEntity>(this DbContext context) where TEntity : class
{
var tableName = (context as IObjectContextAdapter).ObjectContext.CreateObjectSet<TEntity>().EntitySet.Name;
List<TEntity> topEntity = context.Database.SqlQuery<TEntity>(String.Format("SELECT TOP 1 * from {0} WITH (UPDLOCK, HOLDLOCK)", tableName)).ToList();
}
}
现在我可以像这样使用这种方法来在读取后锁定表:
using (var transactionScope = new TransactionScope(TransactionScopeOption.Required, new TransactionOptions{IsolationLevel = System.Transactions.IsolationLevel.ReadCommitted}))
{
using (var context = new FrontierDbContext())
{
context.LockTable<Counter>();
var counter = context.Counters.First();
Thread.Sleep(2000);
counter.Value ++;
Console.WriteLine("Value={0}", counter.Value);
context.SaveChanges();
}
transactionScope.Complete();
}
在此之后,所有想要增加计数器值的线程都会从数据库中获取实际值,因为它们会等待表格空闲。
正如我所说,这是我的解决方法,也许我哪里错了。这就是为什么我想请你指出更好的解决方案,因为我不想重复造轮子。