web-dev-qa-db-ja.com

データベースの同じ行を更新する2つのスレッドを適切に処理する方法

フラットファイルを読み込んで解析するためのT1というスレッドがあります。このファイルの一部を解析するためにT2という新しいスレッドを作成する必要があります。このT2スレッドは、元のエンティティのステータスを更新する必要があります。これも、元のエンティティによって解析および更新されます。スレッドT1。どうすればこの状況に対処できますか?

以下のサンプルレコードを含むフラットファイルを受け取ります。

AAAA
BBBB
AACC
BBCC
AADD
BBDD

最初に、このファイルはReceivedステータスでデータベースに保存されます。ここで、BBまたはAAで始まるすべてのレコードを別のスレッドで処理する必要があります。正常に解析されると、両方のスレッドがデータベース内のこのファイルオブジェクトのステータスをParsedに更新しようとします。場合によっては、staleObjectExceptionを受け取ります。 編集:例外が失われる前に任意のスレッドによって行われた作業。楽観的ロックを使用しています。この問題を回避する最良の方法は何ですか?

2つのスレッドが同じオブジェクトを更新すると、休止状態の例外が発生する可能性がありますか?

上記の投稿は、その一部を理解するのに役立ちますが、私の問題を解決するのには役立ちません。

18
Gaurava Agarwal

パート1-あなたの問題

この例外が発生する主な理由は、Hibernateを楽観的ロックで使用していることです。これは基本的に、スレッドT1またはスレッドT2のいずれかがすでに状態を[〜#〜] parsed [〜#〜]に更新し、もう一方のスレッドが古いバージョンのデータベースに保持されているバージョンよりも小さいバージョンの行で、状態を[〜#〜] parsed [〜#〜]にも更新しようとしています。

ここでの質問は、「2つのスレッドが同じデータを保存しようとしているのか?」です。答えが「はい」の場合、最後の更新が成功しても、最終的には行を同じ状態に更新しているため、問題はありません。その場合、データは常に同期されるため、楽観的ロックは必要ありません。

主な問題は、状態が[〜#〜] received [〜#〜]に設定された後、2つのスレッドT1とT2が次のステータス。その場合、T1が最初に実行された場合(またはその逆)、T2が更新された行のデータを更新し、T1によってすでにプッシュされた変更に基づいてその変更を再適用する必要があることを確認する必要があります。この場合の解決策は次のとおりです。 staleObjectExceptionが発生した場合は、基本的にデータベースからデータを更新して操作を再開する必要があります。

リンクのパート2分析が投稿されました2つのスレッドが同じオブジェクトを更新するときに休止状態の例外が発生する可能性がありますか?アプローチ1、これは多かれ少なかれWinsを更新する最後の状況です。それは多かれ少なかれ楽観的ロック(バージョンカウント)を回避します。ステータス[〜#〜] parsed [〜#〜]を設定するためにT1からT2またはその逆に依存関係がない場合。これは良いはずです。

Aproach 2楽観的ロックこれが今あるものです。解決策は、データを更新して操作を再開することです。

アプローチ3行レベルのDBロックここでの解決策は、アプローチ2の場合とほぼ同じですが、ペシミスティックロックが持続するという小さな修正があります。主な違いは、この場合はREADロックであり、PESSIMISTIC READの場合、データベースからデータを読み取って更新することができない場合があることです。

アプローチ4アプリケーションレベルの同期同期を行うには、さまざまな方法があります。 1つの例は、実際にすべての更新をBlockingQueueまたはJMSキューに配置し(永続化したい場合)、すべての更新を単一のスレッドからプッシュすることです。それを少し視覚化するために、T1とT2は要素をキューに配置し、単一のT3スレッドがオペレーションを読み取り、それらをデータベースサーバーにプッシュします。

アプリケーションレベルの同期を使用する場合、マルチサーバー配置ではすべての構造を配布できるわけではないことに注意してください。

さて、今のところ他に何も考えられません:)

13

質問を理解していることはわかりませんが、たとえば、AAで始まるレコード全体を「解析済み」としてマークするレコードだけを処理しているスレッドT1の論理エラーを構成しているようです。たとえば、T1の更新後、T2がまだBBレコードを処理しているときにアプリケーションがクラッシュした場合はどうなりますか?一部のBBレコードは失われる可能性がありますよね?

とにかく、問題の核心は、2つのスレッドが同じオブジェクトを更新するという競合状態にあることです。古いオブジェクト例外は、スレッドの1つが競合を失ったことを意味します。より良いソリューションは完全に競争を回避します。

(ここでは、個々のレコード処理がべき等であると想定しています。そうでない場合、いくつかの障害モードがレコードの再処理を引き起こすため、より大きな問題があると思います。レコード処理を1回だけ実行する必要がある場合は、メッセージキューがおそらくより良い解決策となる、より難しい問題があります。)

私はJava.util.concurrentの機能を利用してレコードをスレッド化されたワーカーにディスパッチし、すべてのレコードが処理されるまでスレッドが休止状態のブロックと相互作用するようにします。この時点で、そのスレッドはファイルを「解析済み」としてマークできます。

例えば、

// do something like this during initialization, or use a Guava LoadingCache...
Map<RecordType, Executor> executors = new HashMap<>();
// note I'm assuming RecordType looks like an enum
executors.put(RecordType.AA_RECORD, Executors.newSingleThreadExecutor());

次に、ファイルを処理するときに、各レコードを次のようにディスパッチして、キューに入れられたタスクのステータスに対応するフューチャーのリストを作成します。レコードを正常に処理すると、ブール値「true」が返されると仮定します。

List<Future<Boolean>> tasks = new ArrayList<>();
for (Record record: file.getRecords()) {
    Executor executorForRecord = executors.get(record.getRecordType());
    tasks.add(executor.submit(new RecordProcessor(record)));
}

すべてのタスクが正常に完了するのを待ちます。これを行うには、特にGuavaを使用するよりエレガントな方法があります。タスクが例外で失敗した場合、ExecutionExceptionもここで処理する必要があることに注意してください。ここではそれについて詳しく説明します。

boolean allSuccess = true;
for (Future<Boolean> task: tasks) {
    allSuccess = allSuccess && task.get();
    if (!allSuccess) break;
}

// if all your tasks completed successfully, update the file record
if (allSuccess) {
    file.setStatus("Parsed");
}
3
thewmo

各スレッドT1、T2がファイルの異なる部分を解析すると仮定すると、他のスレッド解析をオーバーライドする人がいないことを意味します。最善の方法は、DBコミットから解析プロセスを分離することです。

T1、T2は解析T3を実行するか、メインスレッドはT1、T2の両方が終了した後にコミットを実行します。このアプローチでは、両方のスレッドが終了したときにのみファイルのステータスをParsedに変更する方が正しいと思います。

t3は、T1、T2が終了するまで待機してからDBにコミットするCommitServiceクラスと考えることができます。

CountDownLatch は、これを行うのに役立つツールです。そしてここに

2
Elia Rohana