問題症狀
在Win7和Win10下啟動時均會出現下面的錯誤,但是在OSX和Linux下沒問題
com.mysql.jdbc.exceptions.jdbc4.CommunicationsException: Communications link failure
The last packet successfully received from the server was 18,982 milliseconds ago. The last packet sent successfully to the server was 18,979 milliseconds ago.
at sun.reflect.NativeConstructorAccessorImpl.newInstance0(Native Method)
at sun.reflect.NativeConstructorAccessorImpl.newInstance(NativeConstructorAccessorImpl.java:62)
at sun.reflect.DelegatingConstructorAccessorImpl.newInstance(DelegatingConstructorAccessorImpl.java:45)
at java.lang.reflect.Constructor.newInstance(Constructor.java:423)
at com.mysql.jdbc.Util.handleNewInstance(Util.java:403)
at com.mysql.jdbc.SQLError.createCommunicationsException(SQLError.java:990)
...
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.lang.reflect.Method.invoke(Method.java:498)
at org.springframework.boot.devtools.restart.RestartLauncher.run(RestartLauncher.java:49)
Caused by: java.net.SocketException: Connection reset
at java.net.SocketInputStream.read(SocketInputStream.java:210)
at java.net.SocketInputStream.read(SocketInputStream.java:141)
at com.mysql.jdbc.util.ReadAheadInputStream.fill(ReadAheadInputStream.java:101)
at com.mysql.jdbc.util.ReadAheadInputStream.readFromUnderlyingStreamIfNecessary(ReadAheadInputStream.java:144)
at com.mysql.jdbc.util.ReadAheadInputStream.read(ReadAheadInputStream.java:174)
at com.mysql.jdbc.MysqlIO.readFully(MysqlIO.java:2966)
at com.mysql.jdbc.MysqlIO.reuseAndReadPacket(MysqlIO.java:3427)
... 166 common frames omitted
配置文件
spring:
datasource:
type: com.alibaba.druid.pool.DruidDataSource
druid:
aservice:
driverClassName: com.mysql.jdbc.Driver
url: jdbc:mysql://192.168.1.2:3306/aservice?useUnicode=true&characterEncoding=utf8
username: dbuser
password: dbuser
bservice:
...
...
validationQuery: SELECT 1 FROM DUAL
testWhileIdle: true
對應的各組件版本為
spring-boot-dependencies:2.1.17.RELEASE
druid-spring-boot-starter:1.1.17
mysql-connector-java:auto --> 8.0.21
檢查過程
排除網絡及MySQL服務器賬號問題:用客戶端可以正常連接,同樣的代碼在OSX和Ubuntu20.04下均可正常連接。如果通過公網opnvpn連接到內網,在windows下也可以正常連接
排除mysql-connector-java版本不兼容問題:切換到8.0.16和5.1.46后問題依舊
排除mysql-connector-java自身問題:僅使用spring-jdbc + mysql-connector-java 可以正常連接
在SpringBoot:2.2.4.RELEASE + hikari的項目B上,在同樣的內網環境連接此數據庫,無法連接。因此問題范圍可以縮小到SpringBoot上。
這個項目里出現的錯誤不太一樣,是
201030 14:09:11275 main W xxer.hikari.pool.PoolBase#481 HikariPool-1 - Default transaction isolation level detection failed (No operations allowed after connection closed.).
201030 14:09:11279 main E er.hikari.pool.HikariPool#493 HikariPool-1 - Error thrown while acquiring connection from data source
java.sql.SQLNonTransientConnectionException: No operations allowed after connection closed.
at com.mysql.cj.jdbc.exceptions.SQLError.createSQLException(SQLError.java:110)
at com.mysql.cj.jdbc.exceptions.SQLError.createSQLException(SQLError.java:97)
at com.mysql.cj.jdbc.exceptions.SQLError.createSQLException(SQLError.java:89)
at com.mysql.cj.jdbc.exceptions.SQLError.createSQLException(SQLError.java:63)
at com.mysql.cj.jdbc.exceptions.SQLError.createSQLException(SQLError.java:73)
at com.mysql.cj.jdbc.exceptions.SQLExceptionsMapping.translateException(SQLExceptionsMapping.java:73)
at com.mysql.cj.jdbc.ConnectionImpl.setTransactionIsolation(ConnectionImpl.java:2279)
...
at com.rockbb.banyan.commons.impl.CommonsBoot.main(CommonsBoot.java:14)
Caused by: com.mysql.cj.exceptions.ConnectionIsClosedException: No operations allowed after connection closed.
at sun.reflect.NativeConstructorAccessorImpl.newInstance0(Native Method)
at sun.reflect.NativeConstructorAccessorImpl.newInstance(NativeConstructorAccessorImpl.java:62)
at sun.reflect.DelegatingConstructorAccessorImpl.newInstance(DelegatingConstructorAccessorImpl.java:45)
at java.lang.reflect.Constructor.newInstance(Constructor.java:423)
at com.mysql.cj.exceptions.ExceptionFactory.createException(ExceptionFactory.java:61)
at com.mysql.cj.exceptions.ExceptionFactory.createException(ExceptionFactory.java:105)
at com.mysql.cj.exceptions.ExceptionFactory.createException(ExceptionFactory.java:151)
at com.mysql.cj.NativeSession.checkClosed(NativeSession.java:1274)
at com.mysql.cj.jdbc.ConnectionImpl.checkClosed(ConnectionImpl.java:575)
at com.mysql.cj.jdbc.ConnectionImpl.setTransactionIsolation(ConnectionImpl.java:2226)
... 70 common frames omitted
在項目B上,通過debug發現連接關閉這個狀態是通過NativeSession.forceClose()設置的,調用棧是
com.mysql.cj.jdbc.ConnectionImpl.abortINternal()
com.mysql.cj.jdbc.ConnectionImpl.isValid()
com.zaxxer.hikari.pool.PoolBase.checkValidationSupport()
在這個方法里,判斷連接是否有效有兩種方法,
if (this.isUseJdbc4Validation) {
connection.isValid(1);
} else {
this.executeSql(connection, this.config.getConnectionTestQuery(), false);
}
出錯時使用的是前一種,這里timeout是固定的1秒,在這1秒內無返回就出錯了。因為超時不能修改,所以要改用第二種,可以看到這個對象初始化時這個變量的賦值為
this.isUseJdbc4Validation = config.getConnectionTestQuery() == null;
所以只需要配置中有connection-test-query: "SELECT 1"
這一項就可以了,在hikari的配置中加上這項,程序就可以正常運行了。
回到項目A,項目A使用的是DruidDatasource, 沒有上面的這個參數項,配置這個不起作用。
對項目中出現異常的部分加斷點debug,可以發現異常的來源是com.mysql.cj.protocol.ReadAheadInputStream.fill(), 實際的錯誤來源是socket read timeout,這個類里有一個doDebug變量可以控制是否輸出socket讀寫的信息,再繼續跟蹤得到這個選項是traceProtocol=true, 要配置到dbUrl上。
打開這個信息后發現,實際上連接出錯是在和服務器多次交互后才發生的。
繼續跟蹤,連接的關閉是在這個方法后發生的com.alibaba.druid.pool.vendor.MySqlValidConnectionChecker.isValidConnection(...)
,這個方法里通過ping和sql查詢兩種方式檢查連接有效性,並且查詢的timeout也是1秒,所以先修改timeout試試,在druid的配置上加上
validationQueryTimeout: 30
后,debug中可以發現確實ping阻塞在那里直到超時也無返回,所以這個不能解決問題,再嘗試換成另一種方式檢查有效性。這個開關字段是usePingMethod,經過檢查,這個參數不能通過application.yml帶入,要加在java啟動時的命令行,加上-Ddruid.mysql.usePingMethod=false這個參數啟動后,就會通過SELECT 1去檢查連接有效性,這個檢測就沒問題。因為超時時間也使用於這個檢測,所以最終解決方案是
- 啟動命令行中加上 -Ddruid.mysql.usePingMethod=false 參數
- application.yml的Druid配置中加上 validationQueryTimeout: 30 和 validationQuery: SELECT 1這兩項