戯言

つらつらと気づいたことを書いていきます。人狼とか。

スポンサーサイト


上記の広告は1ヶ月以上更新のないブログに表示されています。
新しい記事を書く事で広告が消せます。

コネクションプール利用時のConnection.setAutoCommit(false)のワナ


Connection.setAutoCommit() でちょっとハマりました。

とあるトランザクション制御が必要ない単一のクエリで、
デフォルトの true が効かずに、autoCommitが意図せずに false に
なっている場合があって、こちらの期待通りの動きをしない可能性がありました。


どういうことかというと・・・、

トランザクション制御が必要なクエリでは、
autoCommit を false に設定していますが、クエリ実行後に、このトランザクションは終了して、
Connection も close するので、false になるのはこのトランザクションのみで、
他のトランザクションへの影響はないと思っていました。

ですが、一旦 false に設定すると、別のクエリで、Connection を取得した
場合に、false の設定のままとなっていて、
マルチスレッド環境で、複数のクエリが動いた場合に、
期待とおりでない動作をするケースがあったようです。

ざっくり書くと、トランザクション制御が必要なクエリを実行するメソッドは、こんな感じで書いていました。

public void トランザクション制御が必要となるクエリの実行(){
	Connection conn = null;
	try {
		conn = createConnection();
		conn.setAutoCommit(false);
			:
			:
			// トランザクション制御するクエリ実行
			:
			:
		conn.commit();
	} catch (SQLException e) {
		conn.rollback();
	} finally {
		conn.close();
	}
}
public void トランザクション制御が必要ないクエリの実行(){
	Connection conn = null;
	try {
		conn = createConnection();
			:
			:
		// トランザクション制御しないクエリ実行
			:
	} finally {
		conn.close();
	}
}

createConnection() というコネクション払出し用のメソッドを作っていて、
その中身はこんな感じです。

public Connection createConnection() throws SQLException {
	try{
		InitialContext ic = new InitialContext();
		DataSource ds = (DataSource)ic.lookup("java:comp/env/jdbc/・・・・");
		Connection conn = ds.getConnection();
		
		return conn;
	}
	catch (NamingException ex){
		// 例外処理(略)
	}
}
「トランザクション制御が必要となるクエリの実行()」が呼ばれ、終了した時点で、
autoCommit は false のままとなってます。

その後、別のトランザクションで、「トランザクション制御が必要ないクエリの実行()」を
呼出した場合に、新たにcreateConnection() を呼び出すのですが、
返ってくる Connection オブジェクトの autoCommit は false になっていました。

単一クエリの実行では、(自動コミットされると思っているので)commit()を
明示的に呼び出していないので、close()が呼ばれるまでの間、
コミットされていない状態になってしまっていました。
createConnection() で return する前に、true 設定するようにしておきました。



では、なぜ、コネクションを切ってるのに、次のコネクションに設定が残ってるのか。
よくよく考えたら、コネクションプールを使ってるからでした。
予めコネクションを作ってプールしといて、必要なときに払いだしているのだから、
当たり前といえば当たり前。

一応、こんな感じのソースで確認しました。

コネクションプールを使う場合
public Connection createConnection() throws SQLException {
	try{
		// DBCPを利用
		InitialContext ic = new InitialContext();
		DataSource ds = (DataSource)ic.lookup("java:comp/env/jdbc/・・・・");
		Connection conn = ds.getConnection();
		
		// 一旦クエリ実行時に false にセットすると、次からは払いだした時点でfalseになる。
		System.out.println("autoCommit=" + conn.getAutoCommit());

		return conn;
	}
	catch (NamingException ex){
		// 例外処理(略)
	}
}
コネクションプールを使わない場合
public Connection createConnection() throws SQLException {
	try{
		// CPを利用しない
		Class.forName("org.apache.derby.jdbc.EmbeddedDriver");
		Connection conn = DriverManager.getConnection("jdbc:derby:/.../....",name, pass);
		
		// 常に true になることを確認。
		System.out.println("autoCommit=" + conn.getAutoCommit());

		return conn;
	}
	catch (ClassNotFoundException ex){
		// 例外処理(略)
	}
}



スポンサーサイト

java.sql.SQLTransactionRollbackException: 要求時間内にロックを獲得できませんでした と デッドロックが原因でロックを獲得できませんでした


プログラムは全く変えてないのに、OS,tomcatのバージョンをあげたら
突然デッドロックが発生するようになっちゃいました。

発生している例外はこれら。
java.sql.SQLTransactionRollbackException: 要求時間内にロックを獲得できませんでした。
java.sql.SQLTransactionRollbackException: デッドロックが原因でロックを獲得できませんでした。

実害としては、ロック解放待ちで、クエリの実行が待ち状態となり、
ブラウザが応答しないように見えるのでサーバが重くなったように感じます。

また、ロックを取得できなかった場合に発生する
java.sql.SQLTransactionRollbackException の例外発生時の処理が
適切でなかったために、
村番号が被ったりするなどの問題が発生しました。

