3

在 C# 方法中,我执行以下返回多行的 SQL 查询:

SELECT [Data], [Version] 
FROM [dbo].[Table] 
WHERE [Id]=@uniqueId AND [ReferenceId] IS NULL 
ORDER BY [Version] Asc

然后我迭代结果并调用一个应该更新表的方法:

while (sqlDataReader.Read())
{
    SqlBytes data = sqlDataReader.GetSqlBytes(0);
    SqlInt64 version = sqlDataReader.GetSqlInt64(1);

    UpdateReference(data, version);
}


UpdateReference(data, version)
{
    // do database unrelated stuff with data

    UPDATE [dbo].[Table] 
    SET [dbo].[Table].[ReferenceId]=..., [dbo].[Table].[Data]=...
    WHERE [dbo].[Table].[Id]=@uniqueId AND [dbo].[Table].[Version]=@version
}

有一段时间这工作得很好,但突然(在SELECT ... INNER JOIN同一张表上执行一些查询之后)停止了。我在第一个 SELECT 上创建了一个事务范围(在调用的同一方法中UpdateReference()):

 using (TransactionScope scope = new TransactionScope())
    SELECT ...
    while (sqlDataReader.Read()) ... UpdateReference();

我得到以下异常:

事务已中止。

如果我删除事务范围,调用 UPDATE 时会在一段时间后发生超时异常:

超时已过。在操作完成之前超时时间已过或服务器没有响应。

但这似乎不是 SQL Server 问题。同样奇怪的是,对于某些记录,没有这样的问题——它们仅在某些表记录上使用第一个 SELECT 时才会发生。

这是我到目前为止发现的:

  • 如果我独立执行查询(从代码),一切正常;
  • 如果我在 SQL Management Studio 中独立执行这两个查询,它们将按预期工作

一个似乎可行的解决方案(现在?)是将第一个查询的结果存储到列表中,然后在SELECT完成后对列表元素调用更新:

List<long> versionList = new List<long>();     
List<byte[]> dataList = new List<byte[]>();   

using (TransactionScope scope = new TransactionScope())
{
    using (SqlConnection connection = new SqlConnection(connectionString))
    {
        connection.Open();                    

        // Execute SELECT ...
        using (SqlCommand sqlCommand = new SqlCommand(selectStatement, connection))
        {
            ...

            using (SqlDataReader sqlDataReader = sqlCommand.ExecuteReader())
            {                                       
                while (sqlDataReader.Read())
                {
                    SqlBytes data = sqlDataReader.GetSqlBytes(0);
                    SqlInt64 version = sqlDataReader.GetSqlInt64(1);

                    // Store result to lists
                    versionList.Add(version.Value);             
                    dataList.Add((byte[])data.ToSqlBinary(););
                }
            }
        }       
    }   

    // Everything works as expected if this loop is placed here; but if it is placed within the above SqlConnection using clause, an exception is thrown:
    // "Network access for Distributed Transaction Manager (MSDTC) has been disabled. Please enable DTC for network access in the security configurationfor MSDTC using the Component Services Administrative tool."
    for (int i = 0; i < versionList.Count; i++)
    {
       UpdateReference(dataList[i], versionList[i]);
    }

    scope.Complete();
}

我不确定这个解决方案是否有任何好处(除了使用比最佳内存更多的内存)或它可能导致的其他潜在问题。如果您能深入了解这里发生的事情以及如何最好地解决它,我将不胜感激。

更新 1

为了清楚起见,这就是我解决问题的方法:

  1. 在 TransactionScope 外执行 SELECT,将结果存储到列表中;

  2. 迭代这些列表并将其内容提供给包含在 TransactionScope 中的 UPDATE

随意批评/改进此解决方案:

Method1()
{
    List<long> versionList = new List<long>();     
    List<byte[]> dataList = new List<byte[]>();   

    using (SqlConnection connection = new SqlConnection(connectionString))
    {
        connection.Open();                    

        // Execute SELECT ...
        using (SqlCommand sqlCommand = new SqlCommand(selectStatement, connection))
        {
            ...

            using (SqlDataReader sqlDataReader = sqlCommand.ExecuteReader())
            {                                       
                while (sqlDataReader.Read())
                {
                    SqlBytes data = sqlDataReader.GetSqlBytes(0);
                    SqlInt64 version = sqlDataReader.GetSqlInt64(1);

                    // Store result to lists
                    versionList.Add(version.Value);             
                    data.Add((byte[])data.ToSqlBinary());
                }
            }
        }

        // Call update
        for (int i = 0; i < versionList.Count; i++)
        {
            UpdateReference(dataList[i], versionList[i]);       
        }   
    }   
}

UpdateReference(data, version)
{
    ...

    using (TransactionScope scope = new TransactionScope())
    {
        using (SqlConnection connection = new SqlConnection(this.ConnectionString))
        {
            connection.Open();

            UPDATE [dbo].[Table] 
            SET [dbo].[Table].[ReferenceId]=..., [dbo].[Table].[Data]=...
            WHERE [dbo].[Table].[Id]=... AND [dbo].[Table].[Version]=@version
        }

        scope.Complete();
    }
}
4

1 回答 1

5

是的,select通常需要锁;在查询本身期间,为了稳定性;但是如果有事务(取决于隔离级别),那些锁可以在查询整个事务后持续存在;特别是钥匙范围锁。当然,同一事务中的代码不会受到这些锁的不利影响。特别重要的是您的连接创建的确切位置以及您正在使用的连接数量:

  • 只有在环境事务中创建并打开连接时,该连接才会在该事务中自动登记;如果您打开连接然后创建环境事务,则连接不会自动登记
  • 如果您在事务范围内有单个连接,它通常会使用 LTM;如果您使用多个连接实例,它通常只会升级为 DTC;在网络上配置 DTC 有点繁琐(dtcping可以提供帮助)
  • 在您的情况下,您需要同时阅读和执行;我怀疑目前您正在使用多个连接来执行此操作;另一种选择是启用 MARS,这将允许您在单个连接上执行这两项操作

然而!就个人而言,我怀疑在您的情况下最简单的选择是首先在事务之外进行查询并进入列表(或类似列表) - 即不要懒惰地假脱机。然后做这项工作,并应用任何更新。如果可能的话,我会尽量避免跨越成百上千个单独命令的单个事务 - 如果你可以批处理该工作,那将是可取的。

于 2013-05-30T11:21:30.853 回答