web-dev-qa-db-ja.com

10 000の100行ごとにflush()メソッドを使用するとトランザクションが遅くなる

_spring-boot_と_spring-data-jpa_および_postgres db_を1つのテーブルで使用するサンプルプロジェクトがあります。

ループ内のINSERT 10 000レコードをテーブルに入れて実行時間を測定しようとしています-100レコードごとにEntityManagerクラスのflush()メソッドを有効または無効にします。

期待される結果は、flush()メソッドを有効にした場合の実行時間は、無効にした場合よりもはるかに短いことですが、実際には反対の結果になります。

serService.Java

_package sample.data;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

@Service
public class UserService {
    @Autowired
    UserRepository userRepository;

    public User save(User user) {
        return userRepository.save(user);
    }
}
_

serRepository.Java

_package sample.data;

import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;

@Repository
public interface UserRepository extends JpaRepository<User, Long> { }
_

Application.Java

_package sample;

import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
import org.springframework.transaction.annotation.Transactional;

import sample.data.User;
import sample.data.UserService;

import javax.persistence.EntityManager;
import javax.persistence.PersistenceContext;

@SpringBootApplication
@EnableJpaRepositories(considerNestedRepositories = true)
public class Application {
    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }

    @Autowired
    private UserService userService;

    @PersistenceContext
    EntityManager entityManager;

    @Bean
    public CommandLineRunner addUsers() {
        return new CommandLineRunner() {
            @Transactional
            public void run(String... args) throws Exception {
                long incoming = System.currentTimeMillis();
                for (int i = 1; i <= 10000; i++) {
                    userService.save(new User("name_" + i));

                    if (i % 100 == 0) {
                        entityManager.flush();
                        entityManager.clear();
                    }
                }
                entityManager.close();
                System.out.println("Time: " + (System.currentTimeMillis() - incoming));
            }
        };
    }
}
_
13
Drakonoved

永続化プロバイダー構成でJDBCバッチ処理を必ず有効にしてください。 Hibernateを使用している場合は、これをSpringプロパティに追加します。

_spring.jpa.properties.hibernate.jdbc.batch_size=20   // or some other reasonable value
_

バッチ処理を有効にしないと、パフォーマンスの低下は100エンティティごとに永続化コンテキストをクリアするオーバーヘッドが原因だと思いますが、それについてはわかりません(測定する必要があります)。

更新:

実際、JDBCバッチ処理を有効または無効にしても、flush()をときどき実行すると、それがない場合よりも速くならないという事実には影響しません。手動で制御しているものflush()howではありませんステートメントまたは単一挿入)、ただし、代わりにwhenを制御しています。データベースへのフラッシュが実行されます。

したがって、比較しているのは次のとおりです。

  1. flush()を使用して100オブジェクトごと:フラッシュ時に100個のインスタンスをデータベースに挿入し、これを10000/100 = 100回にします。
  2. flush()がない場合:コンテキスト内のすべての10000オブジェクトをメモリに収集し、トランザクションのコミット時に10000挿入を実行します。

もう一方のJDBCバッチ処理はフラッシュの発生方法に影響しますが、flush()を使用して発行されたステートメントとflush()を使用せずに発行されたステートメントの数は同じです。

ループの中でたまにフラッシュおよびクリアすることの利点は、キャッシュが保持するオブジェクトが多すぎるために起こりうるOutOfMemoryErrorを回避することです。

7
manouti

マイクロベンチマークを書くのは難しい Aleksey Shipilevが彼の「JMH vs Caliper:reference thread」の投稿で大きく説明しています 。あなたのケースは厳密にはマイクロベンチマークではありませんが:

  1. 10,000回未満の繰り返しでは、JVMはデフォルト設定でコードをウォームアップしてJITできません。コードのパフォーマンスを測定する前に、JVMをウォームアップします。

  2. System.nanoTime()ではなく、経過時間を測定するためのSystem.currentTimeMillis()msで測定している場合、System.currentTimeMillis()のクロックドリフトによって結果が歪んでしまいます。

  3. ボトルネックを特定するために、データベース側でこれを測定する可能性があります。ボトルネックがなければ、根本的な原因が何であるかを理解することは困難です。データベースが大西洋の向こう側にある可能性があり、ネットワーク接続コストがINSERTステートメントのコストに影を落とします。

  4. ベンチマークは十分に分離されていますか?データベースが複数のユーザーと接続で共有されている場合、ベンチマーク以外のパフォーマンスは異なります。

現在のセットアップのボトルネックを見つけ、それを検証する方法について仮定を立て、その仮定に一致するようにベンチマークを変更してから、再度測定して確認します。それがそれを理解する唯一の方法です。

3
Karol Dowbecki

説明していただけますか理由信じます:

期待される結果は、flush()メソッドを有効にした場合の実行時間は、無効にした場合よりもはるかに短いことです。

これは根本的に間違った仮定であるように私には思えます。この簡単な操作を1万回実行すると、フラッシュを使用する場合と使用しない場合よりも高速になると信じる強い理由はありません。

すべてのレコードがメモリに収まる限り、私はexpect非中間フラッシュバージョンの方が高速であることを期待します。ネットワークにアクセスするIOデータベースに100回アクセスする方が、最後に1回実行するよりも高速である必要があります。

1
Ben M

エンティティの永続化で最もコストがかかるのは、データベースへの書き込みです。 JPAでのエンティティの永続化に費やされる時間は、純粋なメモリ内操作であるため、比較すると取るに足らないものです。 IOメモリと比較して)です。

データベースへの書き込みには、かなりの静的オーバーヘッドも含まれる可能性があります。つまり、データベースへの書き込み回数が実行時間に影響を与える可能性があります。 EntityManager#flushを呼び出すときは、保留中のすべての変更をデータベースに書き込むようにHibernateに指示します。

つまり、実行しているのは、データベースへの書き込みが100回の実行と、データベースへの書き込みが1回の実行を比較することです。 IOのオーバーヘッドのため、前者は著しく遅くなります。

1
Tobb

他の回答では言及されていない2つの側面。フラッシュのほかに、Hibernateセッションをクリアする必要があります。これをクリアしないと、サイズが大きくなり、メモリ消費に影響を及ぼし、パフォーマンスが低下する可能性があります。

エンティティを永続化するときのもう1つのことは、IDジェネレーターがhilosequenceを使用することを確認することです。 IDが1、2、3、4、5 .....の場合、各挿入は、IDをインクリメントするために追加の往復があります。

1