我想与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?
不幸的是,这段代码有一个严重的问题。在版本检查和事务提交之间,数据库可能会有一些变化。这是一个标准的time of check to time of use错误。我能看到解决它的唯一方法是将版本检查和事务提交作为单个命令执行。
是否可以使用TransactionScope
执行一些SQL代码以及事务提交?如果没有那么可以使用什么其他解决方案?
EDIT1: 版本需要按应用程序进行,而不是按行。
EDIT2: 我可以使用可序列化的隔离级别,但由于这会导致性能问题,因此它不是一个选项。
答案 0 :(得分:3)
不幸的是,这段代码有一个严重的问题。在版本检查和事务提交之间,数据库可能会有一些变化。
这根本不是真的。与您发布的代码一样,TransactionSope
的默认构造函数使用Serializable
隔离级别。虽然这可以说是problem,但确实有preventing any modification to any row you queried的副作用。这是悲观的并发控制。
但是你应该使用乐观并发控制。您需要使用a TransactionScope
constructor that accepts TransactionOptions
并传递选项以使用更合适的isolation level,例如。读承诺。至于行版本,请使用一个简单的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将找不到它,并且您的结果将是一个空集,因此您知道必须阅读,刷新并再次尝试(发生冲突)。这是a well known optimistic control scheme。
请注意,乐观并发控制在读取和写入跨越两个不同事务的位置更有意义(例如,在T1中读取,显示到用户表单,然后在T2中写入)。当读取和写入发生在相同的事务中时,您最好将其留给引擎。我只需使用snapshot isolation level即可解决问题。