1. 概要

この記事では、読み取り専用トランザクションについて説明します。 それらの目的と使用方法について説明し、パフォーマンスと最適化に関連するいくつかのニュアンスを確認します。 簡単にするために、MySQLのInnoDBエンジンに焦点を当てます。 ただし、説明されている情報の一部は、データベース/ストレージエンジンによって変わる可能性があることに注意してください。

2. トランザクションとは何ですか?

トランザクションは、1つ以上のステートメントで構成される不可分操作です。 この操作内のすべてのステートメントが成功(コミット)または失敗(ロールバック)されるため、これはアトミックです。つまり、すべてまたはまったく意味がありません。 ACIDプロパティの文字「A」はトランザクションのアトミック性を表します。

理解すべきもう1つの重要なことは、InnoDBエンジンのすべてのステートメントが、明示的でない場合は暗黙的にトランザクションになるということです。 方程式に並行性を追加すると、このような概念を理解するのが非常に難しくなります。 次に、別のACIDプロパティであるIsolationの「I」を明確にする必要があります。

分離レベルのプロパティを理解することは、パフォーマンスとパフォーマンスのトレードオフについて推論できるようにするために不可欠です。 一貫性の保証 。 ただし、分離レベルの詳細に入る前に、InnoDBのすべてのステートメントはトランザクションであるため、コミットまたはロールバックできることを覚えておいてください。 トランザクションが指定されていない場合、データベースはトランザクションを作成し、 autocommit プロパティに基づいて、コミットされるかどうかを決定します。

2.1. 分離レベル

この記事では、MySQLのデフォルトの repeatablereadを想定しています。 これは、同じトランザクション内で一貫性のある読み取りを提供します。つまり、最初の読み取りはスナップショット(特定の時点)を確立し、後続のすべての読み取りは相互に一貫性があります。 詳細については、MySQLの公式ドキュメントを参照してください。 もちろん、そのようなスナップショットを保持することには結果がありますが、良好な整合性レベルが保証されます。

異なるデータベースには他の名前または分離レベルオプションがある場合がありますが、ほとんどの場合、それらは類似しています。

3. トランザクションを使用する理由と場所

トランザクションとは何か、およびそのさまざまなプロパティについて理解が深まったところで、読み取り専用トランザクションについて説明しましょう。 前に説明したように、InnoDBエンジンでは、すべてのステートメントがトランザクションであるため、ロックやスナップショットなどが含まれる場合があります。 ただし、トランザクションIDやその他の内部構造で行をマークするなど、トランザクション調整に関連するオーバーヘッドの一部は、プレーンクエリでは必要ない場合があることがわかります。 そこで、読み取り専用トランザクションが機能します。

構文STARTTRANSACTION READ ONLY を使用して、読み取り専用トランザクションを明示的に定義できます。 MySQLは、読み取り専用の遷移を自動的に検出しようとします。 ただし、明示的に宣言する場合は、さらに最適化を適用できます。 読み取り集中型アプリケーションは、これらの最適化を活用して、データベースクラスターのリソース使用率を節約できます

3.1. アプリケーションと データベース

アプリケーションで永続化レイヤーを処理するには、多くの抽象化レイヤーが必要になる可能性があることを知っておく必要があります。 これらの各レイヤーには、異なる責任があります。 ただし、単純化するために、最終的に、これらのレイヤーは、アプリケーションがデータベースを処理する方法、またはデータベースがデータ操作を処理する方法のいずれかに影響を与えるとしましょう。

もちろん、すべてのアプリケーションにこれらすべてのレイヤーがあるわけではありませんが、それは優れた一般化を表しています。 Springアプリケーションがあるとすると、要するに、これらのレイヤーは次の目的を果たします。

  • DAO ビジネスロジックと永続性のニュアンスの間の架け橋として機能します
  • トランザクションの抽象化:トランザクションのアプリケーションレベルの複雑さを処理します(開始、コミット、ロールバック)
  • JPA抽象化:ベンダー間で標準APIを提供するJava仕様
  • ORMフレームワーク:JPAの背後にある実際の実装(Hibernateなど)
  • JDBC :データベースと実際に通信する責任があります

