【问题标题】:TransactionScope and Optimistic concurrencyTransactionScope 和乐观并发
【发布时间】:2017-02-13 15:11:54
【问题描述】:

我想对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 代码以及事务提交?如果没有,那还有什么其他的解决方案可以使用?

编辑1: 版本需要针对每个应用程序,而不是每行。

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

【问题讨论】:

  • 在我看来,最简单的解决方案是将所有这些 sql 代码移动到存储过程中并将其放入事务中。老实说,将 sql 从您的应用程序中移出无论如何都是一个好主意,以创建分层架构并将代码与数据实现分离。
  • @SeanLange 需要在现有系统上进行此更改,并且不能将整个逻辑移至存储过程。

标签: c# sql-server transactions optimistic-concurrency


【解决方案1】:

不幸的是,这段代码有一个严重的问题。在版本检查和事务提交之间,数据库可能会发生一些变化。

这根本不是真的。 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 中写入)。当读取和写入发生在 same 事务中时,最好将其留给引擎。我会简单地使用 snapshot isolation level 来解决问题。

【讨论】:

  • 老实说,在实际代码中,我已将隔离级别更改为 ReadCommitted。虽然没有将其包含在此示例中。将立即编辑。
  • 关于您的解决方案。看来我没有很好地描述我的问题。我需要有一个全局计数器,而不是每行。我不能使用可序列化,因为会有很多读取,并且在每次写入时阻止几个表不是一种选择。稍后将更改问题描述。
  • I need to have a global counter, not per row basis:在并发下维护这样一个计数器基本上是不可能。每行计数器有什么问题,为什么不能用?
  • 基本上其他系统希望能够从我们的数据库中缓存大量数据,并且他们只想获得更改。这个想法是有一个全局计数器(datetime2 而不是任何其他计数器,不会在任何地方保留当前计数器值)。每行计数器不是一个选项。
  • Change Tracking, Change Data Capture。使用全局水印进行更改存在根本缺陷:它无法检测到已删除的行。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2015-09-28
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2011-11-28
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多