SpringのRoutingDataSourceで読取/書込を分離する方法!複数データソース構成を初心者向けに解説
生徒
「Springでデータベースの読み込みと書き込みを別々のDBに分けたいんですが、どうやってやるんですか?」
先生
「SpringではAbstractRoutingDataSourceを使って、読取用と書込用のデータソースを切り替えることができますよ。」
生徒
「それって初心者でも実装できるんですか?設定とか難しそうですけど…」
先生
「大丈夫です。順を追って説明するので、Spring初心者の方でも読取・書込分離をマスターできますよ!」
1. 複数データソース構成とは?
Springで複数のデータソース(DB接続)を定義する構成は、特にマイクロサービスや高トラフィックなシステムでよく使われます。読取用のReadOnlyデータソースと書込用のWriteデータソースを分けることで、処理を最適化できます。
たとえば、SELECTクエリは複数のレプリカに振り分け、INSERTやUPDATEはマスターデータベースに送ることで、DBの負荷を分散できます。
2. RoutingDataSourceの仕組み
RoutingDataSourceは、AbstractRoutingDataSourceクラスを継承して、データソースのルーティングロジックを定義する仕組みです。SpringのトランザクションやAOPと組み合わせて、「この処理は読取」「これは書込」という判断を行います。
たとえば、スレッドローカル変数で現在のコンテキストを保持し、determineCurrentLookupKey()で返すキーに応じて、実際のデータソースを切り替えます。
3. RoutingDataSourceの自作クラス例
次のように、どのデータソースを使うかを判断するためのカスタムクラスを作成します。
public class RoutingDataSource extends AbstractRoutingDataSource {
@Override
protected Object determineCurrentLookupKey() {
return DataSourceContextHolder.getDataSourceType();
}
}
そして、DataSourceContextHolderでスレッドローカルを制御します。
public class DataSourceContextHolder {
private static final ThreadLocal<String> contextHolder = new ThreadLocal<>();
public static void setDataSourceType(String type) {
contextHolder.set(type);
}
public static String getDataSourceType() {
return contextHolder.get();
}
public static void clearDataSourceType() {
contextHolder.remove();
}
}
4. 複数のデータソース定義とRoutingDataSourceの登録
Spring Bootの@Configurationで、書込用・読取用のデータソースとRoutingDataSourceを定義します。
@Configuration
public class DataSourceConfig {
@Bean
@Primary
public DataSource routingDataSource(
@Qualifier("writeDataSource") DataSource writeDataSource,
@Qualifier("readDataSource") DataSource readDataSource) {
Map<Object, Object> targetDataSources = new HashMap<>();
targetDataSources.put("WRITE", writeDataSource);
targetDataSources.put("READ", readDataSource);
RoutingDataSource routingDataSource = new RoutingDataSource();
routingDataSource.setTargetDataSources(targetDataSources);
routingDataSource.setDefaultTargetDataSource(writeDataSource);
return routingDataSource;
}
@Bean("writeDataSource")
public DataSource writeDataSource() {
return DataSourceBuilder.create()
.url("jdbc:mysql://localhost:3306/master")
.username("root")
.password("password")
.build();
}
@Bean("readDataSource")
public DataSource readDataSource() {
return DataSourceBuilder.create()
.url("jdbc:mysql://localhost:3306/replica")
.username("root")
.password("password")
.build();
}
}
5. AOPで処理ごとにデータソースを切り替える
読取専用の処理に対しては@ReadOnlyアノテーションなどを付け、それに応じてAOPでDataSourceContextHolderを切り替えるのが一般的です。
@Aspect
@Component
public class DataSourceAspect {
@Before("@annotation(ReadOnly)")
public void setReadDataSourceType() {
DataSourceContextHolder.setDataSourceType("READ");
}
@After("@annotation(ReadOnly)")
public void clearDataSourceType() {
DataSourceContextHolder.clearDataSourceType();
}
}
6. 読取/書込の実行時イメージ
書込処理の場合は、デフォルトのWRITEが使用され、明示的に@ReadOnlyをつけた場合のみREADが使用される構成になります。トランザクションの読み取り専用フラグ@Transactional(readOnly = true)とも連携可能です。
例として、読取系のサービスは次のように書きます。
@Service
public class UserService {
@ReadOnly
public List<User> findAllUsers() {
return userRepository.findAll();
}
public void createUser(User user) {
userRepository.save(user);
}
}
7. Spring Bootでの注意点と落とし穴
Spring BootでRoutingDataSourceを使う際の落とし穴として、以下のようなポイントがあります。
- Beanの初期化順序:
@Primaryを忘れると動かない。 - Hibernateのキャッシュ:セッションのキャッシュにより、切り替えが無視されるケースがある。
- 非同期処理:スレッドローカルが新しいスレッドに引き継がれないため、
@Asyncとの併用には注意。
これらは開発中に想定外の挙動を起こすことがあるので、ログで確認しながら動作検証するのが大切です。
8. 読取/書込分離のメリット
RoutingDataSourceを使ってSpringで複数データソース構成を取り入れることで、以下のようなメリットがあります。
- 読取と書込の負荷を分散してパフォーマンス向上
- レプリカ活用による可用性の向上
- 障害発生時のフェイルオーバーも可能
初心者でもポイントを押さえれば導入は難しくないので、まずは開発環境で試してみるのがおすすめです。
まとめ
Springで複数データソース構成を扱う場合、RoutingDataSourceを中心にした読取/書込分離の仕組みは、アプリケーション性能向上や安定稼働に欠かせない重要な技術です。特に高トラフィック環境では、SELECTクエリをレプリカDBに振り分け、INSERTやUPDATEはマスターデータベースに向けるといった読み書きの役割分担が、処理効率や可用性の向上に直結します。この記事で紹介したAbstractRoutingDataSourceの基本動作、ThreadLocalで現在のコンテキストを保持するアプローチ、AOPによるデータソース切り替えなどは、Springの柔軟なアーキテクチャを最大限に生かした典型的な実装パターンであり、初心者であっても順を追って理解すれば十分に扱える内容です。 RoutingDataSourceの仕組みは、determineCurrentLookupKey()が返すキーによって実際に利用されるDataSourceを動的に切り替えるという明快なものですが、バックエンドで行われていることはとても効率的です。たとえば、AOPによって呼び出し前にスレッドローカルへ“READ”や“WRITE”といった値をセットし、その値をRoutingDataSourceが受け取って適切な接続先を判断する仕組みは、柔軟性と拡張性を兼ね備えた美しい実装と言えます。また、開発現場ではトランザクション境界やHibernateのキャッシュが影響して、期待どおりにデータソースが切り替わらないケースもあるため、ログ確認やデバッグを通じた動作理解が非常に重要です。 今回の例でも示したように、@PrimaryでRoutingDataSourceをデフォルトに設定し、その他の個別データソース(readDataSource、writeDataSource)を@Beanで定義する手順は、Spring Bootにおける構成の基本形となります。非同期処理におけるThreadLocalの引き継ぎ問題、Bean初期化順序、Hibernateセッションとの関係性など、実運用における注意点も多く存在しますが、これらを理解しておくことで想定外の挙動に対応しやすくなり、より安定したアプリケーションを構築できます。 下記では、今回の学習内容を踏まえて、実際の読取/書込切替をより直感的に理解できるサンプルプログラムを紹介します。
サンプルプログラム:読取/書込の切替を行う簡易サービス例
@Service
public class ProductService {
@ReadOnly
public List<Product> loadProducts() {
// READデータソースへルーティング
return productRepository.findAll();
}
public void registerProduct(Product product) {
// WRITEデータソースへルーティング
productRepository.save(product);
}
}
@Aspect
@Component
public class SwitchAspect {
@Before("@annotation(ReadOnly)")
public void beforeRead() {
DataSourceContextHolder.setDataSourceType("READ");
}
@After("@annotation(ReadOnly)")
public void afterRead() {
DataSourceContextHolder.clearDataSourceType();
}
}
このように、アノテーションを付けるだけでデータソースが自動切替される仕組みは、コードの見通しを良くし、開発者が本来作りたいビジネスロジックに集中できる環境を作ります。SpringのAOPやDIとの高い親和性が、この柔軟な構成を支えています。 また、読取専用APIのパフォーマンス向上、レプリカの活用による冗長構成、フェイルオーバー時の耐障害性など、複数データソース構成には実務上のメリットが非常に多く、特にデータベース負荷が高いサービスでは導入効果が大きい点も理解しておきたいポイントです。初心者でも手順通りに構築すれば難しくないため、まずは小さなプロジェクトや学習環境で動作を試し、徐々に応用へ広げていくことをおすすめします。 それでは最後に、今回学んだ内容を振り返るための先生と生徒の会話形式で、気づきや理解のポイントを整理していきましょう。
生徒
「RoutingDataSourceって難しいイメージがあったんですが、仕組みが分かると意外とシンプルですね。スレッドローカルで切り替えるって発想が面白いです!」
先生
「そうなんですよ。Springは抽象化がしっかりしているので、実際の切替処理はとてもスマートに行われています。」
生徒
「AOPで処理前後にデータソースをセットしたりクリアしたりするのも分かりやすいですね。アノテーションで分離できるのが便利です!」
先生
「その理解はとても良いですね。アプリのどこでREAD、どこでWRITEを使うか明確になるので、保守性も高まります。」
生徒
「注意点として、非同期処理ではThreadLocalが引き継がれないという話も勉強になりました。落とし穴を知っておくことも大事ですね。」
先生
「その通りです。RoutingDataSourceは便利ですが、動きを確かめながら使うのが安全です。今回の理解を元に、さらに複雑な構成にも挑戦してみてください。」