MySQL Connector-Jでの再接続

JDBCのDriverManager#getConnectionによるデータベースへの接続は結構時間がかかるので、短時間に何度もDBにアクセスする場合は、Connectionオブジェクトを保持したい。
しかし、MySQLのデフォルト設定の動作では、DBへの接続は8時間使われないと切られてしまう。常駐型のservletなどの連続運転のアプリでは、途中でこのコネクションが切れることがあり得る。この時間を変更することもできるらしいが、いつか切れるので、それで解決にはならない。従って、切れれば再接続する必要がある。

JDBCアプリ側で再接続する場合、DBへの接続が有効か無効かを知る術が問題になる。接続が切れた後のクエリ送信時の例外はSocketクラスのIOExceptionであり、ドライバ(MySQL Connector-J)にてcatchされるので、JDBCアプリではcatchできない。JDKのAPI仕様書を見るとConnection#isClosedというメソッドがあるが、これはJDBCアプリからcloseしたかどうかを返すものであり、MySQL側で接続を切られてもclosedにはならない。

そもそもservletでは、こういう時の定石はconnection poolingらしい。ServletエンジンでDBへの接続を保持して、servletからの要求に応じて接続を貸し出す仕組みで、暇な時に再接続もしてくれる、J2EEに標準装備されている一般的なものである。勿論Tomcatでも使える。
しかし、Tomcatのマニュアルの"JNDI Datasource HOW-TO"のページを見ると、設定方法がかなり複雑である。もっと簡単な解決方法は無いかと思ってConnector-Jのマニュアルを眺めていると、autoReconnectという設定項目が見つかった。DBとの接続が切れていればドライバが自動的に再接続するという設定だ。Obsoleteなproperty(将来削除される設定項目)であり、SQLExceptionを処理できない場合以外は非推奨と書かれているが、今回はとりあえず目的が達成できれば良かったので、

conn = DriverManager.getConnection("jdbc:mysql:/
/localhost/xxxx?user=xxxx&password=xxxx&autoReconnect=true");

という風にautoReconnectの設定を追加した。

実にシンプルかつスマートに解決できたと思っていたが、Tomcatのログを見て、時々エラーが発生していることに気付いた。調べてみると、どうやらautoReconnectの設定をしていても、DBとの接続が切れると、1回はクエリ送信が例外(SQLException)で終わり、それによって再接続され、その次のクエリから接続が有効になるようだ。
そこで、1回はSQLExceptionに対して再試行する、ということも考えたが、そもそもSQLExceptionをcatchして再接続するのならautoReconnectの機能を使う意味が無い、connection poolingの方がスマートだろう、と考えてやめた。

という訳で、connection poolingを試すべく、TomcatのHowToの通りに設定を書き、テストプログラムをコンパイルすると、javax.sql.*が無いというエラーになった。一瞬で確信した通り、プールされたconnectionを取り出すためのDataSourceクラスが1.3のJVMには存在しないのだった(javax.sqlは1.4以降の仕様らしい)。

途方に暮れてさらにConnector-Jのマニュアルを読み進めると、"Common Problems and Solutions"の所に答えが書いてあった。Connection poolingするか、autoReconnectにした上でSQLExceptionをcatchし、MySQLのエラーコードを調べて、回数の上限を決めて再施行するか、のどちらかをする必要があるそうだ。SQLException#getSQLStateでMySQLのエラーコードがわかり、MySQLのinfoによると接続系のエラーが起こるとSQLStateが"08S01"になるらしいので、SQLStateが"08S01"の場合に最大2回クエリを再施行することで解決した。

int retryCount = 3;
boolean transactionCompleted = false;
do{
try{
Statement st = conn.createStatement();
ResultSet rs = st.executeQuery("xxxx");
transactionCompleted = true;
}
catch(SQLException e){
String sqlState = e.getSQLState();
if (sqlState.equals("08S01")){
retryCount--;
}else{
retryCount = 0;
}
}
finally{
...
}
} while(!transactionCompleted && retryCount > 0);


(5/25追記)
サーバーサイドのJavaでconnection poolingを使う場合、プールするコネクションの数を決めるために、同じクラスのservletが並列で動くのかどうかが気になる。Tomcatに同時に複数のアクセスを行ってもApacheのようにプロセスが増えないからといって、servletがシングルスレッドとは限らない。Java VM内部のJavaスレッドを使ってマルチスレッドで動くのかも知れない。
しかし、Tomcat4のtomcat-docs/class-loader-howto.htmlに

WebappX - A class loader is created for each web application that is deployed in a single Tomcat 4 instance.

と書いてあるので、明示的にマルチスレッドにしない限りは、1つのservletのインスタンスは1つだけで動くようだ。
そうであれば、あるDBへのコネクションを1つのservletだけが使うなら、J2EEのconnection poolingを使ってJava VMにプールさせるメリットはあまり無いような気がする。