C# 实体框架下的悲观并发

C# 实体框架下的悲观并发,c#,sql,multithreading,entity-framework,C#,Sql,Multithreading,Entity Framework,在关于实体框架的文档中,有人写到它不支持开箱即用的悲观并发。这就是为什么在使用LINQtoEntities语法从表中读取实体时无法锁定表的原因 让我们设想下一个任务:有多个线程共享数据库中的一个表。表具有下一个结构: Table Counter Name : varchar[10] Value : bigint 每个线程都希望使用name=“some name”获取计数器,获取其计数器值,然后增加计数器值并保存回数据库 在这里,我们可以看到一个问题,所有这些线程都可以从数据库中读取相同的值并增

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

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

Table Counter
Name : varchar[10]
Value : bigint
每个线程都希望使用name=“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();
    }
    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();
    }
const string updateStr = "update table set counter = counter + 1";
context.ExecuteSqlCommand(updateStr);
context.SaveChanges();
非隔离级别允许我们在从表中读取记录后锁定表。在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();
    }
}
公共静态类DataContextensions
{
公共静态void锁表(此DbContext上下文),其中tenty:class
{
var tableName=(上下文为IObjectContextAdapter).ObjectContext.CreateObjectSet().EntitySet.Name;
List topEntity=context.Database.SqlQuery(String.Format(“使用(UPDLOCK,HOLDLOCK)”从{0}中选择TOP1*,tableName)).ToList();
}
}
现在,我可以在阅读以下内容后想要锁定表时使用此方法:

    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();
    }
    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();
    }
const string updateStr = "update table set counter = counter + 1";
context.ExecuteSqlCommand(updateStr);
context.SaveChanges();
使用(var transactionScope=new transactionScope(TransactionScopeOption.Required,new TransactionOptions{IsolationLevel=System.Transactions.IsolationLevel.ReadCommitted}))
{
使用(var context=new FrontierDbContext())
{
LockTable();
var counter=context.Counters.First();
《睡眠》(2000年);
计数器.Value++;
WriteLine(“Value={0}”,counter.Value);
SaveChanges();
}
transactionScope.Complete();
}
在这之后,所有想要增加计数器值的线程都将从数据库中获得实际值,因为它们将等待表释放

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

在您的情况下,您可以在TransactionScope using block中针对以下特定情况使用变通方法:

    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();
    }
    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();
    }
const string updateStr = "update table set counter = counter + 1";
context.ExecuteSqlCommand(updateStr);
context.SaveChanges();

这不是一个选择。因为我的例子被简化来解释这个问题。如果您的业务需求不仅仅是增加一个计数器,例如对锁定表做更多的工作,那么您仍然会遇到这个问题。有什么原因不能在这种情况下使用乐观并发吗?这是我的问题。如何使用Entity Framework解决此问题?Entity Framework支持乐观并发,您可以将[concurrency]属性应用于您的Value属性,这将导致线程尝试更新计数器的旧值时出现并发异常,我也是如此,我想使用悲观并发,我发现它的独特方式是使用第一个t-sql查询来阻止,第二个查询来获取实体,进行包含,使用它们,然后保存更改。对于EF7,我们似乎可以使用EF的提示,所以我希望可以避免使用查询来阻止行。