Пул соединений Tomcat jdbc - откат отмененной транзакции
Мы настраиваем пул соединений tomcat-jdbc с removeAbandoned=true
, Если соединение прервано, опция работает, но соединение просто закрывается. С Oracle это означает, что текущая транзакция зафиксирована ( см. Этот вопрос). Это не хорошо, потому что незаконченная сделка не должна быть совершена.
Как настроить пул так, чтобы, если соединение было разорвано, текущая транзакция сначала откатывается, а после этого соединение закрывается?
Я старался rollbackOnReturn=true
но пул, кажется, не использует его для заброшенных соединений.
Изменить: мы используем defaultAutoCommit=false
Изменить: один случай, когда это произошло, была отладка интеграционного теста; наша таблица транзакций была усечена из-за такой фиксации
3 ответа
Согласно http://docs.oracle.com/javase/7/docs/api/java/sql/Connection.html:
"Настоятельно рекомендуется, чтобы приложение явно зафиксировало или откатило активную транзакцию до вызова метода close. Если вызывается метод close и существует активная транзакция, результаты определяются реализацией ".
Этот тест, использующий Mysql, а не Oracle, подтверждает этот факт:
import static org.junit.Assert.assertEquals;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import org.junit.Test;
public class DBTest {
public Connection openConnection() throws ClassNotFoundException, SQLException {
Class.forName("com.mysql.jdbc.Driver");
Connection c = DriverManager.getConnection("jdbc:mysql://localhost:3306/test", "root", "password");
c.setAutoCommit(false);
return c;
}
@Test
public void testSO25886466() throws SQLException, ClassNotFoundException {
{
Connection c = openConnection();
PreparedStatement delete = c.prepareStatement("delete from temp");
delete.executeUpdate();
c.commit();
c.close();
}
{
Connection c = openConnection();
PreparedStatement insert = c.prepareStatement("insert into temp values ('a', 'b')");
insert.execute();
//c.commit(); as the op says, DONT commit!!
c.close(); //WITHOUT having closed the statement or committing the transaction!!
}
{
Connection c = openConnection();
PreparedStatement select = c.prepareStatement("select count(*) from temp");
select.execute();
ResultSet rs = select.getResultSet();
while(rs.next()){
assertEquals(0/*i'd expect zero here!*/, rs.getInt(1));
}
rs.close();
select.close();
c.close();
}
}
}
Согласно http://tomcat.apache.org/tomcat-7.0-doc/jdbc-pool.html:
(булево) Флаг, чтобы удалить оставленные соединения, если они превышают removeAbandonedTimeout. Если установлено значение true, соединение считается прерванным и может быть удалено, если оно использовалось дольше, чем removeAbandonedTimeout. Если для этого параметра установлено значение true, можно восстановить соединения БД из приложений, которые не могут закрыть соединение. Смотрите также logAbandoned Значением по умолчанию является false.
Я бы порекомендовал не устанавливать removeAbandoned
так что Oracle закрывает соединение после тайм-аута на стороне сервера, а не Tomcat закрывает его. В этом случае Oracle, вероятно, не будет совершать транзакцию, но вам нужно будет это проверить.
Или вы могли бы увеличить removeAbandonedTimeout
настройки, чтобы ваша программа могла завершиться, и никакие соединения не были прерваны?
Другая проблема, с которой вы столкнулись, заключается в том, что ваше приложение привязано к Oracle, потому что вы полагаетесь на реализацию драйвера, где в спецификации есть дыра. Если вы можете, программируйте в соответствии со спецификациями, чтобы вы могли свободно переносить ваше приложение в другую базу данных, хотя я знаю, что на практике это сложно.
Совершенно другим решением было бы взять пул соединений с открытым исходным кодом и расширить его с помощью перехватчика AOP, который может перехватывать вызовы close
и выяснить, была ли сделка совершена, и если нет, позвонить rollback
на связи. Это довольно сложное решение, хотя...:-)
Хорошо.... Я думаю, что если вы не можете исключить оставленные соединения, у вас есть только 3 варианта:
- Измените метод с Tomcats ConnectionPool, который закрывает оставленное соединение, чтобы сделать откат + закрыть
- Измените метод закрытия вашего соединения, чтобы сделать откат + закрыть
- Позвольте откату БД + закрыть Соединение после тайм-аута (Отключить обработчик Tomcats)
Для варианта 1 вы можете либо отредактировать метод в исходном коде Tomcat, заменить его через HotSwap или Javassist, либо полностью отключить его и написать свой собственный метод, который перебирает все соединения и обнаруживает разорванные и закрывает их.
Для варианта 2 вы можете либо написать свой собственный Wrapper для интерфейса подключения, который заменит вызов close() на rollback+close, и сконфигурировать TomCat для включения соединения в класс Wrapper, либо вы можете использовать HotSwap или Javassist для замены Метод Close в вашем классе соединения во время выполнения.
Для варианта 3 вы полностью отключаете "Целую обработку заброшенных соединений" и настраиваете свою БД на отключение незанятых соединений по истечении определенного времени ожидания. Но у этого будет и недостаток - регулярное отключение соединений в вашем пуле соединений, если они не используются долгое время...
Вы можете просто зарегистрировать JDBCInterceptor для выполнения этой модификации, поэтому вы можете выполнить откат до его закрытия - смотрите здесь: http://tomcat.apache.org/tomcat-7.0-doc/jdbc-pool.html. Откажитесь от вызова, который вызовет разъединение, таким образом, Перехватчик будет уведомлен об этом. Например, вы можете сделать это так:
package test;
import java.sql.SQLException;
import oracle.jdbc.OracleConnection;
import org.apache.tomcat.jdbc.pool.ConnectionPool;
import org.apache.tomcat.jdbc.pool.JdbcInterceptor;
import org.apache.tomcat.jdbc.pool.PooledConnection;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class RollbackInterceptor extends JdbcInterceptor {
/**
* Logger.
*/
private static final Logger LOG = LoggerFactory.getLogger(RollbackInterceptor.class);
/**
* {@inheritDoc}
*/
@Override
public void reset(ConnectionPool parent, PooledConnection con) {
return;
}
/**
* {@inheritDoc}
*/
@Override
public void disconnected(ConnectionPool parent, PooledConnection con, boolean finalizing) {
// if its oracle make sure we rollback here before disconnect just in case a running TX is open
try {
if (con.getConnection().isWrapperFor(OracleConnection.class)) {
if (!con.getConnection().getAutoCommit()) {
LOG.error("Connection {} with Auto-Commit false is going to be closed. Doing an explicit Rollback here!", con);
try {
con.getConnection().rollback();
} catch (SQLException e) {
LOG.error("Failed to rollback connection {} before closing it.", con, e);
}
}
}
} catch (SQLException e) {
LOG.error("Failed to check auto commit of connection {}", con, e);
}
super.disconnected(parent, con, finalizing);
}
}