TransactionScope and Optimistic concurrency



我想对TransactionScope使用乐观并发。这是我到目前为止提出的代码:

var options = new TransactionOptions {IsolationLevel = IsolationLevel.ReadCommitted};
using (var scope = new TransactionScope(TransactionScopeOption.Required, options))
{
using (var connection = new SqlConnection(_connectionString))
{
// ... execute some sql code here
// bump up version
var version = connection.ExecuteScalar<DateTime>(@"
DECLARE @version datetime2 = SYSUTCDATETIME();
UPDATE [Something].[Test]
SET [Version] = @version
WHERE Id = @Id
SELECT @version
", new {Id = id});
// ... execute more sql code here
// check if version has not changed since bump up
// NOTE: version is global for the whole application, not per row basis
var newVersion = connection.ExecuteScalar<DateTime>("SELECT MAX([Version]) FROM [Something].[Test]");
if (newVersion == version) scope.Complete(); // looks fine, mark as completed
}
} // what about changes between scope.Complete() and this line?

不幸的是,此代码有一个严重问题。在版本检查和事务提交之间,数据库可能会发生一些更改。这是对使用时间bug的标准检查时间。我能看到的解决问题的唯一方法是将版本检查和事务提交作为一个命令执行。

是否可以使用TransactionScope在事务提交的同时执行一些SQL代码?如果没有,那么可以使用其他什么解决方案?

第1版:版本需要按应用程序而不是按行。

第2版:我可以使用可序列化的隔离级别,但由于这会导致性能问题,所以这不是一个选项。

不幸的是,此代码存在一个严重问题。在版本检查和事务提交之间,数据库可能会发生一些更改。

这根本不是真的。在您发布的代码中,TransactionSope的默认构造函数使用Serializable隔离级别。虽然这可以说是一个问题,但它确实有阻止对您查询的任何行进行任何修改的副作用。它是悲观的并发控制。

不过,您应该使用乐观并发控制,这是正确的。您需要使用一个接受TransactionOptionsTransactionScope构造函数,并传入选项以使用更合适的隔离级别,例如read-committed。对于行版本,使用一个简单的int,在应用程序中每次写入时都会递增。

UPDATE [Something].[Test]
SET ..., [Version] = @new_version
OUTPUT Inserted.Id
WHERE Id = @Id AND [Version] = @old_version;

@old_version是您查询记录时在记录上找到的版本。@new_version@old_version+1。如果该行在您读取后被修改,则WHERE将找不到它,并且您的结果将是一个空集,因此您知道必须读取、刷新并重试(发生冲突)。这是一个众所周知的乐观控制方案。

不过,请注意,乐观并发控制在读和写跨越两个不同事务的情况下更有意义(例如,在T1中读取,显示给用户表单,然后在T2中写入)。当读取和写入发生在同一事务中时,最好将其留给引擎。我会简单地使用快照隔离级别,它可以开箱即用地解决问题。

相关内容

最新更新