使用休眠从断开的数据库连接中恢复

Recovering from broken database connection with hibernate

我无法从断开的数据库连接中恢复。我的测试用例使用以下库。
guice-坚持 4.0
休眠核心 4.3.1
HikariCP-java6-2.3.9(连接池)。

我的测试在循环中运行一个简单的读取,每次读取之间有一个小的休眠。

@com.google.inject.persist.Transactional
protected void simpleRead() {
  dao = new MyDao(....)
  dao.findBy(...)
}

Dao 是这样获取它的 entityManager

@Inject
protected com.google.inject.Provider<EntityManager> entityManager;

guice模块是这样绑定的

@Override
public void configure() {
  install(new JpaPersistModule("test"));
  bind(JPAInitializer.class).asEagerSingleton();
}

我的 persistence.xml 将交易类型定义为 "RESOURCE-LOCAL"。

我有一些 hikari 设置,但我不确定这些是否相关

 <property name="hibernate.connection.provider_class" value="com.zaxxer.hikari.hibernate.HikariConnectionProvider" /> 
 <property name="hibernate.hikari.maxLifetime" value="1800000" />
 <property name="hibernate.hikari.connectionTestQuery" value="SELECT 1" />
 <property name="hibernate.hikari.leakDetectionThreshold" value="300000" />

当测试循环读取时,我通过从任务管理器中停止服务来中断 SQLServer 连接。如果循环不在 simplRead() 方法中,那么它会优雅地处理下一次尝试读取时的错误,并在服务再次启动时重新连接。 但是,如果服务停止时它在 simpleRead() 方法中,它将失败并且永远不会恢复。第一次失败报如下

