距离上一次写博客,时间已经非常久远了。经历了一段觉得没有东西可以写的时间,本质上应该是失去了监管后的自我放松吧。期间,复写的念头来来走走,都因为懒而作罢。学习这件事情,不能等到用时方恨少……否则变得被动,自我改造中!!!入正题。
关于什么是druid,这里不做介绍了,有兴趣了解的朋友先自行找些资料哈,sorry。这里我打算只是记录一下我遇到的问题。如果你不曾使用过这个东西,那这里的内容可能看着也没啥意思哈。
异常现象
近期的一个用户中心的项目提供对外的登录认证、用户基本信息的调取等接口,给公司的其他平台使用。因此在性能上要求支持300并发,平均响应时间在1s左右。我们采用了druid连接池,为了提高响应速度我们开启了PreparedStatement(pscache)缓存,缓存数量设置在100。在压力测试的时候没有发现什么问题,但是上线后在某种特定情况下,不定期出现服务器卡死的现象。监控了数据库连接数、带宽、cpu、内存、服务器之间的网络连接数等等,均为发现异常情况。
发现问题
经过很多次猜测,并实验。发现我们的问题在于启用了pscache,我们关闭了缓存以后响应速度下来了。但是服务器不会再出现卡死现象。因此,基本上锁定问题出在这个pscache上。 进一步测试实验发现,卡死状态下,出现问题的仅有两个功能登录和机构添加。这两个功能都涉及到数据库的触发器。查询数据库操作日志发现卡死时触发器报错,因此锁定在触发器报错时pscache对该错误的处理上。
查看druid的源码
跟踪druid连接池的代码,类DruidDataSource和OracleExceptionSorter。
DruidDataSource类中的该方法决定是否还将使用过的连接归还给连接池,或者作废出错的连接。
public void handleConnectionException(DruidPooledConnection pooledConnection, Throwable t) throws SQLException {
DruidConnectionHolder holder = pooledConnection.getConnectionHolder();
this.errorCount.incrementAndGet();
this.lastError = t;
this.lastErrorTimeMillis = System.currentTimeMillis();
if(!(t instanceof SQLException)) {
throw new SQLException("Error", t);
} else {
SQLException sqlEx = (SQLException)t;
ConnectionEvent event = new ConnectionEvent(pooledConnection, sqlEx);
Iterator requireDiscard = holder.getConnectionEventListeners().iterator();
while(requireDiscard.hasNext()) {
ConnectionEventListener eventListener = (ConnectionEventListener)requireDiscard.next();
eventListener.connectionErrorOccurred(event);
}
if(this.exceptionSorter != null && this.exceptionSorter.isExceptionFatal(sqlEx)) {
if(pooledConnection.isTraceEnable()) {
Map requireDiscard1 = this.activeConnections;
synchronized(this.activeConnections) {
if(pooledConnection.isTraceEnable()) {
this.activeConnections.remove(pooledConnection);
pooledConnection.setTraceEnable(false);
}
}
}
boolean requireDiscard2 = false;
synchronized(pooledConnection) {
if(!pooledConnection.isClosed() || !pooledConnection.isDisable()) {
holder.setDiscard(true);
pooledConnection.disable(t);
requireDiscard2 = true;
}
}
if(requireDiscard2) {
this.discardConnection(holder.getConnection());
holder.setDiscard(true);
}
LOG.error("discard connection", sqlEx);
}
throw sqlEx;
}
}
O racleExceptionSorter类中的该方法对数据库返回的错误进行处理,标识连接是否作废:
public boolean isExceptionFatal(SQLException e) {
int error_code = Math.abs(e.getErrorCode());
switch(error_code) {
case 28:
case 600:
case 1012:
case 1014:
case 1033:
case 1034:
case 1035:
case 1089:
case 1090:
case 1092:
case 1094:
case 2396:
case 3106:
case 3111:
case 3113:
case 3114:
case 3134:
case 3135:
case 3136:
case 3138:
case 3142:
case 3143:
case 3144:
case 3145:
case 3149:
case 6801:
case 6802:
case 6805:
case 9918:
case 9920:
case 9921:
case 17001:
case 17002:
case 17008:
case 17024:
case 17089:
case 17401:
case 17409:
case 17410:
case 17416:
case 17438:
case 17442:
case 25407:
case 25408:
case 25409:
case 25425:
case 29276:
case 30676:
return true;
default:
if(error_code >= 12100 && error_code <= 12299) {
return true;
} else {
String error_text = e.getMessage().toUpperCase();
return error_code >= 20000 && error_code < 21000 || !error_text.contains("SOCKET") && !error_text.contains("套接字") && !error_text.contains("CONNECTION HAS ALREADY BEEN CLOSED") && !error_text.contains("BROKEN PIPE") && !error_text.contains("管道已结束")?this.fatalErrorCodes.contains(Integer.valueOf(error_code)):true;
}
}
}
}
触发器抛出的错误的错误为:java.sql.SQLException:internal error,对应的错误码我不记得了,但是不在上面这个类的所有case项里。druid认为oracle数据库内部错误不数据应用程序的数据库连接有问题,因此该连接被连接池保持了。但实际上该连接在开始pscache的情况下是有问题的,不能复用。具体为啥没有看懂,汗。
解决办法
触发器发生错误时,手动抛出错误,抛出一个druid能够识别的错误。在触发器END之前添加如下内容:
EXCEPTION
WHEN OTHERS THEN
RAISE NOT_LOGGED_ON;
问题解决,曲线救国,不是正经路子。
总结:这种错误实在是不容易发现,得多做实验。