我正在研究的特定实现存在问题。
我有一个创建新上下文,查询表并从表中获取“ LastNumberUsed”的基本方法,然后对该数字执行一些基本检查,然后最终递增和写回-所有这些都在事务内进行。
我已经编写了一个使用Parallel.For的基本测试应用程序来执行此方法5次。
使用Isolation.Serialization,我发现运行此代码时遇到很多死锁错误。
我已经阅读了一些有关此主题的文章,并尝试将隔离级别更改为快照。我不再遇到死锁,而是发现我收到隔离更新冲突错误。
我真的无所适从。每笔交易大约需要0.009秒才能完成,因此我一直想将代码包装在try..catch中,检查死锁错误并再次运行,但是这感觉像是一个混乱的解决方案。
是否有人对如何处理此问题有任何想法(最好是经验)?
我创建了一个控制台应用程序来演示这一点。
在程序主程序中,我运行以下代码:
Parallel.For(0, totalRequests,
x => TestContract(x, contractId, incrementBy, maxRetries));
TestContract方法如下所示:
//Define the context
using (var context = new Entities())
{
//Define a new transaction
var options = new TransactionOptions {IsolationLevel = IsolationLevel.Serializable};
using (var scope = new TransactionScope(TransactionScopeOption.Required, options))
{
//Get the contract details
var contract = (
from c in context.ContractRanges
where c.ContractId == contractId
select c).FirstOrDefault();
//Simulate activity
Threading.Thread.sleep(50);
//Increment the contract number
contract.Number++;
//Save the changes made to the context
context.SaveChanges();
//Complete the scope
scope.Complete();
}
}
}
最佳答案
暂时搁置隔离级别,让我们集中讨论代码在做什么:
您正在并行运行5个任务,它们调用TestContract
并为所有任务传递相同的contractId
,对吗?
在TestContract
中,通过其contract
提取id
,对其进行一些处理,然后增加合同的Number
属性。
所有这些都在交易范围内。
为什么会陷入僵局?
为了了解您为何陷入僵局,了解Serializable
隔离级别的含义很重要。
documentation of SQL Server Isolation Levels关于Serializable
(强调我的)说以下内容:
语句无法读取已被其他事务修改但尚未提交的数据。
在当前事务完成之前,没有其他事务可以修改当前事务已读取的数据。
其他事务无法插入新行,其键值将落在当前语句中任何语句读取的键范围内
交易,直到当前交易完成。
范围锁放置在与
在事务中执行的每个语句的搜索条件。这个
阻止其他交易更新或插入任何行
将有资格获得当前执行的任何语句
交易。这意味着如果事务中有任何语句
再次执行时,它们将读取相同的行集。的
范围锁将保持到事务完成为止。这是最
限制了隔离级别,因为它锁定了
锁住并保持锁,直到交易完成。因为
并发性较低,仅在必要时使用此选项。这个选项
与在所有SELECT中的所有表上设置HOLDLOCK具有相同的效果
交易中的报表。
回到您的代码,就本示例而言,假设您只有两个并行运行的任务,即TaskA
和TaskB
与contractId=123
,所有任务都在具有Serializable
隔离级别的事务下进行。
让我们尝试描述此执行中的代码发生了什么:
TaskA开始
TaskB启动
TaskA创建具有可序列化隔离级别的Transaction 1234
TaskB创建具有可序列化隔离级别的Transaction 5678
TaskA生成一个SELECT * FROM ContractRanges WHERE ContractId = 123
。
这一点。 SQL Server在ContractRanges
表中ContractId = 123
所在的行中放置一个锁,以防止其他事务对该数据进行突变。
TaskB做出相同的SELECT
语句,并将lock
放在ContractId = 123
表的ContractRanges
行中。
因此,在这一点上,我们在同一行上有两个锁,每个创建的事务一个。
然后,TaskA增加合同的Number
TaskB增加合同的Number
属性
TaskA调用SaveChanges
,后者随后尝试提交事务。
因此,当您尝试提交事务1234
时,我们试图修改具有由事务Number
创建的锁的行中的5678
值,因此,SQL Server开始开始等待lock
释放。为了按照您的要求进行交易。
然后,TaskB
也调用SaveChanges
,就像TaskA
一样,它正在尝试增加合同Number
的123
。在这种情况下,它将在事务lock
从1234
创建的那一行上找到TaskA
。
现在,我们有来自1234
的事务TaskA
等待来自事务5678
的锁定被释放,而事务5678
正等待来自事务1234
的锁定被释放。这意味着我们处于僵局,因为这两个事务都将因为彼此阻塞而永远无法完成。
当SQL Server识别到它处于死锁状态时,它将选择一个事务作为victim
,将其杀死并允许其他事务继续进行。
回到隔离级别,如果您真的需要Serializable
,我没有足够的详细信息来尝试让我发表意见,但是很有可能您不需要它。 Serializable
是最安全和最严格的隔离级别,就像我们看到的,它通过牺牲并发性来实现。
如果您确实需要Serializable
保证,那么您实际上不应该尝试同时更新同一合同的Number
。Snapshot Isolation
替代
你说:
我已经阅读了一些有关此主题的文章,并尝试将隔离级别更改为快照。我不再遇到死锁,而是发现我收到隔离更新冲突错误。
如果您选择使用快照隔离,那正是您想要的行为。这是因为Snapshot
使用乐观并发模型。
这是在相同的MSDN文档上定义的方式(同样,重点是我的):
指定事务中任何语句读取的数据将是
交易中存在的数据的交易一致版本
交易开始。交易只能识别数据
在事务开始之前提交的修改。
开始交易后其他交易对数据的修改
当前事务对在
当前交易。效果就像是
事务获取已提交数据的快照,因为它们存在于
交易开始。
除非正在恢复数据库,否则SNAPSHOT事务在读取数据时不会请求锁定。 SNAPSHOT事务读取数据不会阻止其他事务写入数据。写入数据的事务不会阻止SNAPSHOT事务读取数据。
在数据库恢复的回滚阶段,
如果尝试对SNAPSHOT交易进行锁定,
读取由正在滚动的另一个事务锁定的数据
背部。 SNAPSHOT事务被阻止,直到该事务具有
被回滚。锁释放后立即释放
理所当然。
必须将ALLOW_SNAPSHOT_ISOLATION数据库选项设置为
在可以启动使用SNAPSHOT隔离的事务之前先打开
水平。如果使用SNAPSHOT隔离级别的事务访问
多个数据库中的数据,ALLOW_SNAPSHOT_ISOLATION必须设置为ON
在每个数据库中。
无法将事务设置为SNAPSHOT隔离
从另一个隔离级别开始的级别;这样做会导致
事务中止。如果在SNAPSHOT中开始事务
隔离级别,您可以将其更改为另一个隔离级别,然后
回到快照。事务在其首次访问时开始
数据。
在SNAPSHOT隔离级别下运行的事务可以查看
该交易所做的更改。例如,如果交易
对表执行UPDATE,然后发出SELECT语句
针对同一表格,修改后的数据将包含在
结果集。
让我们尝试描述在“快照隔离”下执行时代码的状态:
假设合同Number
的2
初始值为123
TaskA开始
TaskB启动
TaskA创建具有快照隔离级别的Transaction 1234
TaskB创建具有快照隔离级别的Transaction 5678
在两个快照中,Number = 2
代表合同123
。
TaskA生成一个SELECT * FROM ContractRanges WHERE ContractId = 123
。由于我们在Snapshot
隔离下运行,因此没有locks
。
TaskB做出相同的SELECT
语句,并且不放置任何locks
。
然后,TaskA将合同的Number
增加到3
TaskB将合同的Number
属性增加到3
TaskA调用SaveChanges
,这又使SQL Server将创建事务时创建的快照与数据库的当前状态以及在该事务下进行的未提交的更改进行比较。由于没有发现任何冲突,因此它将提交事务,现在Number
在数据库中的值为3
。
然后,TaskB
也调用SaveChanges
,并尝试提交其事务。当SQL Server将事务快照值与数据库中当前的快照值进行比较时,会发现存在冲突。在快照中,Number
的值为2
,现在它的值为3
。然后,它抛出Update Exception
。
再次,没有死锁,但是TaskB
这次失败了,因为TaskA
突变了也在TaskB
中使用的数据。
如何解决这个问题
既然我们已经介绍了在Serializable
和Snapshot
隔离级别下运行代码时发生的情况,那么您可以做什么fix
它。
好吧,您应该考虑的第一件事是,同时进行同一Contract
记录的突变是否真的有意义。这是我在您的代码中看到的第一个大气味,我将首先尝试理解。您可能需要与您的企业进行讨论,以了解他们是否真的需要合同上的并发性。
如我们所见,假设您确实确实需要同时进行此操作,那么您就不能真正使用Serializable
了,因为这会像您看到的那样在deadlocks
中产生。因此,我们只剩下Snapshot
隔离。
现在,当您捕获OptmisticConcurrencyException
时,实际上要取决于您和您的业务来决定。
例如,一种解决方法是简单地委派给用户,方法是向用户显示错误消息,告知他们他们要更改的数据已被修改,然后询问他们是否要刷新屏幕,从而决定要做什么。以获得最新版本的数据,并在需要时尝试再次执行相同的操作。
如果不是这种情况,则可以重试,另一种选择是让您的代码中包含重试逻辑,该逻辑将在抛出OptmitisticConcurrencyException
时重试执行操作。这是基于这样的假设,即第二次将不会发生并发事务使同一数据发生变异,并且操作现在将成功。