- 問題の説明と分析
プロジェクトではSpringを用いてデータソースを動的に切り替えており、サービスにはDubboを使用しています。しばらく動作させた後、プログラムが異常になり、更新操作が主データベースに切り替わらない現象が発生しました。
この問題は、最初に読み取り操作を行い、その後書き込み操作を行う場合に発生します。
ログ分析の結果、以下の原因が考えられます:
- プログラムが一定期間実行された後にDubboサービスを呼び出す際に(例: [DubboServerHandler-192.168.1.106:20880-thread-199])、デフォルトで最大200スレッドまでしか処理されません。このため、読み取り操作と書き込み操作が同一スレッド内で行われる可能性があります(読み取り操作のトランザクション伝播設定は「supports」、書き込みは「required」)。この状況下でMethodBeforeAdvice.beforeが先に実行され、DataSourceSwitcher.setSlave()が呼ばれます。その後DynamicDataSource.determineCurrentLookupKeyが呼ばれますが、ここではコンテキストホルダーからデータソースのキーが取得されます。その結果、データソースがスレーブ(読み取り専用)に切り替わります。読み取り操作が完了した後、同じスレッド内で更新操作が行われると、再度DynamicDataSource.determineCurrentLookupKeyが呼び出されず、データソースが引き続きスレーブを指しているためエラーが発生します。
DynamicDataSource.determineCurrentLookupKeyとDataSourceSwitcher.setXXX()メソッドの実行順序が問題の鍵であり、これはトランザクションアドバイスとデータソースの動的な設定に関連しています。
<bean id="baseDataSource" class="org.apache.commons.dbcp2.BasicDataSource">
<property name="driverClassName" value="com.mysql.cj.jdbc.Driver"/>
<property name="initialSize" value="30"/>
<property name="maxTotal" value="75"/>
<property name="maxIdle" value="15"/>
<property name="testOnBorrow" value="true"/>
<property name="testWhileIdle" value="true"/>
<property name="testOnReturn" value="false"/>
<property name="defaultAutoCommit" value="false"/>
</bean>
<bean id="mainDataSource" parent="baseDataSource">
<property name="url" value="jdbc:mysql://192.168.60.45:13306/vote?autoReconnect=true&useSSL=false"/>
<property name="username" value="user"/>
<property name="password" value="pass"/>
</bean>
<bean id="replicaDataSource" parent="baseDataSource">
<property name="url" value="jdbc:mysql://192.168.60.45:23306/vote?autoReconnect=true&useSSL=false"/>
<property name="username" value="user"/>
<property name="password" value="pass"/>
</bean>
<bean id="dataSource" class="com.example.service.database.DynamicSource">
<property name="targetDataSources">
<map key-type="java.lang.String">
<entry key="replica" value-ref="replicaDataSource"/>
<entry key="main" value-ref="mainDataSource"/>
</map>
</property>
<property name="defaultTargetDataSource" ref="mainDataSource"/>
</bean>
<aop:aspectj-autoproxy proxy-target-class="true"/>
- DataSourceSwitcherクラス
public class DataSourceSwitcher {
private static final ThreadLocal<String> context = new ThreadLocal<>();
private static final String REPLICA = "replica";
public static void setDataSource(String dataSource) {
Objects.requireNonNull(dataSource, "データソース名はnullにしてはいけません");
context.set(dataSource);
}
public static void setMain() {
context.remove();
}
public static void setReplica() {
setDataSource(REPLICA);
}
public static String getDataSource() {
return context.get();
}
public static void clearDataSource() {
context.remove();
}
}
- DynamicSourceクラス
public class DynamicSource extends AbstractRoutingDataSource {
@Override
protected Object determineCurrentLookupKey() {
return DataSourceSwitcher.getDataSource();
}
}