web-dev-qa-db-ja.com

SQL Serverデータベース呼び出しを使用したマルチスレッドC#アプリケーション

テーブルmainに500,000レコードのSQL Serverデータベースがあります。 child1child2、およびchild3と呼ばれる他の3つのテーブルもあります。 child1child2child3、およびmain間の多対多の関係は、main_child1_relationshipmain_child2_relationshipの3つの関係テーブルを介して実装されます。 、およびmain_child3_relationshipmainのレコードを読み取り、mainを更新し、関係テーブルに新しい行を挿入し、子テーブルに新しいレコードを挿入する必要があります。子テーブルのレコードには一意性制約があるため、実際の計算の擬似コード(CalculateDetails)は次のようになります。

for each record in main
{
   find its child1 like qualities
   for each one of its child1 qualities
   {
      find the record in child1 that matches that quality
      if found
      {
          add a record to main_child1_relationship to connect the two records
      }
      else
      {
          create a new record in child1 for the quality mentioned
          add a record to main_child1_relationship to connect the two records
      }
   }
   ...repeat the above for child2
   ...repeat the above for child3 
}

これは、シングルスレッドアプリとして正常に機能します。しかし、遅すぎます。 C#での処理は非常に重く、時間がかかりすぎます。これをマルチスレッドアプリに変えたいです。

これを行う最良の方法は何ですか? Linq to Sqlを使用しています。

これまでのところ、私のアプローチは、DataContextからのレコードのバッチごとに新しいmainオブジェクトを作成し、ThreadPool.QueueUserWorkItemを使用して処理することでした。ただし、1つのスレッドがレコードを追加してから、次のスレッドが同じレコードを追加しようとするため、これらのバッチはお互いの足指を踏んでいます。

コードは次のとおりです。

    int skip = 0;
    List<int> thisBatch;
    Queue<List<int>> allBatches = new Queue<List<int>>();
    do
    {
        thisBatch = allIds
                .Skip(skip)
                .Take(numberOfRecordsToPullFromDBAtATime).ToList();
        allBatches.Enqueue(thisBatch);
        skip += numberOfRecordsToPullFromDBAtATime;

    } while (thisBatch.Count() > 0);

    while (allBatches.Count() > 0)
    {
        RRDataContext rrdc = new RRDataContext();

        var currentBatch = allBatches.Dequeue();
        lock (locker)  
        {
            runningTasks++;
        }
        System.Threading.ThreadPool.QueueUserWorkItem(x =>
                    ProcessBatch(currentBatch, rrdc));

        lock (locker) 
        {
            while (runningTasks > MAX_NUMBER_OF_THREADS)
            {
                 Monitor.Wait(locker);
                 UpdateGUI();
            }
        }
    }

そして、ProcessBatchは次のとおりです。

    private static void ProcessBatch( 
        List<int> currentBatch, RRDataContext rrdc)
    {
        var topRecords = GetTopRecords(rrdc, currentBatch);
        CalculateDetails(rrdc, topRecords);
        rrdc.Dispose();

        lock (locker)
        {
            runningTasks--;
            Monitor.Pulse(locker);
        };
    }

そして

    private static List<Record> GetTopRecords(RecipeRelationshipsDataContext rrdc, 
                                              List<int> thisBatch)
    {
        List<Record> topRecords;

        topRecords = rrdc.Records
                    .Where(x => thisBatch.Contains(x.Id))
                    .OrderBy(x => x.OrderByMe).ToList();
        return topRecords;
    }

CalculateDetailsは、一番上の擬似コードで最もよく説明されています。

これを行うにはもっと良い方法が必要だと思います。助けてください。どうもありがとう!

23
Barka

問題に対する私の見解は次のとおりです。

  • 複数のスレッドを使用してSQL Serverまたは任意のデータベースにデータを挿入/更新/クエリする場合、デッドロックは現実です。それらが発生すると想定し、適切に処理する必要があります。

  • デッドロックの発生を制限しようとすべきではないということではありません。ただし、 デッドロック の基本的な原因を読み上げて、それらを防ぐための措置を講じることは簡単ですが、SQL Serverは常にあなたを驚かせます:-)