主なポイントは、これらの要因の多くがトランザクションの動作に影響を与える可能性があることです。 それでも、この動作に直接影響を与える特定のプロパティグループに焦点を当てましょう。 通常、クライアントはこれらのプロパティをグローバルレベルまたはセッションレベルで定義できます。 すべてのプロパティのリストは広範囲にわたるため、重要なプロパティのうち2つについてのみ説明します。 ただし、すでにそれらに精通している必要があります。

3.2. トランザクション管理

JDBCドライバーがアプリケーション側からトランザクションを開始する方法は、autocommitプロパティをオフにすることです。 これはBEGINTRANSACTION ステートメントと同等であり、その瞬間から、トランザクションを完了するには、以下のすべてのステートメントをコミットまたはロールバックする必要があります。

グローバルレベルで定義されたこのプロパティは、すべての着信要求を手動トランザクションとして処理するようにデータベースに指示し、ユーザーがコミットまたはロールバックする必要があります。 ただし、ユーザーがセッションレベルでこの定義を上書きした場合、これは無効になります。 その結果、多くのドライバーは、一貫した動作を保証し、アプリケーションがそれを制御できるようにするために、デフォルトでこのプロパティをオフにします。

次に、トランザクションプロパティを使用して、書き込み操作を許可するかどうかを定義できます。 ただし、注意点があります。読み取り専用トランザクションでも、TEMPORARYキーワードを使用して作成されたテーブルを操作することは可能です。 このプロパティにはグローバルスコープとセッションスコープもありますが、通常、アプリケーションではこのプロパティと他のプロパティをセッションレベルで処理します。

注意点は、接続プールを使用する場合、接続を開いて再利用する性質があるためです。 トランザクションと接続を処理するフレームワークまたはライブラリは、新しいトランザクションを開始する前に、セッションがクリーンな状態にあることを確認する必要があります。

このため、いくつかのステートメントを実行して、残りの保留中の変更を破棄し、セッションを適切にセットアップすることができます。

読み取りが多いアプリケーションが読み取り専用トランザクションを活用して、データベースクラスターのリソースを最適化および節約できることはすでに見てきました。 ただし、多くの開発者は、セットアップを切り替えるとデータベースへのラウンドトリップが発生し、接続のスループットに影響を与えることも忘れています。

MySQLでは、これらのプロパティをグローバルレベルで次のように定義できます。

SET GLOBAL TRANSACTION READ WRITE;
SET autocommit = 0;
/* transaction */
commit;

または、セッションレベルでプロパティを設定できます。

SET SESSION TRANSACTION READ ONLY;
SET autocommit = 1;
/* transaction */

3.3. ヒント

1つのクエリのみを実行するトランザクションの場合、autocommitプロパティを有効にすると、ラウンドトリップを節約できる場合があります。 これがアプリケーションで最も一般的な原因である場合は、読み取り専用として設定された別のデータソースを使用し、デフォルトでautocommitを有効にするとさらに効果的です。

ここで、トランザクションにさらにクエリがある場合は、明示的な読み取り専用トランザクションを使用する必要があります。 読み取り専用データソースを作成すると、書き込みトランザクションと読み取り専用トランザクションの切り替えを回避できるため、ラウンドトリップを節約することもできます。 ただし、ワークロードが混在している場合、新しいデータソースの管理の複雑さはそれ自体を正当化しない可能性があります

複数のステートメントを含むトランザクションを処理する場合のもう1つの重要なポイントは、トランザクションの結果を変更し、パフォーマンスに影響を与える可能性があるため、分離レベルによって決定される動作を考慮することです。 簡単にするために、例ではデフォルトのもの(繰り返し可能な読み取り)のみを考慮します。

