Tomcat JDBC连接池 - 回滚被遗弃的事务

3
我们使用removeAbandoned=true配置tomcat-jdbc连接池。如果连接被弃用,该选项确实生效,但连接只是关闭。对于Oracle来说,这意味着当前事务已提交(请参见此问题)。这并不好,因为未完成的事务不应该被提交。
如何配置池,以便如果连接被弃用,那么首先回滚当前事务,然后关闭连接?
我尝试了rollbackOnReturn=true,但是池似乎没有将其用于弃用的连接。 编辑:我们使用defaultAutoCommit=false 编辑:出现这种情况的其中一种情况是集成测试的调试;由于此类提交,我们的事务表被截断。

我首先会问,为什么你把废弃连接当作正常情况?Tomcat 中的这个选项是用于故障排除的。正确管理你的连接,例如在完成后关闭连接,适当设置池超时等。其次,任何数据库都不太可能在没有显式或隐式提交的情况下提交某些内容。看起来你漏掉了什么。 - kaqqao
这显然不是一个正常的情况。但是当发生这种情况时,我希望它_不要_提交。一种出现这种情况的情况是集成测试的调试,我们的表被截断了。至于您提到的数据库问题,Oracle在连接关闭时会进行提交,这是实现细节,请参见https://dev59.com/H3VC5IYBdhLWcg3wqzLV#218637。 - Aleksey Otrubennikov
在使用 defaultAutoCommit=false 后,问题仍未得到解决...... - Deepak Bhatia
3个回答

2
根据http://docs.oracle.com/javase/7/docs/api/java/sql/Connection.html#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
(boolean) Flag to remove abandoned connections if they exceed the removeAbandonedTimeout. If set to true a connection is considered abandoned and eligible for removal 如果它的使用时间超过了removeAbandonedTimeout,则将其视为已被遗弃并可进行删除。如果设置为true,可以从未关闭连接的应用程序中恢复db连接。另请参阅logAbandoned,默认值为false。
我建议不要设置removeAbandoned,而是让Oracle在服务器端超时后关闭连接,而不是Tomcat关闭连接。在这种情况下,Oracle可能不会提交事务,但您需要进行测试。
或者,您可以增加removeAbandonedTimeout设置的值,以便您的程序可以完成,没有连接被遗弃。
你面临的另一个问题是,你的应用程序已经与Oracle绑定在一起,因为你依赖于驱动程序实现,而规范中存在漏洞。如果可能的话,应该按照规范进行编程,这样你就可以自由地将应用程序迁移到不同的数据库,尽管我知道这在实践中很难。
完全不同的解决方案是,使用开源连接池,并使用AOP拦截器扩展它,以便可以拦截对close的调用,并确定是否已提交事务,如果没有,则在连接上调用rollback。不过这是一个相当复杂的解决方案... :-)

0
你可以注册一个JDBC拦截器来进行这个修改,这样在关闭之前就可以回滚 - 可以看这里:http://tomcat.apache.org/tomcat-7.0-doc/jdbc-pool.html#JDBC_interceptors。Abandon会调用release,而release会调用disconnect,因此拦截器将被通知到这一点。 例如,你可以像这样做:
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);
}

}

0

好的....我认为如果您不能排除废弃的连接,您只有3个选择:

  1. 更改Tomcat连接池的方法,将废弃的连接关闭为回滚+关闭
  2. 更改连接的关闭方法,使其执行回滚+关闭操作
  3. 让数据库在超时后执行回滚+关闭连接(禁用Tomcat处理程序)

对于选项1,您可以通过编辑Tomcat源代码中的方法、使用HotSwapJavassist替换方法,或者完全禁用它,并编写自己的方法,循环遍历所有连接并检测废弃的连接并关闭它们。

对于选项2,您可以编写自己的连接接口包装器,将close()调用替换为rollback+close,并配置TomCat将连接包装到您的包装器类中,或者您可以使用HotSwapJavassist在运行时替换连接类中的Close方法。

对于选项3,您可以完全禁用废弃连接的整个处理过程,并配置您的数据库在一定时间后关闭空闲连接。但这将有一个缺点,即当连接长时间未被使用时,它也会定期杀死连接池中的连接...


这些建议和我的有什么不同?我在回答中表述得不够清楚,但修改Tomcat或连接池的代码是一个非常糟糕的想法。你将永远无法迁移你的应用程序。 - Ant Kutschera
@AntKutschera 我认为一个很大的区别在于可以热替换代码,这样你的应用程序就完全可移植和可更新,因为你可以在运行时插入装饰器!我没有在你的回答中看到任何关于在运行时更改类代码的参考。此外,您也没有提到只需禁用Tomcat超时并编写自己的超时函数来处理废弃连接的可能性,这是相当简单的逻辑。 - Falco

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接