<br> org.hibernate.TransactionException:JDBC 开始交易失败: javax.persistence.PersistenceException:org.hibernate.TransactionException:JDBC 开始交易失败: 在 org.hibernate.jpa.spi.AbstractEntityManagerImpl.convert(AbstractEntityManagerImpl.java:1763) 在 org.hibernate.jpa.spi.AbstractEntityManagerImpl.convert(AbstractEntityManagerImpl.java:1677) 在 org.hibernate.jpa.spi.AbstractEntityManagerImpl.throwPersistenceException(AbstractEntityManagerImpl.java:1771) 在 org.hibernate.jpa.internal.TransactionImpl.begin(TransactionImpl.java:64) 在 com.google.inject.persist.jpa.JpaLocalTxnInterceptor.invoke(JpaLocalTxnInterceptor.java:66) 在 net.impro.portal.server.model.dao.TestConnectionStuff.testConnectionBroken(TestConnectionStuff.java:40) 在 sun.reflect.NativeMethodAccessorImpl.invoke0(本机方法) 在 sun.reflect.NativeMethodAccessorImpl.invoke(未知来源) 在 sun.reflect.DelegatingMethodAccessorImpl.invoke(来源不明) 在 java.lang.reflect.Method.invoke(来源不明) 在 org.testng.internal.MethodInvocationHelper.invokeMethod(MethodInvocationHelper.java:80) 在 org.testng.internal.Invoker.invokeMethod(Invoker.java:714) 在 org.testng.internal.Invoker.invokeTestMethod(Invoker.java:901) 在 org.testng.internal.Invoker.invokeTestMethods(Invoker.java:1231) 在 org.testng.internal.TestMethodWorker.invokeTestMethods(TestMethodWorker.java:127) 在 org.testng.internal.TestMethodWorker.run(TestMethodWorker.java:111) 在 org.testng.TestRunner.privateRun(TestRunner.java:767) 在 org.testng.TestRunner.run(TestRunner.java:617) 在 org.testng.SuiteRunner.runTest(SuiteRunner.java:334) 在 org.testng.SuiteRunner.runSequentially(SuiteRunner.java:329) 在 org.testng.SuiteRunner.privateRun(SuiteRunner.java:291) 在 org.testng.SuiteRunner.run(SuiteRunner.java:240) 在 org.testng.SuiteRunnerWorker.runSuite(SuiteRunnerWorker.java:52) 在 org.testng.SuiteRunnerWorker.run(SuiteRunnerWorker.java:86) 在 org.testng.TestNG.runSuitesSequentially(TestNG.java:1197) 在 org.testng.TestNG.runSuitesLocally(TestNG.java:1122) 在 org.testng.TestNG.run(TestNG.java:1030) 在 org.testng.remote.RemoteTestNG.run(远程TestNG.java:111) 在 org.testng.remote.RemoteTestNG.initAndRun(远程TestNG.java:204) 在 org.testng.remote.RemoteTestNG.main(远程TestNG.java:175) 原因:org.hibernate.TransactionException:JDBC 开始交易失败: 在 org.hibernate.engine.transaction.internal.jdbc.JdbcTransaction.doBegin(JdbcTransaction.java:76) 在 org.hibernate.engine.transaction.spi.AbstractTransactionImpl.begin(摘要TransactionImpl.java:162) 在 org.hibernate.internal.SessionImpl.beginTransaction(SessionImpl.java:1431) 在 org.hibernate.jpa.internal.TransactionImpl.begin(TransactionImpl.java:61) ... 还有 26 个 由以下原因引起:java.sql.SQLException:I/O 错误:对等方重置连接:套接字写入错误 在 net.sourceforge.jtds.jdbc.TdsCore.executeSQL(TdsCore.java:1059) 在 net.sourceforge.jtds.jdbc.TdsCore.submitSQL(TdsCore.java:905) 在 net.sourceforge.jtds.jdbc.JtdsConnection.setAutoCommit(JtdsConnection.java:2291) 在 com.zaxxer.hikari.proxy.ConnectionProxy.setAutoCommit(ConnectionProxy.java:334) 在 com.zaxxer.hikari.proxy.ConnectionJavassistProxy.setAutoCommit(ConnectionJavassistProxy.java) 在 org.hibernate.engine.transaction.internal.jdbc.JdbcTransaction.doBegin(JdbcTransaction.java:72) ... 29 更多 Caused by: java.net.SocketException: Connection reset by peer: 套接字写入错误 在 java.net.SocketOutputStream.socketWrite0(本机方法) 在 java.net.SocketOutputStream.socketWrite(未知来源) 在 java.net.SocketOutputStream.write(未知来源) 在 java.io.DataOutputStream.write(来源不明) 在 net.sourceforge.jtds.jdbc.SharedSocket.sendNetPacket(SharedSocket.java:717) 在 net.sourceforge.jtds.jdbc.RequestStream.putPacket(RequestStream.java:570) 在 net.sourceforge.jtds.jdbc.RequestStream.flush(RequestStream.java:518) 在 net.sourceforge.jtds.jdbc.TdsCore.executeSQL(TdsCore.java:1046) ... 34 更多 ...

并且因为现在没有正确清理,后续请求会像这样失败

org.hibernate.TransactionException: Already have an associated managed connection

我发现很难确定是哪一层导致了这个问题。根据我的阅读,连接池应该测试断开的连接并清理,但我怀疑它的 guice-persist 永远具有与其线程本地对象关联的断开的 EntityManager 引用。当然,这也可能是我使用它们的方式。

编辑

经过进一步调查,我注意到如果我在每次测试读取之间等待 > 1000 毫秒,那么代码会按预期运行。我得到下面的StackTrace,然后在我恢复SQLServer服务后,它愉快地重连了。任何小于 1000 毫秒的时间,我都会收到上面列出的原始错误 post。似乎休眠不以相同的方式处理 IO/Error 并且连接永远处于断开状态。

