こんにちは。 エキサイト株式会社の三浦です。
DBでは、可用性の担保のために Primary
と Replication
のインスタンスをそれぞれ用意することが多々あります。
こうしたDBをSpring Bootで扱う際、適切にSQL実行先を振り分けないとせっかく分けた意味がなくなってしまいます。
今回は、適切に Primary
/ Replication
インスタンスにSQLを振り分ける方法を2通り紹介します。
DBの可用性と Primary
/ Replication
インスタンス
DBは、多くのアプリケーションにとって、アプリケーション上で扱うデータが入っている重要なサービスであり、DBに障害が起きてしまった場合サービス自体に障害が起きてしまうケースがほとんどです。 そのため、DBの可用性を高めることは、DBを扱うアプリケーションにとってはかなり重要性の高い要件と言えます。
DBの可用性を高める方法の1つに、 Primary
/ Replication
インスタンスを別々に用意する、というものがあります。
Primary
インスタンスではDBへのデータの書き込み、及び読み込みの両方が可能であり、常にオリジナルのデータが入っています。
データの整合性を保つため、基本的に Primary
インスタンスは1台のみで、かつ書き込みはこのインスタンスのみにしか行なえません。
Replication
インスタンスではデータの読み込みのみが可能であり、 Primary
インスタンスのデータを常時同期するようになっています。
Replication
インスタンスではあくまで複製されたデータのみを扱い、かつ読み込みのみを行うため、 Primary
インスタンスと異なり複数台作ることが可能です。
多くのアプリケーションでは書き込みに対して読み込みの量のほうが圧倒的に多いため、上記の構成にした上で、基本的に Primary
インスタンスでは書き込みのみ、 Replication
インスタンスでは読み込みのみを行うようにし、アクセス量に応じて Replication
インスタンスの数を増やすことで負荷を分散して、可用性を担保します。
Spring Bootで上記のような構成のDBを扱う際も、もちろん Primary
インスタンスでは書き込みのみ、 Replication
インスタンスでは読み込みのみを行うようにする必要があります。
Spring Bootで Primary
/ Replication
インスタンスを持つDBを扱う方法
Spring BootでこういったDBを扱う際は、 JDBC
の機能を使用する方法と、 DataSource
クラスをカスタマイズする方法の2通りが主に存在します。
JDBCの機能を使用する方法
おそらく最も簡単な方法が JDBC
の機能を使用する方法です。
JDBCには replication
の機能があり、以下の2工程でPrimary
/ Replication
に適切に振り分けてくれるようになります。
1. @Transactional
を使用する
Primary
インスタンスにアクセスしてほしいメソッドには @Transactional()
アノテーションを、 Replication
インスタンスにアクセスしてほしいメソッドには @Transactional(readOnly = true)
アノテーションを付与する
2. アクセス先DBのURLを以下のように指定する
jdbc:mysql:replication://primary_db:3306,replication_db:3306/sample_schema
これだけで、適切にSQLを割り振ってくれます。 これには以下のメリット・デメリットが存在します。
メリット
- 非常に簡単に指定できる
デメリット
- JDBCのreplication機能が使えない場合は使用できない
- コネクションプールで保持されるコネクション数が
Primary
とReplication
で同数になってしまうPrimary
インスタンスは1台しか作れないため、Primary
の最大同時接続数にコネクション数を合わせるとReplication
用のコネクションが不足したり、Replication
で必要なコネクション数に合わせるとPrimary
の最大同時接続数が足りなくなってしまう恐れがある
DataSource
クラスをカスタマイズする方法
DataSouce
を使う方法は少し複雑ですが、JDBCに比べて柔軟な対応が可能になります。
DataSource
を使う場合も、JDBCと同じく @Transactional
で振り分け先を指定します。
変わるのはその後です。
1. @Transactional
を使用する
Primary
インスタンスにアクセスしてほしいメソッドには @Transactional()
アノテーションを、 Replication
インスタンスにアクセスしてほしいメソッドには @Transactional(readOnly = true)
アノテーションを付与する
2. DataSource
周りをカスタマイズする
DataSource
周りをカスタマイズします。
ここでは HikariDataSource
を使用していますが、おそらく他の DataSource
を使っても大丈夫だと思います。
また、MaximumPoolSize
等の細かい設定も、必要に応じて変更してください。
今回はコード内に直接各種プロパティを書いていますが、実際に使用する際はそういったプロパティは application.properties
などに書いておき、このコード内で呼び出すようにするほうが管理しやすいでしょう。
import com.zaxxer.hikari.HikariDataSource; import org.apache.ibatis.session.SqlSessionFactory; import org.mybatis.spring.SqlSessionFactoryBean; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.boot.autoconfigure.jdbc.DataSourceProperties; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Primary; import org.springframework.jdbc.datasource.DataSourceTransactionManager; import org.springframework.jdbc.datasource.LazyConnectionDataSourceProxy; import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource; import org.springframework.transaction.PlatformTransactionManager; import org.springframework.transaction.support.TransactionSynchronizationManager; import java.util.HashMap; import java.util.Map; import java.util.Objects; import javax.sql.DataSource; @Configuration public class DataSourceConfig { private final String primaryDataSourcePropertiesName = "primaryDataSourceProperties"; private final String replicationDataSourcePropertiesName = "replicationDataSourceProperties"; private final String primaryDataSourceName = "primaryDataSource"; private final String replicationDataSourceName = "replicationDataSource"; private final String routerDataSourceName = "routingDataSource"; private final String mainDataSourceName = "dataSource"; public enum DataSourceType { PRIMARY, REPLICATION } /** * TransactionのreadOnlyをもとにDataSourceをルーティングするためのカスタムルーティングクラス */ public static class CustomRoutingDataSource extends AbstractRoutingDataSource { @Override protected Object determineCurrentLookupKey() { return TransactionSynchronizationManager.isCurrentTransactionReadOnly() ? DataSourceType.REPLICATION : DataSourceType.PRIMARY; } } /** * Primary用DataSourceのプロパティを作成 * @return Primary用DataSourceのプロパティ */ @Bean(name = primaryDataSourcePropertiesName) @Primary public DataSourceProperties primaryDataSourceProperties() { DataSourceProperties dataSourceProperties = new DataSourceProperties(); dataSourceProperties.setDriverClassName("com.mysql.cj.jdbc.Driver"); dataSourceProperties.setUsername("user"); dataSourceProperties.setPassword("password"); dataSourceProperties.setUrl("jdbc:mysql://primary_db:3306/sample_schema"); return dataSourceProperties; } /** * Replication用DataSourceのプロパティを作成 * @return Replication用DataSourceのプロパティ */ @Bean(name = replicationDataSourcePropertiesName) public DataSourceProperties replicationDataSourceProperties() { DataSourceProperties dataSourceProperties = new DataSourceProperties(); dataSourceProperties.setDriverClassName("com.mysql.cj.jdbc.Driver"); dataSourceProperties.setUsername("user"); dataSourceProperties.setPassword("password"); dataSourceProperties.setUrl("jdbc:mysql://replication_db:3306/sample_schema"); return dataSourceProperties; } /** * Primary用DBエンドポイントのDataSourceを作成 * @return Primary用DBエンドポイントのDataSource */ @Bean(name = primaryDataSourceName) @Primary public DataSource primaryDataSource(@Qualifier(primaryDataSourcePropertiesName) DataSourceProperties dataSourceProperties) { HikariDataSource hikariDataSource = dataSourceProperties.initializeDataSourceBuilder().type(HikariDataSource.class).build(); hikariDataSource.setMaxLifetime(600000); hikariDataSource.setMaximumPoolSize(10); hikariDataSource.setReadOnly(false); return hikariDataSource; } /** * Replication用DBエンドポイントのDataSourceを作成 * @return Replication用DBエンドポイントのDataSource */ @Bean(name = replicationDataSourceName) public DataSource replicationDataSource(@Qualifier(replicationDataSourcePropertiesName) DataSourceProperties dataSourceProperties) { HikariDataSource hikariDataSource = dataSourceProperties.initializeDataSourceBuilder().type(HikariDataSource.class).build(); hikariDataSource.setMaxLifetime(600000); hikariDataSource.setMaximumPoolSize(20); hikariDataSource.setReadOnly(true); return hikariDataSource; } /** * SQLを実行するDataSourceを、Transactionによってルーティングするための設定を作成 * @param primaryDataSource Primary用DBエンドポイントのDataSource * @param replicationDataSource Replication用DBエンドポイントのDataSource * @return SQLを実行するDataSourceを、Transactionによってルーティングするための設定 */ @Bean(name = routerDataSourceName) public DataSource routingDataSource( @Qualifier(primaryDataSourceName) final DataSource primaryDataSource, @Qualifier(replicationDataSourceName) final DataSource replicationDataSource ) { CustomRoutingDataSource routingDataSource = new CustomRoutingDataSource(); Map<Object, Object> dataSourceMap = new HashMap<>(); dataSourceMap.put(DataSourceType.PRIMARY, primaryDataSource); dataSourceMap.put(DataSourceType.REPLICATION, replicationDataSource); routingDataSource.setTargetDataSources(dataSourceMap); routingDataSource.setDefaultTargetDataSource(primaryDataSource); return routingDataSource; } /** * TransactionによってDataSourceをルーティングするDataSourceを作成 * @param routingDataSource SQLを実行するDataSourceを、Transactionによってルーティングするための設定 * @return TransactionによってDataSourceをルーティングするDataSource */ @Bean(name = mainDataSourceName) public DataSource dataSource(@Qualifier(routerDataSourceName) DataSource routingDataSource) { return new LazyConnectionDataSourceProxy(routingDataSource); } /** * トランザクションマネージャを作成 * @param dataSource TransactionによってDataSourceをルーティングするDataSource * @return トランザクションマネージャ */ @Bean @Primary public PlatformTransactionManager transactionManager(@Qualifier(mainDataSourceName) DataSource dataSource) { return new DataSourceTransactionManager(dataSource); } /** * SQLセッションファクトリを作成 * @param dataSource TransactionによってDataSourceをルーティングするDataSource * @return SQLセッションファクトリ * @throws Exception 例外 */ @Bean @Primary public SqlSessionFactory sqlSessionFactory(@Qualifier(mainDataSourceName) DataSource dataSource) throws Exception { SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean(); sqlSessionFactoryBean.setDataSource(dataSource); return sqlSessionFactoryBean.getObject(); } }
少し長くなりましたが、この設定で適切にSQLを割り振ってくれます。 これには以下のメリット・デメリットが存在します。
メリット
- JDBCのreplicationを使えなくても使用することが出来る
Primary
/Replication
でコネクションプールのコネクション数などを完全に別々に設定することが出来る
デメリット
- 設定が少し複雑
最後に
今回は、 Primary
/ Replication
構成のDBにSpring Bootからアクセスする方法を2つ紹介しました。
個人的には、
が適切だと思っており、そのため、多くの場合 DataSource
をカスタマイズする方法が適切なのではないかと考えています。
もちろん上記で挙げた以外の思わぬ副作用がある可能性もありますし、アプリケーションごとに様々な要件もあると思いますので、一概に「どちらがいい」と言うのは難しいところです。 このあたりは、各アプリケーションごとに判断していただけると良いかと思います。
この記事が何かしら参考になりましたら幸いです。