発生の原因を探すのに大変苦労しましたが、
根本原因は凡ミスで、PreparedStatementのclose処理漏れ。

今まで問題が顕在化しなかった理由がよくわかりません。
tomcat のバージョンが変わったので、DBCP の処理に変更でもあったからなのだろうか・・・。

JDBCドライバは変更してないしなあ。
今まではGCで救われていたとか。

とにもかくにもプログラムが問題なので、修正しました。

問題があったソースはこんな感じでした。

selectした結果が1件ならupdateして、0件ならinsertするような処理です。

Connection conn = null;
PreparedStatement state = null;
ResultSet rs = null;

try {
	conn = createConnection();	// DBCPからConnectionを取得するためのメソッド
	conn.setAutoCommit(false);

	state = conn.prepareStatement("SELECT ・・・・");
	state.setString(1, HN);
	state.setString(2, TRIP);

	rs = state.executeQuery();
	rs.next();
	int count = rs.getInt("COUNT");

	// INSERT
	if (count == 0){
		query = "INSERT ・・・";
	}
	// UPDATE
	else if (count == 1){
		query = "UPDATE ・・・";
	} else {
		// 略	
	}

	state = conn.prepareStatement(query);
	state.execute();
	conn.commit();
		
} catch (SQLException ex) {
	ex.printStackTrace();
	try{
		if (conn != null) conn.rollback();
	}catch(SQLException e){
		e.printStackTrace();
	}

} finally {
	try {
		if (rs != null) rs.close();
	} catch (Exception ex) {
		ex.printStackTrace();
	}
	try {
		if (state != null) state.close();
	} catch (Exception ex) {
		ex.printStackTrace();
	}
	try {
		if (conn != null)conn.close();
	} catch (Exception ex) {
		ex.printStackTrace();
	}
}

一見finally句でcloseできているように見えます。

でもダメ。

9行目のPreparedStatementを閉じることなく
28行目で上書きしているので、
9行目のPreparedStatementがcloseされずに残ってしまうのです。

気づいてませんでした。

Statementなら、この書き方でもOKみたいだけど、
PreparedStatementの場合は、使いまわす前にcloseしないと、
オブジェクトが残ってしまうようです。

対応としては、16行目に、state.close()を追加するだけです。
念のためにrs.close()も追加しときました。

同じような問題で、
forループ中でprepareStatementを更新していて、
closeしていないパターンもありました。

Connection conn = null;
PreparedStatement state = null;

try {
	conn = createConnection();
	conn.setAutoCommit(false);

	for (数回の繰り返し){
		query = "INSERT ・・・"
		// ここでqueryをちょっと変更

		state = conn.prepareStatement(query);
		state.execute();
	}
	conn.commit();

} catch (SQLException ex) {
	ex.printStackTrace();
	try{
		if (conn != null) conn.rollback();
	}catch(SQLException e){
		e.printStackTrace();
	}

} finally {
	try {
		if (rs != null) rs.close();
	} catch (Exception ex) {
		ex.printStackTrace();
	}
	try {
		if (state != null) state.close();
	} catch (Exception ex) {
		ex.printStackTrace();
	}
	try {
		if (conn != null)conn.close();
	} catch (Exception ex) {
		ex.printStackTrace();
	}
}

finally句でConnection、ResultSet、Statementを全てcloseしていて、
close漏れはないと思い込んでいたので、
close漏れをなかなか発見できませんでした。

これを見つけるまでに時間がかかってしまったので、
発見前に別の対策をいろいろと打ったんですが、
close漏れが根本の問題でした。



発見前に打った対策にも、一応触れておくと。。。

ロックでトラぶっていたので、なるべくDBへのアクセスを減らして
ロックがかかるかもしれない処理を抑制しようということで、
一例としては、現在の村番号で最大の番号を取得する処理があるのですが、
あまり更新もないのに参照はいろんなところからされていて、
その参照のたびにデータベースにアクセスに行っていたので、
この無駄を省くために、
番号が変更になったとき(村が建ったとき)に村番号をstaticな変数に格納しておき、
最大番号を知りたい場合はその変数を見に行くようにしました。

など


また、DB関連の例外処理も不味かったです。
あまり例外が発生しなそうなところは、
catch時のルーチン内でとりあえずnullを返すような乱暴な作りにしちゃってましたが、
ユーザ向けメッセージを表示するなり、
throwして呼び出し元に返したりなど、
きちんと書き直しました。

手を抜くとやばいですね。






野田ちゃんと、結婚しました。


野田ちゃんが困ってたので、また助けてきました。

今までのPOH1、2、3Liteと比較すると、今回のPOH4Liteはかなり難易度が下がってます。
一応、100点もらったけど、まだまだ先があるのではと疑ってしまいます。

今までと違って、1問解いて終わりとなるのではなく、
1問解くと先に進める形式になってるし、あやしい…。

https://paiza.jp/poh/enkoi-ending/e9e73681



でも、これで終わりっぽいですね。




上記広告は1ヶ月以上更新のないブログに表示されています。新しい記事を書くことで広告を消せます。