LastaFluteでMaster/SlaveDB

master/slave対応の概要

同じ Schema に対して、MasterDB と SlaveDB と二つ以上のDBを用意する、Master/SlaveDB 構成のときの LastaFlute でのデフォルトのやり方が存在します。

master/slaveの目的

そもそもの Master/SlaveDB の目的として、以下のことを想定しています。

  • A. MasterDB に障害が発生したら SlaveDB が代わってサービス継続
  • B. 検索の一部をslaveにして、DBサーバーの負荷分散 ☆ポイント!

A だけであればアプリは意識しないのであまり関係ありませんが、B を(アプリで)やるのであれば、意識する必要があります。

master/slave対応の流れ

LastaFluteでの master/slave 対応は、以下のような流れです。

  • SelectableDataSourceProxy を想定した Di xml 構造を構築する
  • SlaveDBAccessor で SlaveDB に検索する

キーポイント: SelectableDataSourceProxy

SelectableDataSourceProxy がキーポイントとなります。 DBFluteが使う DataSource を、この Proxy に差し替え、動的に master と slave の DataSource を差し替えられるようにします。

このクラス自体は、もともと master/slave のための機能ではなく、冗長化複数DBのための機能です。 master/slaveも冗長化複数DBの一つのパターンとして捉えています。

ただ、実際にコンポーネント登録するクラスは、SelectableDataSourceProxy を継承して、 master/slave に最適化させた MasterBasisSelectableDataSource です。

e.g. Master/SlaveDBの DI xml の include 構成 @Dixml
javax.sql.DataSource
 ^
 |-SelectableDataSourceProxy // 冗長化複数DBのためのクラス
    ^
    |-MasterBasisSelectableDataSource // master/slaveのためのクラス

Master/SlaveDBの設定方法

Di xml構造

こういう、Di xml構成を作ります。

e.g. Master/SlaveDBの DI xml の include 構成 @Dixml
app.xml // 手作り: アプリの Di xml のルート
 |
 |-dbflute.xml // DBFlute自動生成: includes jta.xml, jdbc.xml, tx_aop.xml
 |  |
 |  |-rdb.xml // Lasta Di組込み: includes jta.xml, jdbc.xml, tx_aop.xml
 |  |  |
 |  |  |-jta.xml // Lasta Di組込み: TransactionManager, UserTransactionなど
 |  |  |-(jdbc.xml) // LastaFlute組込み: jdbc+.xmlに上書きされる
 |  |  |-tx_aop.xml // Lasta Di組込み: まあ、気にしなくていい
 |  |  |
 |  |  |-jdbc+.xml // ☆手作り: SelectableDataSourceProxy(dataSource)を定義
 |  |  |  |
 |  |  |  |-plugin/selectable_datasource.xml // LastaFlute組込み: SelectableDataSourceHolderの定義
 |  |  |  |
 |  |  |  |-jdbc-master.xml // ☆手作り: master用のjdbc.xml
 |  |  |  |  |
 |  |  |  |  |-jta.xml // ConnectionPoolなどがTransactionManagerを使うため
 |  |  |  |  |-lastaflute_assist.xml // [app]_config.propertiesを使うため
 |  |  |  |
 |  |  |  |-jdbc-slave.xml // ☆手作り: slave用のjdbc.xml
 |  |  |  |  |
 |  |  |  |  |-jta.xml // masterと同じ
 |  |  |  |  |-lastaflute_assist.xml // masterと同じ
... 

jdbc+.xml

アプリで、jdbc+.xml を作成します。("+" を付けると、LastaFlute組込みの jdbc.xml が完全上書きされます)

ここで SelectableDataSourceProxy をコンポーネント定義することで、 DBFlute が利用する DataSource がProxy化され、masterに接続するのかslaveに接続するのか切り分けられます。 また、SlaveDBAccessor も定義することで、アプリで SlaveDB に狙ってアクセスできます。

e.g. jdbc+.xml for master/slave  @Dixml
<components>
    <include path="plugin/selectable_datasource.xml"/>
    <include path="jdbc-master.xml"/>
    <include path="jdbc-slave.xml"/>
    <component name="dataSource" class="org.lastaflute.db.replication.slavedb.MasterBasisSelectableDataSource"/>
    <component name="slaveDBAccessor" class="org.lastaflute.db.replication.slavedb.SlaveDBAccessorImpl"/>
</components>

