Springフレームワークによる分散トランザクション検証(Atomikos)

こんにちは。エンジニアマネージャーの岩本です。
最近はマネージメントの傍らPJの基盤技術を構築したりしています。

最近新規ゲームPJの基盤構築をしていて、MySQLの分散トランザクションについて検証をしました。
運用中のタイトルでは結構力技でDB分散をしていますが、「保守が大変」「もっとスマートに出来るようになりたい」という懸念点がありました。
せっかく新規で作れるならばこの際思い切って変えてしまえ!!という勢いです。
※運用中のタイトルのDB分散はこちらの記事を参照して下さい。

MySQLの分散トランザクション(XAトランザクション)のクエリー例が下記となります。
ローカル環境でテストをしたときのログを表示しています。DBのスキーマーを2つ用意してそれぞれにselect,insertをしたときの例です。
ログの見方は左から「ID」「コマンド」「クエリー」です。

DBログ

通常のトランザクションと違うところは「XA START」「XA END」「XA PREPARE」「XA COMMIT」です。コマンドの詳しい説明はMySQLマニュアルを参照して下さい。
今回紹介するのは、オープンソースのJTA実装であるAtomikosを使用した分散トランザクションの導入です。Javaでオープンソースな分散トランザクションのライブラリですが、ググってもAtomikos以外の情報があまり無いですね。商用版ならば何個かあったのですが・・・という状況です。良いのあったら教えて下さい!!

また、設定に関して追加したい項目の記載がなかったため、ライブラリのソースを確認しながらの作業となりました。

Atomikosマニュアル

検証環境は以下のとおりです。

– MySQL5.6
– Java:1.8
– spring : 4.1.4.RELEASE
– mybatis : 3.3.0
– mybatis-spring : 1.2.3
– mysql-connector-java : 5.1.36
– Atomikos transactions-jdbc: 3.9.3
– jta : 1.1

Spring+MyBatis+Atomikosとの連携方法

下記図1のような2つのuserDB構成のサンプルを作っていきます。「user_01DB」「user_02DB」の各DBのテーブル構成は同じです。各DBの振り分けのロジックは、「ユーザIDをDB数で割った余り」のような単純なロジックで既にアプリ側に実装されているものとします。

データベース構成図

データソース設定

   <!-- データソースの共通設定 -->
    <bean id="xaDataSourceBase" abstract="true" class="com.atomikos.jdbc.AtomikosDataSourceBean" init-method="init" destroy-method="close">
        <property name="xaDataSourceClassName" value="com.mysql.jdbc.jdbc2.optional.MysqlXADataSource" />
        <property name="borrowConnectionTimeout" value="5" />
        <property name="maxIdleTime" value="600" />
        <property name="reapTimeout" value="0" />
        <property name="maintenanceInterval" value="600" />
        <property name="loginTimeout" value="0" />
        <property name="maxLifetime" value="3600" />
        <property name="xaProperties">
            <props merge="true" >
                <prop key="useUnicode">true</prop>
                <prop key="characterEncoding">UTF-8</prop>
                <prop key="socketTimeout">3000</prop>
                <prop key="connectTimeout">3000</prop>
                <prop key="pinGlobalTxToPhysicalConnection">true</prop>
            </props>
        </property>
    </bean>

    <!--  ***** 個別データソース設定はここから下 ***** -->
    <!-- ユーザ1 スキーマ設定 -->
    <bean id="userDataSource_1" parent="xaDataSourceBase">
        <property name="uniqueResourceName" value="master" />
        <property name="minPoolSize" value="1" />
        <property name="maxPoolSize" value="100" />
        <property name="xaProperties">
            <props merge="true">
                <prop key="url">jdbc:mysql://127.0.0.1:3306/user_01</prop>
                <prop key="user">root</prop>
                <prop key="password"></prop>
            </props>
        </property>
    </bean>

    <!-- ユーザ2 スキーマ設定 -->
    <bean id="userDataSource_2" parent="xaDataSourceBase">
        <property name="uniqueResourceName" value="user" />
        <property name="minPoolSize" value="1" />
        <property name="maxPoolSize" value="100" />
        <property name="xaProperties">
            <props merge="true">
                <prop key="url">jdbc:mysql://127.0.0.1:3306/user_02"</prop>
                <prop key="user">root</prop>
                <prop key="password"></prop>
            </props>
        </property>
    </bean>

今回データソースとして、Atomikosのcom.atomikos.jdbc.AtomikosDataSourceBeanを使用しています。これをdbcpとあわせて使用したい場合はorg.apache.commons.dbcp.BasicDataSourceに変えて設定も合わせて下さい。

設定のポイントとしては、DBを分散する時に「共通の設定」と「DB個別の設定」に分けている点です。これによりDBが増えたときの設定ファイルの肥大化を抑えています。

共通設定を作成するポイント

<bean id="xaDataSourceBase" abstract="true"/>

上記設定部分の「abstract=”true”」を追加しabstract属性にすることです。parent属性と合わせて使用するとJavaの継承パターンを実現することができます

DB毎設定を作成するポイント:

<bean id="userDataSource_1" parent="xaDataSourceBase">
    <property name="xaProperties">
        <props merge="true">

parent="xaDataSourceBasexaDataSourceBaseを継承しています。 props merge="true"で親のxaPropertiesの定義にDB毎の個別設定を追加しています。