4. それを実践する

ここで、アプリケーション側から、これらのプロパティを処理する方法と、どのレイヤーがそのような動作にアクセスできるかを理解しようとします。 しかし、繰り返しになりますが、それを行うには多くの異なる方法があり、フレームワークに応じて、これは変わる可能性があることは明らかです。 したがって、JPAとSpringを例にとると、他の状況でもどのように見えるかをよく理解できます。

4.1. JPA

JPA / Hibernateを使用して、アプリケーションで読み取り専用トランザクションを効果的に定義する方法を見てみましょう。

EntityManagerFactory entityManagerFactory = Persistence.createEntityManagerFactory("jpa-unit");
EntityManager entityManager = entityManagerFactory.createEntityManager();
entityManager.unwrap(Session.class).setDefaultReadOnly(true);
entityManager.getTransaction().begin();
entityManager.find(Book.class, id);
entityManager.getTransaction().commit();

JPAで読み取り専用トランザクションを定義する標準的な方法がないことに注意することが重要です。 そのため、実際のHibernateセッションを取得して、読み取り専用として定義する必要がありました。

4.2. JPA + Spring

Springトランザクション管理システムを使用すると、次に示すように、さらに簡単になります。

@Transactional(readOnly = true)
public Book getBookById(long id) {
    return entityManagerFactory.createEntityManager().find(Book.class, id);
}

これにより、 Springがトランザクションモードのオープン、クローズ、定義を担当します。ただし、Spring Data JPAを使用する場合のように、これが不要な場合もあります。

Spring JPAリポジトリ基本クラスは、すべてのメソッドを読み取り専用トランザクションとしてマークします。 このアノテーションをクラスレベルで追加することにより、メソッドレベルで @Transactional を追加するだけで、メソッドの動作を変更できます。

最後に、データソースを構成するときに、読み取り専用接続を定義し、autcommitプロパティを変更することもできます。 これまで見てきたように、読み取りのみが必要な場合、これによりアプリケーションのパフォーマンスをさらに向上させることができます。 データソースはこれらの構成を保持します。

@Bean
public DataSource readOnlyDataSource() {
    HikariConfig config = new HikariConfig();
    config.setJdbcUrl("jdbc:mysql://localhost/baeldung?useUnicode=true&characterEncoding=UTF-8");
    config.setUsername("baeldung");
    config.setPassword("baeldung");
    config.setReadOnly(true);
    config.setAutoCommit(true);
    return new HikariDataSource(config);
}

ただし、これは、アプリケーションの主な特性が単一のクエリリソースであるシナリオでのみ意味があります。 また、Spring Data JPAを使用する場合は、Springによって作成されたデフォルトのトランザクションを無効にする必要があります。 したがって、enableDefaultTransactionsプロパティをfalseに構成するだけで済みます。

@Configuration
@EnableJpaRepositories(enableDefaultTransactions = false)
@EnableTransactionManagement
public class Config {
    //Definition of data sources and other persistence related beans
}

この時点から、必要に応じて @Transactional(readOnly = true)を追加する完全な制御と責任があります。 それでも、これはアプリケーションの大部分には当てはまらないため、アプリケーションがそれらから利益を得ることが確実でない限り、これらの構成を変更しないでください。

4.3. ルーティングステートメント

より現実的なシナリオでは、ライター1つと読み取り専用 oneの2つのデータソースを持つことができます。 次に、コンポーネントレベルで使用するデータソースを定義する必要があります。 このアプローチは、読み取り接続をより効率的に処理し、セッションがクリーンで適切なセットアップであることを確認するために使用される不要なコマンドを防ぎます

この結果に到達する方法は複数ありますが、最初にルーターデータソースクラスを作成します。

public class RoutingDS extends AbstractRoutingDataSource {