ただ、SelectableDataSourceProxyは、別に Master/SlaveDB 専用のクラスではなく、デフォルトのDBを定めていませんので、実際に登録する具象クラスは、MasterBasisSelectableDataSource にすると良いでしょう。そうすると、DataSourceのキーを何も指定してないときに、MasterDB をアクセスするようになります。 (SelectableDataSourceProxyだと、何も指定されていなければ例外)

SelectableDataSourceProxy が、SelectableDataSourceHolder を利用するので、LastaFlute組込みの plugin/selectable_datasource.xml を include します。

SelectableDataSourceProxy のコンポーネント名は、DBFluteがDataSourceをDIするときに使う名前にします。 複数DB構成とかにしていなければ、多くの場合デフォルトの "dataSource" でOKです。 (厳密には、DBFluteも型でDIしたりするので、あまり名前は関係ないかもですが、しっかり合わせておきましょう)

jdbc-master.xml

アプリで、jdbc-master.xml を作成します。

内容は、LastaFlute組込みの jdbc.xml を参考にして、master用に書き換えたものにします。

ただ、DataSourceコンポーネントに関しては(xaDataSourceの方ではありません)、コンポーネント名を masterDataSource に変更します。仕組みの中では、そのコンポーネント名で特定します。

また、provider.config().getXxx() 部分も、master用のプロパティを定義して FreeGen でメソッドを自動生成しましょう。 (デフォルトの接続設定をmasterにするとかであれば、そのままでも)

jdbc-slave.xml

アプリで、jdbc-slave.xml を作成します。

内容は、LastaFlute組込みの jdbc.xml を参考にして、slave用に書き換えたものにします。

ただ、DataSourceコンポーネントに関しては(xaDataSourceの方ではありません)、コンポーネント名を slaveDataSource に変更します。仕組みの中では、そのコンポーネント名で特定します。

また、provider.config().getXxx() 部分も、slave用のプロパティを定義して FreeGen でメソッドを自動生成しましょう。

もし、SlaveDB が複数になるケースでは、ファイル名やコンポーネント名の "slave" 部分を slaveSea や slaveLand などにして、SlaveDBの数だけ追加します。ただし、そのケースでは SlaveDBAccessor はそのままでは利用できません。 (SlaveDBAccessor は、slaveが一つであることを前提に実装されています)

LastaFluteをアップグレードするとき

アプリで、LastaFlute組込みの定義(jdbc.xml)やクラスを意識することになるので、めったには変わりませんが、LastaFlute のアップグレードをするときには必ずmaster/slave周りを意識して動作確認をしてください。(万が一、構造や名前が変わったりしたときのために)

アプリでの実装方法

ひとまず、ベタなやり方

アプリでの、非常に単純で ベタな実装方法 は以下のようになります。

e.g. SelectableDataSourceHolder を直接使ったベタな実装方法 @Java
@Resource
private MemberBhv memberBhv;
@Resource
private SelectableDataSourceHolder selectableDataSourceHolder; // injected

public void fooAndBar() {
    selectableDataSourceHolder.switchSelectableDataSourceKey("master");
    memberBhv.update(member); // master の会員を更新

    selectableDataSourceHolder.switchSelectableDataSourceKey("slave");
    ... = memberBhv.select(cb -> ...); // slave の会員を検索
}

SelectableDataSourceHolder は、LastaFlute に組み込まれている plugin/selectable_datasource.xml にて定義されています。 デフォルトでは何の DataSource とも関連付いていないため、DBアクセスする前は必ず何かしらの DB を指定する必要があります。

ただ、通常このような利用はあり得ず、仕組みをわかりやすく理解するための Example です。

SlaveDBAccessorを使ったやり方

現実的には、DBアクセスのたびに指定するのはあり得ない感じなので、LastaFluteで提供されている SlaveDBAccessor を使うと良いでしょう。

e.g. SlaveDBAccessorを使ってSlaveDBを狙ってアクセス @Java
@Resource
private SlaveDBAccessor slaveDBAccessor;

public void sea() {
    // 必ず slave に対して検索
    List<Member> memberList = slaveDBAccessor.accessFixedly(() -> {
        return memberBhv.selectList(cb -> ...);
    });

    // 引数の判定次第で slave に対して検索
    List<Member> memberList = slaveDBAccessor.accessIfNeeds(() -> {
        return memberBhv.selectList(cb -> ...);
    }, isSlaveDB()); // 何かしらアプリケーション的な判定

    // 引数の数値次第で半々に slave に対して検索
    List<Member> memberList = slaveDBAccessor.accessRandomFifty(() -> {
        return memberBhv.selectList(cb -> ...);
    }, getDeterminationNumber()); // ランダム判定のためのlong
}