これにより各DB共通の項目はxaDataSourceBaseにまとめ個別項目は「userDataSource_1」と「userDataSource_2」に分けることが出来ます。

MyBatisとの連携設定

   <!--  ***** 共通設定 ***** -->
    <bean id="baseSessionFactory" abstract="true" class="org.mybatis.spring.SqlSessionFactoryBean">
        <property name="typeAliasesPackage" value="jp.co.apb.game.core.entities.dto" />
        <property name="configLocation" value="classpath:db/mybatis-config.xml"/>
    </bean>

    <!-- user1 -->
    <bean id="userSessionFactory_01" parent="baseSessionFactory" abstract="true" class="org.mybatis.spring.SqlSessionFactoryBean">
        <property name="dataSource" ref="userDataSource_01"/>
        <property name="mapperLocations" value="classpath*:db/sql/user/**/*.xml" />
    </bean>
    <bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">
        <property name="sqlSessionFactory" ref="userSessionFactory_01"/>
        <property name="basePackage" value= "jp.co.apb.game.core.entities.mapper.user" />
    </bean>

    <!-- user2 -->
    <bean id="userSessionFactory_02" parent="baseSessionFactory" abstract="true" class="org.mybatis.spring.SqlSessionFactoryBean">
        <property name="dataSource" ref="userDataSource_02"/>
        <property name="mapperLocations" value="classpath*:db/sql/user/**/*.xml" />
    </bean>
    <bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">
        <property name="sqlSessionFactory" ref="userSessionFactory_02"/>
        <property name="basePackage" value= "jp.co.apb.game.core.entities.mapper.user" />
    </bean>

同じように共通設定と個別設定をわけています。その他の設定はMyBatisのマニュアル通りです。

また実際のプロジェクトでは、Mapperの設定に関してDBが増えても設定があまり増えないようにカスタマイズをしています。

トランザクションマネージャー設定

   <!-- 分散トランザクション -->
    <bean id="setAtomikosSystemProps" class="org.springframework.beans.factory.config.MethodInvokingFactoryBean">
        <property name="targetObject">
            <!-- System.getProperties() -->
            <bean class="org.springframework.beans.factory.config.MethodInvokingFactoryBean">
                <property name="targetClass" value="java.lang.System" />
                <property name="targetMethod" value="getProperties" />
            </bean>
        </property>
        <property name="targetMethod" value="putAll" />
        <property name="arguments">
            <util:properties>
                <!-- atomikosの設定ファイル検索を行わない(userTransactionServiceのコンストラクタで渡しているため) -->
                <prop key="com.atomikos.icatch.no_file">true</prop>
                <!--  ファクトリクラスの指定 -->
                <prop key="com.atomikos.icatch.service">com.atomikos.icatch.standalone.UserTransactionServiceFactory</prop>
            </util:properties>
        </property>
    </bean>
    <bean id="userTransactionService" class="com.atomikos.icatch.config.UserTransactionServiceImp" depends-on="setAtomikosSystemProps" init-method="init" destroy-method="shutdownForce">
        <!-- 設定の説明:http://www.atomikos.com/Documentation/JtaProperties -->
        <constructor-arg>
            <props>
                <prop key="com.atomikos.icatch.max_timeout">10000</prop>
                <prop key="com.atomikos.icatch.default_jta_timeout">10000</prop>
                <prop key="com.atomikos.icatch.max_actives">200</prop>
                <prop key="com.atomikos.icatch.serial_jta_transactions">false</prop>
                <prop key="com.atomikos.icatch.log_base_dir">./</prop>
                <prop key="com.atomikos.icatch.log_base_name">atomikos.log</prop>
            </props>
        </constructor-arg>
    </bean>

    <bean id="atomikosTransactionManager" class="com.atomikos.icatch.jta.J2eeTransactionManager" depends-on="userTransactionService"/>
    <bean id="atomikosUserTransaction" class="com.atomikos.icatch.jta.J2eeUserTransaction" depends-on="userTransactionService"/>
    <bean id="txManager" class="org.springframework.transaction.jta.JtaTransactionManager" depends-on="userTransactionService">
        <property name="transactionManager" ref="atomikosTransactionManager" /> 
        <property name="userTransaction" ref="atomikosUserTransaction" /> 
    </bean>

    <!-- アノテーションベースのトランザクション管理を有効にする -->
    <tx:annotation-driven transaction-manager="txManager" />

トランザクションンマネージャーとして、Atomikosを使う設定です。userTransactionServiceImplは、初期化時にデフォルトでtransactions.propertiesというプロパティファイルを検索し、見つからないと警告ログを出力します。
設定ファイルを複数に分けたくないためsetAtomikosSystemPropsでプロパティファイルを検索しないように設定しています。

まとめ

マニュアル等で公開されている設定方法は、説明の都合上必要最低限の設定の場合が多い印象でした。
実際に実務で使用する場合は、プロジェクトの要件に合わせてカスタマイズする必要がありますが、その一例として参考にしていただければ幸いです。

次回は、分散トランザクションとは違うアプローチで、「SpringのChainedTransactionManager」と「LazyConnectionDataSourceProxy」を使用したトランザクションの構築について紹介出来ればと思っています。