デッドロックの理由:

  • スレッドが多すぎる-スレッドの数を最小限に抑えようとしますが、もちろん最大のパフォーマンスを得るためにはもっと多くのスレッドが必要です。

  • 十分なインデックスがありません。選択と更新が十分に選択的でない場合、SQLは正常な範囲よりも大きな範囲のロックを取得します。適切なインデックスを指定してください。

  • インデックスが多すぎます。インデックスを更新するとデッドロックが発生するため、インデックスを必要最小限に減らすようにしてください。

  • トランザクションの分離レベルが高すぎます。 .NETを使用する場合のデフォルト 分離レベル は「シリアル化可能」ですが、SQL Serverを使用するデフォルトは「Read Committed」です。分離レベルを下げることは大いに役立ちます(もちろん適切な場合)。

これは私があなたの問題に取り組む方法です:

  • 独自のスレッドソリューションをロールバックするのではなく、TaskParallelライブラリを使用します。私のメインメソッドは次のようになります:

    using (var dc = new TestDataContext())
    {
        // Get all the ids of interest.
        // I assume you mark successfully updated rows in some way
        // in the update transaction.
        List<int> ids = dc.TestItems.Where(...).Select(item => item.Id).ToList();
    
        var problematicIds = new List<ErrorType>();
    
        // Either allow the TaskParallel library to select what it considers
        // as the optimum degree of parallelism by omitting the 
        // ParallelOptions parameter, or specify what you want.
        Parallel.ForEach(ids, new ParallelOptions {MaxDegreeOfParallelism = 8},
                            id => CalculateDetails(id, problematicIds));
    }
    
  • デッドロックエラーの再試行でCalculateDetailsメソッドを実行します

    private static void CalculateDetails(int id, List<ErrorType> problematicIds)
    {
        try
        {
            // Handle deadlocks
            DeadlockRetryHelper.Execute(() => CalculateDetails(id));
        }
        catch (Exception e)
        {
            // Too many deadlock retries (or other exception). 
            // Record so we can diagnose problem or retry later
            problematicIds.Add(new ErrorType(id, e));
        }
    }
    
  • 主要なCalculateDetailsメソッド

    private static void CalculateDetails(int id)
    {
        // Creating a new DeviceContext is not expensive.
        // No need to create outside of this method.
        using (var dc = new TestDataContext())
        {
            // TODO: adjust IsolationLevel to minimize deadlocks
            // If you don't need to change the isolation level 
            // then you can remove the TransactionScope altogether
            using (var scope = new TransactionScope(
                TransactionScopeOption.Required,
                new TransactionOptions {IsolationLevel = IsolationLevel.Serializable}))
            {
                TestItem item = dc.TestItems.Single(i => i.Id == id);
    
                // work done here
    
                dc.SubmitChanges();
                scope.Complete();
            }
        }
    }
    
  • そしてもちろん、デッドロック再試行ヘルパーの私の実装

    public static class DeadlockRetryHelper
    {
        private const int MaxRetries = 4;
        private const int SqlDeadlock = 1205;
    
        public static void Execute(Action action, int maxRetries = MaxRetries)
        {
            if (HasAmbientTransaction())
            {
                // Deadlock blows out containing transaction
                // so no point retrying if already in tx.
                action();
            }
    
            int retries = 0;
    
            while (retries < maxRetries)
            {
                try
                {
                    action();
                    return;
                }
                catch (Exception e)
                {
                    if (IsSqlDeadlock(e))
                    {
                        retries++;
                        // Delay subsequent retries - not sure if this helps or not
                        Thread.Sleep(100 * retries);
                    }
                    else
                    {
                        throw;
                    }
                }
            }
    
            action();
        }
    
        private static bool HasAmbientTransaction()
        {
            return Transaction.Current != null;
        }
    
        private static bool IsSqlDeadlock(Exception exception)
        {
            if (exception == null)
            {
                return false;
            }
    
            var sqlException = exception as SqlException;
    
            if (sqlException != null && sqlException.Number == SqlDeadlock)
            {
                return true;
            }
    
            if (exception.InnerException != null)
            {
                return IsSqlDeadlock(exception.InnerException);
            }
    
            return false;
        }
    }
    
  • もう1つの可能性は、パーティション戦略を使用することです