javax.persistence.QueryTimeoutException: Could not open connection
    at org.hibernate.jpa.spi.AbstractEntityManagerImpl.convert(AbstractEntityManagerImpl.java:1725)
    at org.hibernate.jpa.spi.AbstractEntityManagerImpl.convert(AbstractEntityManagerImpl.java:1677)
    at org.hibernate.jpa.spi.AbstractEntityManagerImpl.throwPersistenceException(AbstractEntityManagerImpl.java:1771)
    at org.hibernate.jpa.internal.TransactionImpl.begin(TransactionImpl.java:64)
    at com.google.inject.persist.jpa.JpaLocalTxnInterceptor.invoke(JpaLocalTxnInterceptor.java:66)
    at net.impro.portal.server.model.dao.TestConnectionStuff.testConnectionBroken(TestConnectionStuff.java:40)
    at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at sun.reflect.NativeMethodAccessorImpl.invoke(Unknown Source)
    at sun.reflect.DelegatingMethodAccessorImpl.invoke(Unknown Source)
    at java.lang.reflect.Method.invoke(Unknown Source)
    at org.testng.internal.MethodInvocationHelper.invokeMethod(MethodInvocationHelper.java:80)
    at org.testng.internal.Invoker.invokeMethod(Invoker.java:714)
    at org.testng.internal.Invoker.invokeTestMethod(Invoker.java:901)
    at org.testng.internal.Invoker.invokeTestMethods(Invoker.java:1231)
    at org.testng.internal.TestMethodWorker.invokeTestMethods(TestMethodWorker.java:127)
    at org.testng.internal.TestMethodWorker.run(TestMethodWorker.java:111)
    at org.testng.TestRunner.privateRun(TestRunner.java:767)
    at org.testng.TestRunner.run(TestRunner.java:617)
    at org.testng.SuiteRunner.runTest(SuiteRunner.java:334)
    at org.testng.SuiteRunner.runSequentially(SuiteRunner.java:329)
    at org.testng.SuiteRunner.privateRun(SuiteRunner.java:291)
    at org.testng.SuiteRunner.run(SuiteRunner.java:240)
    at org.testng.SuiteRunnerWorker.runSuite(SuiteRunnerWorker.java:52)
    at org.testng.SuiteRunnerWorker.run(SuiteRunnerWorker.java:86)
    at org.testng.TestNG.runSuitesSequentially(TestNG.java:1197)
    at org.testng.TestNG.runSuitesLocally(TestNG.java:1122)
    at org.testng.TestNG.run(TestNG.java:1030)
    at org.testng.remote.RemoteTestNG.run(RemoteTestNG.java:111)
    at org.testng.remote.RemoteTestNG.initAndRun(RemoteTestNG.java:204)
    at org.testng.remote.RemoteTestNG.main(RemoteTestNG.java:175)
Caused by: org.hibernate.QueryTimeoutException: Could not open connection
    at org.hibernate.exception.internal.SQLExceptionTypeDelegate.convert(SQLExceptionTypeDelegate.java:83)
    at org.hibernate.exception.internal.StandardSQLExceptionConverter.convert(StandardSQLExceptionConverter.java:49)
    at org.hibernate.engine.jdbc.spi.SqlExceptionHelper.convert(SqlExceptionHelper.java:126)
    at org.hibernate.engine.jdbc.spi.SqlExceptionHelper.convert(SqlExceptionHelper.java:112)
    at org.hibernate.engine.jdbc.internal.LogicalConnectionImpl.obtainConnection(LogicalConnectionImpl.java:235)
    at org.hibernate.engine.jdbc.internal.LogicalConnectionImpl.getConnection(LogicalConnectionImpl.java:171)
    at org.hibernate.engine.transaction.internal.jdbc.JdbcTransaction.doBegin(JdbcTransaction.java:67)
    at org.hibernate.engine.transaction.spi.AbstractTransactionImpl.begin(AbstractTransactionImpl.java:162)
    at org.hibernate.internal.SessionImpl.beginTransaction(SessionImpl.java:1471)
    at org.hibernate.jpa.internal.TransactionImpl.begin(TransactionImpl.java:61)
    ... 26 more
