gpt4 book ai didi

c# - 如何在没有死锁的情况下为一个简单的操作阻塞 EF6.1 中的表?

转载 作者:行者123 更新时间:2023-11-30 15:24:53 25 4
gpt4 key购买 nike

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

特拉维斯,谢谢你!

最佳答案

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

RepeatableRead:
Locks are placed on all data that is used in a query, preventing other users from updating the data. Prevents non-repeatable reads but phantom rows are still possible. IsolationLevel Enumeration MDN (emphasis mine)

如果主键发生变化,锁将被释放并类似于新行。然后可以读取该行,并且该值将是更新之前的值。这可能会发生,尽管我认为这很可能是脏读。

这里可能会发生脏读,因为该行仅在查询执行时被锁定,也就是说,在获取期间和保存期间,执行查询。因此,在修改实际值期间,可能会读取数据库中的值并导致 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();
}
}
}

关于c# - 如何在没有死锁的情况下为一个简单的操作阻塞 EF6.1 中的表?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/32148386/

25 4 0
Copyright 2021 - 2024 cfsdn All Rights Reserved 蜀ICP备2022000587号
广告合作:1813099741@qq.com 6ren.com