テーブルをいくつかの異なるデータセットに自然に分割できる場合は、 SQL Serverのパーティションテーブルとインデックス を使用するか、 手動で既存のテーブルをいくつかのテーブルのセットに分割します 。 2番目のオプションは面倒なので、SQL Serverのパーティション分割を使用することをお勧めします。また、組み込みのパーティション分割は、SQL Enterprise Editionでのみ使用できます。

パーティショニングが可能な場合は、たとえば8つの異なるセットでデータを壊したパーティションスキームを選択できます。これで、元のシングルスレッドコードを使用できますが、それぞれ8つのスレッドが個別のパーティションをターゲットにします。これで、デッドロックは発生しません(少なくとも最小数は発生しません)。

それが理にかなっていることを願っています。

48
Phil

概要

問題の根本は、Entity FrameworkのObjectContextのようなL2S DataContextはスレッドセーフではないということです。 このMSDNフォーラム交換 で説明されているように、.NET ORMソリューションでの非同期操作のサポートは、.NET 4.0の時点でまだ保留中です。独自のソリューションを展開する必要があります。これは、フレームワークがシングルスレッドであることを前提とする場合、必ずしも簡単に実行できるとは限らないということです。

L2SはADO.NETの上に構築されていることに注意してください。ADO.NETは非同期操作を完全にサポートしています。個人的には、その下位層を直接処理し、SQLを自分で記述して、ネットワーク上で何が起きているのかを完全に理解しました。

SQL Serverソリューション?

そうは言っても、私は尋ねる必要があります-これはC#のソリューションでなければなりませんか一連の挿入/更新ステートメントからソリューションを構成できる場合は、SQLを直接送信するだけで、スレッド化とパフォーマンスの問題はなくなります。*問題は、実際のデータ変換とは関係ないようです作成されましたが、.NETからのパフォーマンス向上を中心にしています。方程式から.NETを削除すると、タスクがより簡単になります。結局のところ、最良のソリューションは、多くの場合、最小限のコードを記述できるソリューションです。 ;)

更新/挿入ロジックを厳密に設定されたリレーショナルな方法で表現できない場合でも、SQL Serverには、レコードの繰り返しとロジックの実行のための組み込みメカニズムがあります。事実はあなたのタスクに適切であること。

これが繰り返し発生しなければならないタスクである場合、ストアドプロシージャとしてコーディングすることで大きなメリットが得られます。

*もちろん、長時間実行されるSQLには、ロックのエスカレーションやインデックスの使用など、対処しなければならない独自の問題があります。

C#ソリューション

もちろん、SQLでこれを行うのは問題ではないかもしれません。たとえば、コードの決定は、たとえば他の場所からのデータに依存しているかもしれません。あなたはいくつかの典型的なマルチスレッドのバグに言及しますが、あなたのコードを見ることなく、私はそれらに特に役立つことはできません。

C#からこれを行うことは明らかに実行可能ですが、発信するたびに一定量の遅延が存在するという事実に対処する必要があります。プールされた接続の使用、複数のアクティブな結果セットの有効化、クエリの実行に非同期のBegin/Endメソッドを使用することにより、ネットワーク遅延の影響を軽減できます。これらのすべてを使用しても、SQL Serverからアプリケーションにデータを送信するのにコストがかかることを受け入れる必要があります。

コードがそれ自体を完全にステップ実行しないようにする最良の方法の1つは、スレッド間で可変デー​​タをできるだけ共有しないようにすることです。これは、複数のスレッド間で同じDataContextを共有しないことを意味します。次に最適な方法は、共有データに触れるコードの重要なセクションをロックすることです。lockは、最初の読み取りから最後の書き込みまで、すべてのDataContextアクセスをブロックします。このアプローチでは、マルチスレッドの利点が完全になくなる可能性があります。ロックをよりきめ細かくすることができますが、これは苦痛の道であることに注意してください。

