如何在 EF6.1 中为一个简单的操作阻塞一个表而没有死锁?

Posted

技术标签:

【中文标题】如何在 EF6.1 中为一个简单的操作阻塞一个表而没有死锁?【英文标题】:How to block a table in EF6.1 for one simple operation without deadlocks? 【发布时间】:2015-11-15 20:59:36 【问题描述】:

我对 EF6.1 和 SQL Server 的行为感到非常沮丧和困惑。

我想要一张表,其中包含(目前)一行,其中包含我想用于其他任务的值。每次用户执行此功能时,我都想获取值的内容 - 为简单起见只是一个 int - 并设置一个新值。因为我想将此值也用作其他表的主键,所以它必须是唯一的并且应该是无间隙的。

我的想法是创建一个交易来获取价值,计算一个新的并在数据库中更新它。只要提交了新值,我就想阻止该表。然后下一个可以获取并设置新的值。

但经过 5 小时的测试后,它仍然无法正常工作。 为了模拟(仅 3 次)同时访问,我想创建一个控制台应用程序并使用内置 Thread.Sleep(200) 启动它 3 次。

这是我正在测试的代码:

[Table("Tests")]
public class Test

    public int Id  get; set; 
    public int Value  get; set; 

该表已经存在,并且其中有一行 Id = 1, Value = 0 。

在我的控制台应用程序中,我有:

for( int i = 1; i <= 200; i++ )

    Thread.Sleep( 200 );
    using( var db = new MyDbContext( connectionString ) )
    
        using( var trans = db.Database.BeginTransaction( IsolationLevel.RepeatableRead ) )
        

            var entry = db.Tests.Find( 1 );
            db.Entry( entry ).Entity.Value += 1;

            // Just for testing
            Console.WriteLine( string.Format( "0,5", i ) + "   " + string.Format( "0,7", db.Entry( entry ).Entity.Value ) );

            db.SaveChanges();
            trans.Commit();
        
    

我的问题: 有时启动第二个应用程序就足够了,我会遇到死锁。 我尝试将隔离级别设置为IsolationLevel.ReadCommited,但在这样做之后我得到了重复。 我怎么完全不知道我做错了什么。有人可以帮帮我吗?

更新

当我将 for 带入事务时,第二个启动的应用程序将等待第一个应用程序运行(大约 20 秒),然后开始读取和更新值。这按预期工作,但为什么上面的“模拟”不起作用?

using( var db = new MyDbContext( connectionString ) )

    using( var trans = db.Database.BeginTransaction( IsolationLevel.Serializable ) )
    
        for( int i = 1; i <= 2000; i++ )
        
            var entry = db.Tests.Find( 1 );
            db.Entry( entry ).Entity.Value += 1;

            // Just for testing
            Console.WriteLine( string.Format( "0,5", i ) + "   " + string.Format( "0,7", db.Entry( entry ).Entity.Value ) );

            db.SaveChanges();
        
        trans.Commit();
    

解决方案

感谢Travis J这里的解决方案:

for( int i = 1; i <= 2000; i++ )

    using( var db = new MyDbContext( connectionString ) )
    
        using( var trans = db.Database.BeginTransaction( IsolationLevel.Serializable ) )
        

            db.Database.ExecuteSqlCommand( "SELECT TOP 1 *FROM Tests WITH (TABLOCKX, HOLDLOCK)" );

            var entry = db.Tests.Find( 1 );
            db.Entry( entry ).Entity.Value += 1;

            // Just for testing
            Console.WriteLine( string.Format( "0,5", i ) + "   " + string.Format( "0,7", db.Entry( entry ).Entity.Value ) );

            db.SaveChanges();
            trans.Commit();
        
    

...让我添加评论: 在我的情况下,它也适用于IsolationLevel.RepeatableReaddb.Database.ExecuteSqlCommand( "SELECT TOP 1 *FROM Tests WITH (TABLOCKX, HOLDLOCK)" ); 仅适用于事务内部。事实证明,我在没有任何事务的情况下尝试了此代码,结果与使用 IsolationLevel.ReadCommited 的事务相同。

特拉维斯,谢谢!

【问题讨论】:

如何为您的测试表定义键?它是 id 和 value 的组合吗? @TravisJ 不,如上所示。我在上面的代码中使用 Code First,之后没有对数据库进行任何更改。有问题吗? 【参考方案1】:

