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

时间:2015-08-21 19:58:22

标签: c# entity-framework deadlock

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

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

我的想法是创建一个事务来获取值,计算一个新值并在db中更新它。只要提交新值,我想阻止该表。然后下一个可以获取并设置新值。

但经过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.RepeatableRead db.Database.ExecuteSqlCommand( "SELECT TOP 1 *FROM Tests WITH (TABLOCKX, HOLDLOCK)" );仅适用于交易。事实证明,我在没有任何交易的情况下尝试过此代码,结果与使用IsolationLevel.ReadCommited的交易相同。

特拉维斯,谢谢!

3 个答案:

答案 0 :(得分:3)

您遇到的现象可能是由两个因素之一引起的。没有在MSSQL中看到表设计本身就很难分辨。这些因素要么是脏读,要么是幻读。正在更改的值正在更改生效之前读取。这导致值增加两次,但两次都是从x - >增加的。 y,x-&gt; y而不是从x - >; y,y - &gt; z(这就是为什么你观察到一个小于预期总数的原因。

  

REPEATABLEREAD
  锁定放在查询中使用的所有数据上,防止其他用户更新数据。防止不可重复的读取,但仍然可以幻像行IsolationLevel Enumeration MDN(强调我的)

如果主键正在更改,则锁定将被释放,其行为类似于新行。然后可以读取该行,并且该值将是更新之前的值。这可能正在发生,虽然我认为它很可能是一个脏读。

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

为了防止这种情况,必须使用更严格的锁定。 Serializable似乎是这里的自然选择,因为它声明锁保持到事务完成,并且没有发出共享锁。但是,使用可序列化范围时仍可以进行脏读。这里唯一真正的选择是锁定整个表

https://stackoverflow.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();
        }
    }
}

答案 1 :(得分:1)

我会改用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'
}

答案 2 :(得分:0)

锁定......取2。

锁定整个表格是一件混乱的事情。所以我会用两种方法之一来解决这个问题:

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

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

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

这将保证'Value'只增加一次。请注意OUTPUT子句,它保证了干净的读取。该行被锁定以进行操作并读取。

2.。)使用RowVersions

为什么不让他们也尝试在ROWVERSION子句中识别ID为 AND WHERE的行。只要更新触及行(即使值未更改),ROWVERSION数据类型将始终更改。

虽然您可能会获得不再是该行的最新版本的更新,但是等待某些事情陷入僵局,这比IMHO更清晰。