我对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
的交易相同。
答案 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更清晰。