HikariCP 与 SQLTimeoutException

最近碰到一个问题:项目的数据库连接池使用的是 HkiariCP,对每个 SQL 语句的执行超时时间设置为 30秒,结果有个 SQL 超时了,抛出异常 SQLTimeoutException,应用层回滚事务时抛出了连接已关闭的异常。但事实上事务却提交了。

写了个简单的代码来模拟生产场景:
在 Spring 的声明式事务内,有一个 insert 操作,然后是一个 update 操作,在数据库客户端执行 select for update 把要更新的行锁住,这样 update 操作就会超时。

多次调试发现 HikariCP 在碰到 SQL 异常时有个检查机制,满足特定条件的异常会直接关闭底层数据库连接,Spring 拿到的是连接的代理,由于连接已关闭,自然没法回滚事务,会碰到连接已关闭异常。

HikariProxyPreparedStatement

public int executeUpdate() throws SQLException {
    try {
        return super.executeUpdate();
    } catch (SQLException var2) {
        throw this.checkException(var2);
    }
}

final SQLException checkException(SQLException e)
{
  return connection.checkException(e);
}

ProxyConnection

final SQLException checkException(SQLException sqle)
{
  boolean evict = false;
  SQLException nse = sqle;
  final SQLExceptionOverride exceptionOverride = poolEntry.getPoolBase().exceptionOverride;
  for (int depth = 0; delegate != ClosedConnection.CLOSED_CONNECTION && nse != null && depth < 10; depth++) {
     final String sqlState = nse.getSQLState();
     if (sqlState != null && sqlState.startsWith("08")
         || nse instanceof SQLTimeoutException
         || ERROR_STATES.contains(sqlState)
         || ERROR_CODES.contains(nse.getErrorCode())) {

        if (exceptionOverride != null && exceptionOverride.adjudicate(nse) == DO_NOT_EVICT) {
           break;
        }

        // broken connection
        evict = true;
        break;
     }
     else {
        nse = nse.getNextException();
     }
  }

  if (evict) {
     SQLException exception = (nse != null) ? nse : sqle;
     LOGGER.warn("{} - Connection {} marked as broken because of SQLSTATE({}), ErrorCode({})",
        poolEntry.getPoolName(), delegate, exception.getSQLState(), exception.getErrorCode(), exception);
     leakTask.cancel();
     poolEntry.evict("(connection is broken)");  // 这里由异步线程来关闭底层的物理连接
     delegate = ClosedConnection.CLOSED_CONNECTION; // 连接代理被替换为已关闭的,Spring 自然无法回滚事务
  }

  return sqle;
}

那么还有个问题,为什么连接关闭会提交事务,得看看 JDBC Connection 的文档,有如下的特别说明:连接关闭时,事务是提交还是回滚取决于时具体的实现,毕竟 JDBC 只是一个规范、上层 API 。
https://docs.oracle.com/javase/8/docs/api/java/sql/Connection.html#close–

It is strongly recommended that an application explicitly commits or rolls back an active transaction prior to calling the 
close method.  If the 
close method is called and there is an active transaction, the results are implementation-defined. 

那么怎么由上层应用来决定是提交还是回滚事务呢,从 checkException 方法可以发现,如果配置一个 SQLExceptionOverride 实现类且其方法 adjudicate(nse) 返回 DO_NOT_EVICT 就不会直接关闭底层连接。

我们可以定制一个 SQLExceptionOverride 实现如下,然后通过配置属性 exceptionOverrideClassName 来指定。

public class MyExceptionOverride implements SQLExceptionOverride {
    public Override adjudicate(SQLException sqlException) {
        // HikariCP 碰到 SQLException 不直接关闭底层连接,由上层应用来决定。
        return Override.CONTINUE_EVICT;
    }
}

欢迎关注我的微信公众号: coderbee笔记

发表回复

您的电子邮箱地址不会被公开。 必填项已用*标注

此站点使用Akismet来减少垃圾评论。了解我们如何处理您的评论数据