はるかに良いのは、オペレーションを完全に互いに分離することです。ロジックを「メイン」レコードに分割できる場合、それは理想的です。つまり、さまざまな子テーブル間に関係がなく、「メイン」の1つのレコードが次の意味を持たない限りです。別の方法として、次のように操作を複数のスレッドに分割できます。

private IList<int> GetMainIds()
{
    using (var context = new MyDataContext())
        return context.Main.Select(m => m.Id).ToList();
}

private void FixUpSingleRecord(int mainRecordId)
{
    using (var localContext = new MyDataContext())
    {
        var main = localContext.Main.FirstOrDefault(m => m.Id == mainRecordId);

        if (main == null)
            return;

        foreach (var childOneQuality in main.ChildOneQualities)
        {
            // If child one is not found, create it
            // Create the relationship if needed
        }

        // Repeat for ChildTwo and ChildThree

        localContext.SaveChanges();
    }
}

public void FixUpMain()
{
    var ids = GetMainIds();
    foreach (var id in ids)
    {
        var localId = id; // Avoid closing over an iteration member
        ThreadPool.QueueUserWorkItem(delegate { FixUpSingleRecord(id) });
    }
}

明らかにこれはあなたの質問の擬似コードと同じくらいおもちゃの例ですが、うまくいけば、それらの間にタスクをスコープする方法について考えてもらうことができます。それが、正しいC#ソリューションの鍵になると思います。

編集更新およびコメントへの対応

データの一貫性の問題が発生している場合は、トランザクションセマンティクスを適用することをお勧めします。これを行うには、System.Transactions.TransactionScopeを使用します(System.Transactionsへの参照を追加します)。または、内部接続にアクセスし、その上でBeginTransaction(またはDataConnectionメソッドが呼び出されたもの)を呼び出すことにより、ADO.NETレベルでこれを実行できる場合があります。

デッドロックについても言及します。 SQL Serverのデッドロックと戦っているということは、実際のSQLクエリがお互いの足指を踏んでいることを示しています。実際に何が送信されているのかがわからなければ、何が起こっているのか、どのように修正するのかを詳細に言うのは困難です。 SQLデッドロックはSQLクエリの結果であり、必ずしもC#スレッド構造からではないと言うだけで十分です。実際に何が起こっているのかを調べる必要があります。私の腸は、各「メイン」レコードが他のレコードから本当に独立している場合、行およびテーブルのロックは必要ないはずであり、Linq to SQLがおそらくここの犯人であることを教えてくれます。

DataContext.Logプロパティを例えばConsole.Out。私は個人的に使用したことはありませんが、LINQPadがL2S機能を提供していることを理解しています。

SQL Server Management Studioを使用すると、アクティビティモニターを使用して、ロックのエスカレーションをリアルタイムで監視できます。クエリアナライザを使用すると、SQL Serverがクエリを実行する方法を正確に把握できます。これらを使用すると、コードがサーバー側で実行していること、そしてそれを修正する方法を理解できるはずです。

6
Ben

すべてのXML処理もSQLサーバーに移行することをお勧めします。すべてのデッドロックが消えるだけでなく、パフォーマンスが大幅に向上し、決して戻りたくなくなるでしょう。

例で最もよく説明されます。この例では、XML BLOBが既にメインテーブルにあると想定しています(クローゼットと呼びます)。次のスキーマを想定します。

CREATE TABLE closet (id int PRIMARY KEY, xmldoc ntext) 
CREATE TABLE shoe(id int PRIMARY KEY IDENTITY, color nvarchar(20))
CREATE TABLE closet_shoe_relationship (
    closet_id int REFERENCES closet(id),
    shoe_id int REFERENCES shoe(id)
)

そして、私はあなたのデータ(メインテーブルのみ)が最初は次のように見えることを期待しています:

INSERT INTO closet(id, xmldoc) VALUES (1, '<ROOT><shoe><color>blue</color></shoe></ROOT>')
INSERT INTO closet(id, xmldoc) VALUES (2, '<ROOT><shoe><color>red</color></shoe></ROOT>')

次に、タスク全体が次のように簡単になります。

INSERT INTO shoe(color) SELECT DISTINCT CAST(CAST(xmldoc AS xml).query('//shoe/color/text()') AS nvarchar) AS color from closet
INSERT INTO closet_shoe_relationship(closet_id, shoe_id) SELECT closet.id, shoe.id FROM shoe JOIN closet ON CAST(CAST(closet.xmldoc AS xml).query('//shoe/color/text()') AS nvarchar) = shoe.color

ただし、同様の処理を多数行うことを考えると、メインBLOBをXML型として宣言し、これをさらに簡素化することで、作業を楽にすることができます。

INSERT INTO shoe(color)
    SELECT DISTINCT CAST(xmldoc.query('//shoe/color/text()') AS nvarchar)
    FROM closet
INSERT INTO closet_shoe_relationship(closet_id, shoe_id)
    SELECT closet.id, shoe.id
    FROM shoe JOIN closet
        ON CAST(xmldoc.query('//shoe/color/text()') AS nvarchar) = shoe.color

繰り返し呼び出されるXpathの結果を一時的にまたは永続的に事前計算したり、メインテーブルの初期設定をBULK INSERTに変換したりするなど、追加のパフォーマンス最適化が可能ですが、これらを成功させる必要はないと思います。

2
Jirka Hanika

sqlサーバーのデッドロックは正常であり、このタイプのシナリオでは予想されます-MSの推奨事項は、db側ではなく これらはアプリケーション側で処理する必要があります です。

ただし、ストアドプロシージャが1回だけ呼び出されるようにする必要がある場合は、sp_getapplockを使用してsql mutexロックを使用できます。これを実装する方法の例を次に示します

BEGIN TRAN
DECLARE @mutex_result int;
EXEC @mutex_result = sp_getapplock @Resource = 'CheckSetFileTransferLock',
 @LockMode = 'Exclusive';

IF ( @mutex_result < 0)
BEGIN
    ROLLBACK TRAN

END

-- do some stuff

EXEC @mutex_result = sp_releaseapplock @Resource = 'CheckSetFileTransferLock'
COMMIT TRAN  
1
Johnv2020

この問題は LimitedConcurrencyLevelTask​​Scheduler の助けを借りて解決できます

public class InOutMessagesController
{
    private static LimitedConcurrencyLevelTaskScheduler scheduler = new LimitedConcurrencyLevelTaskScheduler(1);
    private TaskFactory taskFactory = new TaskFactory(scheduler);
    private TaskFactory<MyTask<Object[]>> taskFactoryWithResult = new TaskFactory<MyTask<Object[]>>(scheduler);
    private ConcurrentBag<Task> tasks = new ConcurrentBag<Task>();
    private ConcurrentBag<MyTask<Object[]>> tasksWithResult = new ConcurrentBag<MyTask<Object[]>>();
    private ConcurrentBag<int> endedTaskIds = new ConcurrentBag<int>();
    private ConcurrentBag<int> endedTaskWithResultIds = new ConcurrentBag<int>();
    private Task TaskForgetEndedTasks;
    private static object taskForgetLocker = new object();


    #region Conveyor
    private async void AddTaskVoidToQueue(Task task)
    {
        try
        {
            tasks.Add(task);

            await taskFactory.StartNew(() => task.Start());

            if (TaskForgetEndedTasks == null)
            {
                ForgetTasks();
            }
        }
        catch (Exception ex)
        {
            NLogger.Error(ex);
        }
    }

    private async Task<Object[]> AddTaskWithResultToQueue(MyTask<Object[]> task)
    {
        ForgetTasks();

        tasksWithResult.Add(task);

        return await taskFactoryWithResult.StartNew(() => { task.Start(); return task; }).Result;
    }