您遇到的现象可能是由以下两个因素之一引起的。如果不查看 MSSQL 中的表设计本身,就很难判断。这些因素要么是脏读,要么是幻读。正在更改的值在更改生效之前被读取。这会导致值增加两次,但两次都是从 x -> y, x-> y 开始,而不是从 x -> y, y -> z 开始(这就是为什么您观察到的数字小于预期总数的原因。

可重复读取: 对查询中使用的所有数据进行锁定,以防止其他用户更新数据。防止不可重复读取,但仍然可能出现幻行IsolationLevel Enumeration MDN(强调我的)

如果主键发生变化,锁将被释放,并且会像新行一样起作用。然后可以读取该行,并且该值将是更新之前的值。这可能正在发生,尽管我认为这很可能是脏读。

这里可能会发生脏读,因为该行仅在执行查询时才被锁定,也就是说,在获取和保存期间,会执行查询。所以在这之间,当实际值被修改时,可能会读取数据库中的值并导致 10 变成 11 两次。

为了防止这种情况,必须使用更严格的锁。可序列化在这里似乎是自然的选择,因为它声明锁会一直保持到事务完成,并且不会发出共享锁。但是,在使用可序列化作用域时,仍然可能出现脏读。这里唯一真正的选择是锁定整个表

https://***.com/a/22721849/1026459也同意,并给出示例代码:

entities.Database.ExecuteSqlCommand(
    "SELECT TOP 1 KeyColumn FROM MyTable WITH (TABLOCKX, HOLDLOCK)"
);

这会转化为您的这种情况(请注意,我还将隔离更改为可序列化以希望进一步加强锁定)

for( int i = 1; i <= 200; i++ )

    Thread.Sleep( 200 );
    using( var db = new MyDbContext( connectionString ) )
    
        using( var trans = db.Database.BeginTransaction( IsolationLevel.Serializable ) )
        

            db.Database.ExecuteSqlCommand(
                "SELECT TOP 1 FROM Test WITH (TABLOCKX, HOLDLOCK)"
            );

            var entry = db.Tests.Find( 1 );
            db.Entry( entry ).Entity.Value += 1;

            // Just for testing
            Console.WriteLine( string.Format( "0,5", i ) + "   " + string.Format( "0,7", db.Entry( entry ).Entity.Value ) );

            db.SaveChanges();
            trans.Commit();
        
    

【讨论】:

对下一位读者稍作修正:在我的例子中,sql 命令是"SELECT TOP 1 * FROM Tests WITH (TABLOCKX, HOLDLOCK)"非常感谢! 对我来说坏消息:我已经测试过这条线,但它没有工作......显然当时我还有另一个问题。最后但并非最不重要的一点:你能告诉我,为什么我的 Update 中的代码在没有 tablelock 的情况下工作? 我在想rollback在哪里?!由于DbContextTransactionusing 块包围,所以不需要调用trans.Rollback()?对吗? @AliBagheriShakib 因为他使用的是using,所以不需要在try...catch-phrase 中调用trans.Rollback();。它在usingDispose 中自动调用。有关更详细的说明,请参阅this answer。【参考方案2】:

我会改用 TransactionScope。人们会警告您,它将升级为使用分布式事务,但这只是在它需要它的情况下。任何内部事务(如使用 SaveChanges() 创建的事务)都应将其自身纳入外部环境范围。

TransactionScope 创建代码

public static TransactionScope CreateTransactionScope(TransactionScopeOption transactionScopeOption = TransactionScopeOption.Required)
        
            var transactionOptions = new TransactionOptions
            
                IsolationLevel = System.Transactions.IsolationLevel.ReadCommitted,
                Timeout = TransactionManager.MaximumTimeout
            ;

            return new TransactionScope(transactionScopeOption, transactionOptions);
        

用法

using(var transaction = CreateTransactionScope())
//do stuff

transaction.Complete();  //don't forget this...and it's 'complete' not 'commit'

【讨论】:

我试过你的代码,但它产生的结果与.BeginTransaction() 相同。我以 3 秒的延迟启动了该应用程序 5 次。当值为 0 时,预期结果为 1000,因为增量为 5*200。但结果是:855。这意味着 145 个值被使用了两次。 我想我明白你在说什么。事物的“读取”方面没有锁定。如果您将其设为单个 sql 语句,您将得到事务保证,甚至不必搞乱包装事务或任何东西。更新 dbo.Tests 设置值 = 值 + 1【参考方案3】:

锁定...采取 2。

锁定整个表是一件麻烦事。所以我会通过以下两种方式之一来解决这个问题:

1.) 在SQL语句中执行操作

您可以简单地增加更新语句中的值。

UPDATE dbo.Tests
SET Value = Value + 1
OUTPUT inserted.Id, inserted.Value
WHERE ...

这将保证“值”只增加一次。请注意OUTPUT 子句,它保证了干净的读取。该行被锁定以供操作和读取。

2.) 使用 RowVersions

为什么不让他们也尝试在WHERE 子句中识别具有 ID AND ROWVERSION 的行。每当更新触及行时,ROWVERSION 数据类型总是会改变(即使值没有改变)。

虽然您可能会获得不再是该行最新版本的更新,但恕我直言,这是一种比等待某些东西死锁更干净的方法。

【讨论】:

嗯,好主意...我会在接下来的几天里尝试这个,并提供反馈。

以上是关于如何在 EF6.1 中为一个简单的操作阻塞一个表而没有死锁?的主要内容,如果未能解决你的问题,请参考以下文章

如何在 PL/SQL 中连接两个表而不创建新表

如何参数化 SQL 表而不会受到 SQL 注入的影响

将两行插入数据库表而不是一行

如何创建临时表而不丢失 django 中的 ORM?

如何删除所有表而不是 Postgresql 中的模式?

如何在EF6.1中过滤掉子集合