web-dev-qa-db-ja.com

Spring JPA:saveandflushとsaveのコストはいくらですか?

マイクロサービスのセットから構築されたアプリケーションがあります。 1つのサービスはデータを受信し、Spring JPAおよびEclipseリンクを介してデータを永続化してから、2番目のサービスにアラート(AMQP)を送信します。

次に、特定の条件に基づいて、2番目のサービスが保存されたデータに対してRESTfull Webサービスを呼び出して、保存された情報を取得します。

データが以前に保存されていても、RESTfullサービスがnullデータセットを返すことがあることに気付きました。永続化サービスのコードを見ると、saveandflushの代わりにsaveが使用されているため、ダウンストリームサービスがクエリを実行するのに十分な速度でデータがフラッシュされていないと想定します。

  • Saveandflushには疲れる費用がありますか、それともデフォルトで使用するのが合理的ですか?
  • ダウンストリームアプリケーションへのデータ可用性の即時性を保証しますか?

元の永続化関数は@Transactional

13
skyman

問題の予後

ここでの問題は、save vs saveAndFlushとは関係ないと思います。この問題は、Spring _@Transactional_メソッドの性質と、データベースとAMQPブローカーの両方を含む分散環境内でのこれらのトランザクションの不正使用に関連しているようです。そしておそらく、その有毒な混合物に、JPAコンテキストがどのように機能するかについてのいくつかの基本的な誤解を加えてください。

説明では、_@Transactional_メソッド内でJPAトランザクションを開始し、トランザクション中(ただしコミットする前)にAMQPブローカーにメッセージを送信することを暗示しているようです。その後、キューの反対側で、コンシューマアプリケーションがメッセージを取得し、RESTサービス呼び出しを行います。この時点で、パブリッシャー側からのトランザクションの変更はまだ行われていないことに気付きます。データベースにコミットされているため、コンシューマ側には表示されません。

問題は、JPAトランザクションがディスクにコミットされる前に、それらのAMQPメッセージをJPAトランザクション内で伝播することです。コンシューマーがメッセージを読み取って処理するまでに、発行側からのトランザクションがまだ完了していない可能性があります。そのため、これらの変更はコンシューマアプリケーションには表示されません。

AMPQ実装がRabbitの場合、この問題は以前に見たことがあります。データベーストランザクションマネージャーを使用する_@Transactional_メソッドを開始し、そのメソッド内でRabbitTemplateを使用して対応するメッセージを送信する場合。

RabbitTemplateがトランザクションチャネル(つまり_channelTransacted=true_)を使用していない場合、データベーストランザクションがコミットされる前にメッセージが配信されます。 RabbitTemplateでトランザクションチャネルを有効にすると(デフォルトでは無効になっています)、問題の一部を解決できると思います。

_<rabbit:template id="rabbitTemplate" 
                 connection-factory="connectionFactory" 
                 channel-transacted="true"/>
_

チャネルが処理されると、RabbitTemplateは現在のデータベーストランザクション(明らかにJPAトランザクション)に「参加」します。 JPAトランザクションがコミットされると、Rabbitチャネルの変更もコミットするエピローグコードが実行され、メッセージの実際の「送信」が強制されます。

save vs saveAndFlushについて

JPAコンテキストの変更をフラッシュすることで問題は解決したと思われるかもしれませんが、間違いです。 JPAコンテキストをフラッシュすると、エンティティの変更(メモリ内のその時点で)が強制的にディスクに書き込まれますが、対応するデータベーストランザクション内でディスクに書き込まれ、JPAトランザクションがコミットされるまでコミットされません。これは_@Transactional_メソッドの最後に発生します(残念ながらAMQPメッセージを送信してからしばらく経った後、上記で説明したようにトランザクションチャネルを使用しない場合)。

したがって、JPAコンテキストをフラッシュしても、パブリッシャアプリケーションで_@Transactional_メソッドが終了するまで、コンシューマアプリケーションはそれらの変更を(従来のデータベース分離レベルのルールに従って)表示しません。

save(entity)を呼び出すとき、EntityManagerは変更をすぐに同期する必要はありません。ほとんどのJPA実装は、エンティティをメモリ内でダーティとしてマークし、すべての変更をデータベースと同期し、データベースレベルでそれらの変更をコミットする最後の瞬間まで待機します。

注:気まぐれなEntityManagerがそうすることを決定するまでではなく、それらの変更の一部をすぐにディスクに移行したい場合があります。これの典型的な例は、トランザクション中に後で必要になる追加レコードを生成するために実行する必要があるデータベーステーブルにトリガーがある場合に発生します。そのため、トリガーが強制的に実行されるように、変更をディスクに強制的にフラッシュします。

コンテキストをフラッシュすることにより、メモリ内の変更をディスクに強制的に同期させるだけですが、これはそれらの変更の即時データベースコミットを意味するものではありません。したがって、フラッシュしたこれらの変更は、必ずしも他のトランザクションからは見えません。ほとんどの場合、従来のデータベース分離レベルに基づいてそうしません。

2PCの問題

ここでのもう1つの古典的な問題は、データベースとAMQPブローカーが2つの独立したシステムであることです。これがRabbitについての場合、2PC(2フェーズコミット)はありません。

そのため、興味深いシナリオ、たとえばデータベーストランザクションは正常にコミットされますが、Rabbitはメッセージのコミットに失敗します。その場合、トランザクション全体を繰り返す必要があり、データベースの副作用をスキップして、Rabbitにメッセージを送信しようとするだけです。

おそらく Springでの分散トランザクション、XAの有無にかかわらず に関するこの記事を読む必要があります。特にチェーントランザクションに関するセクションは、この問題に対処するのに役立ちます。

彼らは、より複雑なトランザクションマネージャの定義を提案しています。例えば:

_<bean id="jdbcTransactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
    <property name="dataSource" ref="dataSource"/>
</bean>

<bean id="rabbitTransactionManager" class="org.springframework.amqp.rabbit.transaction.RabbitTransactionManager">
    <property name="connectionFactory" ref="connectionFactory"/>
</bean>

<bean id="chainedTransactionManager" class="org.springframework.data.transaction.ChainedTransactionManager">
    <constructor-arg name="transactionManagers">
        <array>
            <ref bean="rabbitTransactionManager"/>
            <ref bean="jdbcTransactionManager"/>
        </array>
    </constructor-arg>
</bean>
_

そして、コードでは、そのチェーントランザクションマネージャーを使用して、データベーストランザクション部分とRabbitトランザクション部分の両方を調整します。

現在、データベース部分をコミットする可能性はありますが、Rabbitトランザクション部分は失敗する可能性があります。

だから、このようなものを想像してください:

_@Retry
@Transactional("chainedTransactionManager")
public void myServiceOperation() {
    if(workNotDone()) {
        doDatabaseTransactionWork();
    }
    sendMessagesToRabbit();
}
_

このように、何らかの理由でRabbitトランザクション部分が失敗し、チェーントランザクション全体を再試行するように強制された場合、データベースの副作用を繰り返さず、失敗したメッセージをRabbitに送信するだけです。

同時に、データベース部分に障害が発生した場合、Rabbitにメッセージを送信したことがないため、問題はありません。

あるいは、データベースの副作用がべき等である場合は、チェックをスキップして、データベースの変更を再適用し、ウサギにメッセージを送信するだけで済みます。

真実は、最初はあなたがやろうとしていることは非常に簡単に思えますが、さまざまな問題を掘り下げて理解すると、これを正しく行うのは難しいビジネスであることがわかります。

36
Edwin Dalorzo