Entity Framework中的悲观并发控制

5

在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();
    }

在此之后,所有想要增加计数器值的线程都会从数据库中获取实际值,因为它们会等待表格空闲。

正如我所说,这是我的解决方法,也许我哪里错了。这就是为什么我想请你指出更好的解决方案,因为我不想重复造轮子。


在此场景下,为什么不能使用乐观并发控制呢? - Ismail Hawayel
那是我的问题。我怎样才能使用实体框架来解决这个问题? - BootGenius
Entity Framework支持乐观并发,您可以将[Concurrency]属性应用于Value属性,这样当线程尝试更新计数器的旧值时,就会导致并发异常。 - Ismail Hawayel
我也遇到了同样的情况,我想使用悲观并发控制,唯一的方法是使用第一个 T-SQL 查询来阻塞,然后使用第二个查询来获取实体,进行包含、处理它们,最后保存更改。随着 EF7 的推出,我们似乎可以在 EF 中使用提示,因此我希望能够避免使用查询来阻塞行。 - Álvaro García
1个回答

0
在您的情况下,您可以在TransactionScope using块中使用一个解决方法来处理这种特定情况,如下所示:
const string updateStr = "update table set counter = counter + 1";
context.ExecuteSqlCommand(updateStr);
context.SaveChanges();

1
这不是一个选项。因为我的例子被简化以解释问题。如果您的业务需求不仅仅是增加计数器,而且例如需要在锁定表中执行更多工作,那么您仍将面临此问题。 - BootGenius

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接