万が一、SlaveDBのメソッド内で更新処理をしてしまったとしても、更新処理は自動的に master にアクセスします。 SlaveDBAccessorには、そういった安全対策が施されています。

独自に振り分けるやり方

現実的には、DBアクセスのたびに指定するのはあり得ない感じなので、Interceptor などの共通処理で切り替えます。デフォルトのDBの設定と切り替えのロジックを実装します。

例えば、通常は MasterDB の方にDBアクセスし、SlaveDB を示すアノテーションがメソッドに定義されていたら、そのメソッド内だけ SlaveDB にDBアクセスするというやり方が考えられます。

e.g. 独自に振り分けるアプリのInterceptorの実装 @Java
@Resource
private SelectableDataSourceHolder selectableDataSourceHolder; // injected

public Object invoke(MethodInvocation invocation) throws Throwable {
    String current = selectableDataSourceHolder.getSelectableDataSourceName();
    try {
        String selectableName = deriveSelectableDataSourceKey();
        selectableDataSourceHolder.switchSelectableDataSourceKey(selectableName);
        return invocation.proceed();
    } finally {
        selectableDataSourceHolder.switchSelectableDataSourceKey(current);
    }
}

protected String deriveSelectableDataSourceKey(MethodInvocation invocation) {
    if (hasSlaveAnnotation(invocation)) {
        return "slave";
    } else {
        return "master"; // MasterDB as default
    }
}

protected boolean hasSlaveAnnotation(MethodInvocation invocation) {
    // メソッドに SlaveDB を示すアノテーションが付いていたら true
    // (そのアノテーションは自作)
    return ...
}
e.g. 独自に振り分けたときのアプリの実装 (Logic) @Java
@Resource
private MemberBhv memberBhv;
@Resource
private PurchaseBhv purchaseBhv;

@SlaveDB
public void selectHeavy() { // このメソッド内のDBアクセスは全て SlaveDB へ
    MemberCB cb = ...
    ... = memberBhv.selectList(cb);
    PurchaseCB cb = ...
    ... = purchaseBhv.selectList(cb);
}

この Interceptor は、DBアクセスをする可能性のあるプロセスの入り口となるコンポーネント、 および、DBアクセス先を切り替える可能性のあるコンポーネントに関連付けます。例えば、Page, Action, Service, Logicクラスなどです。 バリデーションやバリデーションエラーなどの処理の中でDBアクセスをする場合は、それらメソッドにも関連付ける必要があります。

一方で、SlaveDBに更新処理を実行しないように注意する必要があります。ここでの例で言えば、SlaveDB アノテーションが付与されているメソッド内で insert() や upadte() を呼び出してはいけません。 SlaveDBAccessorの中でやっているような、"すべり止め" 処理も入れると良いでしょう。

ちょこちょこ注意点

Transactionは独立

Master と Slave のようなレプリケーション構成の場合は、そもそも更新処理を Master に集中させないといけないためあまり気にする必要はありませんが、TransactionはそれぞれのDBごとに独立したものになります。 そのことだけはしっかり理解しておいた方が良いでしょう。

SlaveDBは遅延の可能性

master/slaveをどのように実現しているか次第ですが、RDBのmaster/slave機能の都合上、どうしてもmasterの更新からslaveへの反映までに遅延が発生する可能性があります。 それを許容できない検索なのに slave を使ってしまうと、思わぬ事故を引き起こしてしまうかもしれませんので注意しましょう。

ローカル開発環境では、どうする?

ローカル開発環境で、master/slave構成がすんなり構築できるのであれば特に問題はありませんが、そうでない場合はちょっと注意が必要です。

その場合、masterもslaveも同じDBを参照すれば、つじつまが合ってテストはおおよそできますが、UnitTest などで、masterとslaveに対するTransactionが同時に発行されるときはちょっと困ります。

例えば、UnitTestの中でmasterに対して更新した結果が、slaveに対する検索で参照ができません。 同じDBでも、Tranasction が(同時に)別になっているので、masterに対する更新が Commit されるまで、slaveの方の Tranasction ではその更新結果を検索できないのです。 そこに依存しているロジックがあると、UnitTestがまともにできないという可能性もあります。

しょっちゅう出てくる問題ではないと思うので、問題が出てくるまでお茶を濁すか、ローカルだけは SlaveDBAccessorNothing を使うようにするとか、工夫が必要でしょう。(ローカルで簡単にmaster/slaveができちゃえば一番世話ないですが)