    public RoutingDS(DataSource writer, DataSource reader) {
        Map<Object, Object> dataSources = new HashMap<>();
        dataSources.put("writer", writer);
        dataSources.put("reader", reader);

        setTargetDataSources(dataSources);
    }

    @Override
    protected Object determineCurrentLookupKey() {
        return ReadOnlyContext.isReadOnly() ? "reader" : "writer";
    }
}

ルーティングデータソースについて知っておくべきことがたくさんあります。 ただし、要約すると、この場合、このクラスは、アプリケーションが要求したときに適切なデータソースを返します。 これを行うには、実行時にデータソースコンテキストを保持するReadOnlyContentクラスを使用します。

public class ReadOnlyContext {

    private static final ThreadLocal<AtomicInteger> READ_ONLY_LEVEL = ThreadLocal.withInitial(() -> new AtomicInteger(0));

    //default constructor

    public static boolean isReadOnly() {
        return READ_ONLY_LEVEL.get()
            .get() > 0;
    }

    public static void enter() {
        READ_ONLY_LEVEL.get()
            .incrementAndGet();
    }

    public static void exit() {
        READ_ONLY_LEVEL.get()
            .decrementAndGet();
    }
}

次に、これらのデータソースを定義し、Springコンテキストに登録する必要があります。 このために必要なのは、以前に作成したRoutingDSクラスのみです。

//annotations mentioned previously
public Config {
    //other beans...

    @Bean
    public DataSource routingDataSource() {
        return new RoutingDS(
          dataSource(false, false),
          dataSource(true, true)
        );
    }
    
    private DataSource dataSource(boolean readOnly, boolean isAutoCommit) {
        HikariConfig config = new HikariConfig();
        config.setJdbcUrl("jdbc:mysql://localhost/baeldung?useUnicode=true&characterEncoding=UTF-8");
        config.setUsername("baeldung");
        config.setPassword("baeldung");
        config.setReadOnly(readOnly);
        config.setAutoCommit(isAutoCommit);
        return new HikariDataSource(config);
    }

    // other beans...
}

もうすぐです— 注釈を作成して、コンポーネントを読み取り専用コンテキストでラップするタイミングをSpringに通知しましょう。 このために、@ReaderDSアノテーションを使用します。

@Inherited
@Retention(RetentionPolicy.RUNTIME)
public @interface ReaderDS {
}

最後に、 AOP を使用して、コンポーネントの実行をコンテキスト内でラップします。

@Aspect
@Component
public class ReadOnlyInterception {
    @Around("@annotation(com.baeldung.readonlytransactions.mysql.spring.ReaderDS)")
    public Object aroundMethod(ProceedingJoinPoint joinPoint) throws Throwable {
        try {
            ReadOnlyContext.enter();
            return joinPoint.proceed();
        } finally {
            ReadOnlyContext.exit();
        }
    }
}

通常、可能な限り最高のポイントレベルで注釈を追加します。 それでも、簡単にするために、リポジトリレイヤーを追加します。コンポーネントには、クエリが1つだけあります。

public interface BookRepository extends JpaRepository<BookEntity, Long> {

    @ReaderDS
    @Query("Select t from BookEntity t where t.id = ?1")
    BookEntity get(Long id);
}

ご覧のとおり、この設定では、読み取り専用トランザクション全体を活用し、セッションコンテキストスイッチを回避することで、読み取り専用操作をより効率的に処理できます。 その結果、これにより、アプリケーションのスループットと応答性が大幅に向上する可能性があります。

5. 結論

この記事では、読み取り専用トランザクションとその利点について説明しました。 また、MySQL InnoDBエンジンがそれらをどのように処理するか、およびアプリケーションのトランザクションに影響を与える主要なプロパティを構成する方法も理解しました。 さらに、専用のデータソースなどの専用のリソースを使用して、追加の改善の可能性について説明しました。 いつものように、この記事で使用されているすべてのコードサンプルは、GitHubから入手できます。