实体框架的悲观并发

Posted

技术标签:

【中文标题】实体框架的悲观并发【英文标题】:Pessimistic concurrency with Entity Framework 【发布时间】:2014-04-02 14:07:24 【问题描述】:

在有关实体框架的文档中写道,它不支持开箱即用的悲观并发。这就是为什么在使用 Linq to Entities 语法从表中读取实体时无法锁定表的原因。

假设我们有下一个任务:数据库中有多个线程共享一个表。表具有下一个结构:

Table Counter
Name : varchar[10]
Value : bigint

每个线程都想获取 name = "some name" 的计数器,获取它的计数器值,而不是增加它并保存回数据库。

在这里我们可以看到一个问题,所有这些线程都可以从数据库中读取相同的值并增加它。假设我们有 5 个线程。初始计数器值为1。如果所有线程都读取计数器,则它们都得到值1。然后它们增加它的值并将其保存回数据库。最后我们的计数器值等于 2。但期望值是 6,因为我们希望每个线程都从数据库中获取真正的值。

我认为可以像这样使用具有高度隔离性的事务范围来解决这个问题:

    using (var transactionScope = new TransactionScope(TransactionScopeOption.Required, new TransactionOptionsIsolationLevel = 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();
    

没有任何隔离级别允许我们在读取记录后锁定表。在事务 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 TransactionOptionsIsolationLevel = 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();
    

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

正如我所说,这是我的解决方法,也许我在某个地方错了。这就是为什么我想请您指出更好的解决方案。因为我不想重新发明*** 提前致谢!

【问题讨论】:

这种场景有什么原因不能使用乐观并发吗? 那是我的问题。如何使用 Entity Framework 解决这个问题? 实体框架支持乐观并发,您可以将 [Concurrency] 属性应用到您的 Value 属性,这应该会导致您在线程尝试更新计数器的旧值时获得并发异常, 我在同样的情况下,我想使用悲观并发,我发现它的独特方法是使用第一个 t-sql 查询来阻止和第二个查询来获取实体,做包括,与他们一起工作,然后保存费用。用EF7好像我们可以用EF使用hints,所以我希望可以避免使用查询来阻塞行。 【参考方案1】:

在您的情况下,您可以在 TransactionScope using 块中针对这种特定情况使用解决方法,如下所示:

const string updateStr = "update table set counter = counter + 1";
context.ExecuteSqlCommand(updateStr);
context.SaveChanges();

【讨论】:

这不是一个选项。因为我的示例被简化以解释问题。如果你的业务需求不仅仅是增加一个计数器,例如对锁表做更多的工作,你仍然会遇到这个问题

以上是关于实体框架的悲观并发的主要内容,如果未能解决你的问题,请参考以下文章

实体框架并发令牌 DateTime 类型的问题

并发问题实体框架继承类

使用实体框架和存储过程进行并发检查

hibernate基础24:乐观锁和悲观锁

hibernate基础24:乐观锁和悲观锁

EF实体框架之CodeFirst七