    private Object[] GetEqualTaskWithResult(string methodName)
    {
        var equalTask = tasksWithResult.FirstOrDefault(x => x.MethodName == methodName);

        if (equalTask == null)
        {
            return null;
        }
        else
        {
            return equalTask.Result;
        }
    }

    private void ForgetTasks()
    {
        Task.WaitAll(tasks.Where(x => x.Status == TaskStatus.Running || x.Status == TaskStatus.Created || x.Status == TaskStatus.WaitingToRun).ToArray());

        lock (taskForgetLocker)
        {
            if (TaskForgetEndedTasks == null)
            {
                TaskForgetEndedTasks = new Task(ForgetEndedTasks);

                TaskForgetEndedTasks.Start();
            }

            TaskForgetEndedTasks.Wait();

            TaskForgetEndedTasks = null;
        }
    }

    private void ForgetEndedTasks()
    {
        try
        {
            var completedTasks = tasks.Where(x => x.IsCompleted || x.IsFaulted || x.IsCanceled);
            var completedTasksWithResult = tasksWithResult.Where(x => x.IsCompleted || x.IsFaulted || x.IsCanceled);

            if (completedTasks.Count() > 0)
            {
                foreach (var ts in completedTasks)
                {
                    if (ts.Exception != null)
                    {
                        NLogger.Error(ts.Exception);

                        if (ts.Exception.InnerException != null)
                        {
                            NLogger.Error(ts.Exception.InnerException);
                        }
                    }

                    endedTaskIds.Add(ts.Id);
                }

                if (endedTaskIds.Count != 0)
                {
                    foreach (var t in endedTaskIds)
                    {
                        Task ct = completedTasks.FirstOrDefault(x => x.Id == t);

                        tasks.TryTake(out ct);
                    }
                }

                endedTaskIds = new ConcurrentBag<int>();
            }

            if (completedTasksWithResult.Count() > 0)
            {
                foreach (var ts in completedTasksWithResult)
                {
                    if (ts.Exception != null)
                    {
                        NLogger.Error(ts.Exception);

                        if (ts.Exception.InnerException != null)
                        {
                            NLogger.Error(ts.Exception.InnerException);
                        }
                    }

                    endedTaskWithResultIds.Add(ts.Id);
                }

                foreach (var t in endedTaskWithResultIds)
                {
                    var ct = tasksWithResult.FirstOrDefault(x => x.Id == t);

                    tasksWithResult.TryTake(out ct);
                }

                endedTaskWithResultIds = new ConcurrentBag<int>();
            }
        }
        catch(Exception ex)
        {
            NLogger.Error(ex);
        }
    }
    #endregion Conveyor

    internal void UpdateProduct(List<ProductData> products)
    {
            var updateProductDataTask = new Task(() => ADOWorker.UpdateProductData(products));

            AddTaskVoidToQueue(updateProductDataTask);
    }

    internal async Task<IEnumerable<ProductData>> GetProduct()
    {
        string methodName = "GetProductData";

        Product_Data[] result = GetEqualTaskWithResult(methodName) as Product_Data[];

        if (result == null)
        {
            var task = new MyTask<Object[]>(ADOWorker.GetProductData, methodName);

            result = await AddTaskWithResultToQueue(task) as Product_Data[];
        }

        return result;
    }
}

public class ADOWorker
{
    public Object[] GetProductData()
    {
        entities = new DataContext();

        return entities.Product_Data.ToArray();
    }

    public void UpdateProductData(List<Product_Data> products)
    {
            entities = new DataContext();

            foreach (Product_Data pr_data in products)
            {
                entities.sp_Product_Data_Upd(pr_data);
            }            
    }
}
1
Maxim

これは明らかかもしれませんが、各タプルをループしてサーブレットコンテナで作業を行うには、レコードごとのオーバーヘッドが多くかかります。

可能であれば、ロジックを1つ以上のストアドプロシージャとして書き換えて、その処理の一部またはすべてをSQLサーバーに移動します。

0
Scott Smith