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(今のところ?) うまくいくように見える 1 つの解決策は、最初のクエリの結果をリストに保存し、終了後にリスト要素の更新を呼び出すことです。

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. これらのリストを反復し、そのコンテンツを UPDATE にフィードします。これは TransactionScope に含まれています。

このソリューションを自由に批判/改善してください。

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役立つ場合があります)
  • あなたの場合、リーダーと実行を同時にしたい; 現時点では、複数の接続を使用してこれを行っていると思われます。もう 1 つのオプションは、単一の接続で両方の操作を実行できる MARS を有効にすることです。

でも!個人的には、あなたの場合の最も簡単なオプションは、最初にトランザクションのでリスト(または同様のもの)にクエリを実行することだと思います-つまり、遅延スプーリングではありません。次に、作業を行い、更新を適用します。可能であれば、数百/数千の個別のコマンドにまたがる単一のトランザクションを回避しようとします。その作業をバッチ処理できる場合は、それが望ましいでしょう。

于 2013-05-30T11:21:30.853 に答える