Caused by: java.sql.SQLTimeoutException: Timeout after 30004ms of waiting for a connection.
    at com.zaxxer.hikari.pool.BaseHikariPool.getConnection(BaseHikariPool.java:233)
    at com.zaxxer.hikari.pool.BaseHikariPool.getConnection(BaseHikariPool.java:183)
    at com.zaxxer.hikari.HikariDataSource.getConnection(HikariDataSource.java:93)
    at com.zaxxer.hikari.hibernate.HikariConnectionProvider.getConnection(HikariConnectionProvider.java:101)
    at org.hibernate.internal.AbstractSessionImpl$NonContextualJdbcConnectionAccess.obtainConnection(AbstractSessionImpl.java:380)
    at org.hibernate.engine.jdbc.internal.LogicalConnectionImpl.obtainConnection(LogicalConnectionImpl.java:228)
    ... 31 more
Caused by: java.sql.SQLException: Network error IOException: Connection refused: connect
    at net.sourceforge.jtds.jdbc.JtdsConnection.<init>(JtdsConnection.java:436)
    at net.sourceforge.jtds.jdbc.Driver.connect(Driver.java:184)
    at com.zaxxer.hikari.util.DriverDataSource.getConnection(DriverDataSource.java:95)
    at com.zaxxer.hikari.util.DriverDataSource.getConnection(DriverDataSource.java:101)
    at com.zaxxer.hikari.pool.BaseHikariPool.addConnection(BaseHikariPool.java:444)
    at com.zaxxer.hikari.pool.BaseHikariPool.run(BaseHikariPool.java:419)
    at java.util.concurrent.Executors$RunnableAdapter.call(Unknown Source)
    at java.util.concurrent.FutureTask.run(Unknown Source)
    at java.util.concurrent.ThreadPoolExecutor.runWorker(Unknown Source)
    at java.util.concurrent.ThreadPoolExecutor$Worker.run(Unknown Source)
    at java.lang.Thread.run(Unknown Source)
Caused by: java.net.ConnectException: Connection refused: connect
    at java.net.DualStackPlainSocketImpl.waitForConnect(Native Method)
    at java.net.DualStackPlainSocketImpl.socketConnect(Unknown Source)
    at java.net.AbstractPlainSocketImpl.doConnect(Unknown Source)
    at java.net.AbstractPlainSocketImpl.connectToAddress(Unknown Source)
    at java.net.AbstractPlainSocketImpl.connect(Unknown Source)
    at java.net.PlainSocketImpl.connect(Unknown Source)
    at java.net.SocksSocketImpl.connect(Unknown Source)
    at java.net.Socket.connect(Unknown Source)
    at net.sourceforge.jtds.jdbc.SharedSocket.createSocketForJDBC3(SharedSocket.java:288)
    at net.sourceforge.jtds.jdbc.SharedSocket.<init>(SharedSocket.java:251)
    at net.sourceforge.jtds.jdbc.JtdsConnection.<init>(JtdsConnection.java:331)
    ... 10 more

您使用的是哪个版本的 jTDS?看起来该版本的驱动程序没有用 SQLException 包装 SocketException,其中包含以“08”开头的 SQLState 值(表示断开连接)。

HikariCP checks for these SQLStates and flags the connection for eviction. That connection should be closed 再也没有回来。

更新版本的 jTDS 驱动程序 appears 正确处理它,如果我正确阅读他们的代码。

更新:这当然假设 Hibernate 在异常之后仍然关闭连接(就像在 finally 块中一样)。

更新 2:我错过了第二个异常部分,对我来说这看起来像是一个 Hibernate 问题……没有从第一个异常中正确恢复。如果您为 com.zaxxer.hikari 包启用日志记录,您应该会看到有关连接被逐出的警告。看到了吗?

更新3: 我查看了 jTDS 1.3.0,它正在做正确的事情。您 can see here that it will generate a SQLException with SQLState="08S01", which will be checked 被 HikariCP 和连接驱逐了。

现在更多的怀疑在于 Hibernate。我现在没有时间查看他们的代码,但我建议您查看,使用堆栈跟踪中的行号。

我终于找到了问题,这是一个已知的 hibernate bug 状态 "blocker"。

如果没有任何合理的解决方法,数据库恢复似乎是不可能的,或者至少是非常不切实际的。