手写DAO框架(七)-如何保证连接可用
版权声明:本文为博客园博主「水木桶」的原创文章,遵循CC 4.0 by-sa版权协议,转载请附上原文出处链接及本声明。
原文链接:https://www.cnblogs.com/shuimutong/p/11408219.html
背景
手写DAO框架系列前后更新了5篇文章,外加1篇使用示例,GDAO框架至此已经初具雏形,简单的使用不成问题。接下来就是对框架进行不断的优化。
顺便说一下性能
手写DAO框架(六)-后续之框架使用示例 此篇文章对GDAO的性能较少提及,这里就简单的记载一下,后续有机会再更新。
数据示例:id:14005 name:BatchName-4009 age:52,其中id是自动生成。
数据库是mariadb,和测试程序位于同一台机器
测试数据条数5000条,一次调用
userDao.add(uds[i]);
方法插入一条数据。
系统 | 配置 | 结果 |
macOS | 2c4t,8g,固态硬盘 | 总耗时:8629.0ms,平均耗时:1.726ms |
Win10 | 6c6t,24g,固态硬盘 | 总耗时:5153.0ms,平均耗时:1.031ms |
一、当前问题分析
对于一个提供连接池功能的DAO框架,如果保存的连接失效了无法自动移除池,如果连接数据库的网络出现闪断连接无法继续使用,只能通过重启服务来达到初始化连接的目的,这样的做法显然是不够优雅的。
为了提高框架的稳定性,所以决定对框架的连接部分做一次优化。
二、需求整理
通过网上查资料,拟定了几个点。
1、自动重连
autoReconnect=true
JDBC通过配置可实现
2、连接有效性检测
a、配置连接检测语句。备注:有的数据库Driver支持ping(),可以使用。
3、连接泄露检查
当连接从连接池借出后,长时间(配置时间)不归还,将强制回收。
4、了解到的其他问题
如果连接闲置时间过长,可能被mysql主动关闭。
正常的使用-归还(连接放到队列,队列是先进先出,下次再取,形成一个循环)流程,可避免连接闲置时间过长,暂缓优化。
优化点确定
2、连接有效性检测
3、连接泄露检查
三、编码实现
博主开始写代码了,需要一段时间
连接有效性检测,根据配置来检测,代码参考MysqlValidConnectionChecker。
连接泄露检查,通过启一条线程,根据配置时间进行检查。
----------------------经过了好几天的编写,代码完成了---------------------------------------------
直接上代码。
1、增加配置
#########v03##########
#连接检测语句 checkConnectionValidationQuery=select 1
#归还连接时检测连接,true false
checkConnectionWhenReturn=true #定时检测连接间隔时长(分钟) periodCheckConnectionTimeMin=10 #连接泄露检测 connectionLeakCheck=true #连接泄露检测间隔时长(分钟) connectionLeakCheckPeriodTimeMin=10 #强制归还连接时长(小时) forceReturnConnectionTimeHour=6
说明:
1)配置可以分为1个基础,3个部分。
1个基础是指连接检测语句,3个部分分别对应归还连接时检测连接、定时检测连接间隔时长、连接泄露检测
2)定时检测主要是防止连接中断了不能自动生成新的连接,连接间隔时长如果设为0,则不会定时检测。
2、主要代码
1 package me.lovegao.gdao.connection; 2 3 import java.sql.Connection; 4 import java.sql.SQLException; 5 import java.util.ArrayList; 6 import java.util.HashSet; 7 import java.util.Iterator; 8 import java.util.List; 9 import java.util.Map; 10 import java.util.Map.Entry; 11 import java.util.Properties; 12 import java.util.Queue; 13 import java.util.Set; 14 import java.util.concurrent.ConcurrentHashMap; 15 import java.util.concurrent.ConcurrentLinkedQueue; 16 import java.util.concurrent.ExecutorService; 17 import java.util.concurrent.Executors; 18 19 import org.apache.commons.lang3.StringUtils; 20 import org.apache.commons.lang3.math.NumberUtils; 21 import org.slf4j.Logger; 22 import org.slf4j.LoggerFactory; 23 24 import me.lovegao.gdao.bean.SystemConstant; 25 import me.lovegao.gdao.util.ConnectionUtil; 26 27 /** 28 * 第二版简易连接池实现<br/> 29 * 主要增加连接有效性检测,包括归还连接时检测,定时检测连接 30 * 31 * @author simple 32 * 33 */ 34 public class SimpleV2ConnectionPool extends SimpleConnectionPool { 35 private final static Logger log = LoggerFactory.getLogger(SimpleV2ConnectionPool.class); 36 private ExecutorService ES; 37 // 归还连接时检测连接,这步最好做成异步的,避免影响归还速度 38 private boolean checkConnectionWhenReturn = false; 39 // 连接检测语句 40 private String checkConnectionValidationQuery; 41 // 定时检测连接的时间(分钟) 42 private int periodCheckConnectionTimeMin; 43 /** 待检测连接 **/ 44 private volatile Queue<Connection> TO_CHECK_CONNECTION_POOL; 45 //查询超时时间 46 private final int QUERY_TIMEOUT_SECONDS; 47 //连接泄露检测 48 private boolean checkConnectionLeak = false; 49 //连接泄露检测间隔时长-分钟 50 private int checkConnectionLeakPeriodTimeMin = 30; 51 //强制归还连接时长(小时) 52 private double forceReturnConnectionTimeHour; 53 //连接最大空闲时长(小时) 54 // private double connectionMaxIdleTimeHour; 55 /**连接最后借出时间**/ 56 private Map<Integer, Long> CONNECTION_OUT_TIME_MAP_POOL = null; 57 58 public SimpleV2ConnectionPool(Properties properties) throws Exception { 59 super(properties); 60 QUERY_TIMEOUT_SECONDS = super.getQueryTimeoutSecond(); 61 initProp(properties); 62 initCheck(); 63 } 64 65 private void initProp(Properties properties) { 66 //连接有效性检测配置 67 if (properties.containsKey(SystemConstant.STR_CHECK_CONNECTION_VALIDATION_QUERY)) { 68 checkConnectionValidationQuery = properties 69 .getProperty(SystemConstant.STR_CHECK_CONNECTION_VALIDATION_QUERY); 70 String checkWhenReturn = properties.getProperty(SystemConstant.STR_CHECK_CONNECTION_WHEN_RETURN); 71 if (checkWhenReturn.toLowerCase().equals("true")) { 72 checkConnectionWhenReturn = true; 73 } 74 if (properties.containsKey(SystemConstant.STR_PERIOD_CHECK_CONNECTION_TIME_MIN)) { 75 String periodTimeStr = properties.getProperty(SystemConstant.STR_PERIOD_CHECK_CONNECTION_TIME_MIN); 76 if (StringUtils.isNumeric(periodTimeStr)) { 77 periodCheckConnectionTimeMin = Integer.parseInt(periodTimeStr); 78 } 79 } 80 } 81 //连接泄露检测配置 82 if (properties.containsKey(SystemConstant.STR_CONNECTION_LEAK_CHECK)) { 83 String leakCheckStr = properties.getProperty(SystemConstant.STR_CONNECTION_LEAK_CHECK); 84 if (leakCheckStr.toLowerCase().equals("true")) { 85 String leakCheckPeriodTimeStr = properties.getProperty(SystemConstant.STR_CONNECTION_LEAK_CHECK_PERIOD_TIME_MIN); 86 if (StringUtils.isNumeric(leakCheckPeriodTimeStr)) { 87 checkConnectionLeakPeriodTimeMin = Integer.parseInt(leakCheckPeriodTimeStr); 88 } 89 String forceReturnTimeStr = properties.getProperty(SystemConstant.STR_FORCE_RETURN_CONNECTION_TIME_HOUR); 90 if (NumberUtils.isNumber(forceReturnTimeStr)) { 91 forceReturnConnectionTimeHour = Double.parseDouble(forceReturnTimeStr); 92 if(forceReturnConnectionTimeHour > 0) { 93 checkConnectionLeak = true; 94 } 95 } 96 //最大空闲检测,功能上和定时检测连接重复,暂时不开发。 97 98 //需要同时配置(强制归还时间)才能检测连接泄露 99 if(forceReturnConnectionTimeHour > 0) { 100 checkConnectionLeak = true; 101 } 102 } 103 } 104 StringBuilder infoSb = new StringBuilder(); 105 infoSb.append("SimpleV2ConnectionPoolInitDone------") 106 .append(",checkConnectionValidationQuery:").append(checkConnectionValidationQuery) 107 .append(",checkConnectionWhenReturn:").append(checkConnectionWhenReturn) 108 .append(",periodCheckConnectionTimeMin:").append(periodCheckConnectionTimeMin) 109 .append(",checkConnectionLeak:").append(checkConnectionLeak) 110 .append(",checkConnectionLeakPeriodTimeMin:").append(checkConnectionLeakPeriodTimeMin) 111 .append(",forceReturnConnectionTimeHour:").append(forceReturnConnectionTimeHour); 112 System.out.println(infoSb.toString()); 113 } 114 115 @Override 116 public Connection getConnection() throws Exception { 117 Connection conn = super.getConnection(); 118 if(checkConnectionLeak) { 119 int connHashCode = conn.hashCode(); 120 CONNECTION_OUT_TIME_MAP_POOL.put(connHashCode, System.currentTimeMillis()); 121 } 122 return conn; 123 } 124 125 @Override 126 public void returnConnection(Connection conn) { 127 //检测连接泄露 128 if(checkConnectionLeak) { 129 int connHashCode = conn.hashCode(); 130 //连接超时过长,已经被主动移除 131 if(!CONNECTION_OUT_TIME_MAP_POOL.containsKey(connHashCode)) { 132 return; 133 } else { 134 CONNECTION_OUT_TIME_MAP_POOL.remove(connHashCode); 135 } 136 } 137 //检测归还连接 138 if (checkConnectionWhenReturn) { 139 if(TO_CHECK_CONNECTION_POOL.isEmpty()) { 140 synchronized(TO_CHECK_CONNECTION_POOL) { 141 if(TO_CHECK_CONNECTION_POOL.isEmpty()) { 142 TO_CHECK_CONNECTION_POOL.add(conn); 143 TO_CHECK_CONNECTION_POOL.notifyAll(); 144 } else { 145 TO_CHECK_CONNECTION_POOL.add(conn); 146 } 147 } 148 } else { 149 TO_CHECK_CONNECTION_POOL.add(conn); 150 } 151 } else { 152 superReturnConnection(conn); 153 } 154 } 155 156 157 @Override 158 public void closeConnectionPool() { 159 if(ES != null) { 160 ES.shutdownNow(); 161 } 162 super.closeConnectionPool(); 163 } 164 165 private void superReturnConnection(Connection conn) { 166 super.returnConnection(conn); 167 } 168 169 private Connection superGetByConnectionHashCode(int hashCode) { 170 return super.getByConnectionHashCode(hashCode); 171 } 172 173 // 初始化检查 174 private void initCheck() { 175 int threadPoolSize = 0; 176 if(checkConnectionWhenReturn) { 177 threadPoolSize += 2; 178 TO_CHECK_CONNECTION_POOL = new ConcurrentLinkedQueue(); 179 } 180 if(periodCheckConnectionTimeMin > 0) { 181 threadPoolSize++; 182 } 183 //需要同时配置(强制归还时间、最大空闲时间)才能检测连接泄露 184 if(checkConnectionLeak) { 185 threadPoolSize++; 186 CONNECTION_OUT_TIME_MAP_POOL = new ConcurrentHashMap(); 187 } 188 if(threadPoolSize > 0) { 189 ES = Executors.newFixedThreadPool(threadPoolSize); 190 // 检查归还连接 191 if(checkConnectionWhenReturn) { 192 //启两个线程同时检测 193 for(int i=0; i<2; i++) { 194 ES.execute(new ReturnConnectionCheck()); 195 } 196 } 197 //定时检测连接 198 if (periodCheckConnectionTimeMin > 0) { 199 ES.execute(new ConnectionPeriodCheck()); 200 } 201 //连接泄露检测 202 if(checkConnectionLeak) { 203 ES.execute(new ConnectionLeakCheck()); 204 } 205 } 206 } 207 208 /** 209 * 连接泄露定时检测 210 * @author simple 211 * 212 */ 213 class ConnectionLeakCheck implements Runnable { 214 int sleepTimeMs = checkConnectionLeakPeriodTimeMin * 60 * 1000; 215 @Override 216 public void run() { 217 Set<Integer> preConnHashCodeSet = new HashSet(); 218 while (true) { 219 if(ES.isShutdown()) { 220 break; 221 } 222 try { 223 Thread.sleep(sleepTimeMs); 224 } catch (Exception e) { 225 log.error("ConnectionLeakCheckSleepException", e); 226 } 227 try { 228 checkConnectionLeak(preConnHashCodeSet); 229 } catch (Exception e) { 230 log.error("ConnectionLeakCheckException", e); 231 } 232 } 233 } 234 } 235 236 //检测连接泄露 237 private void checkConnectionLeak(Set<Integer> preConnHashCodeSet) throws Exception { 238 if(CONNECTION_OUT_TIME_MAP_POOL.size() < 1) { 239 preConnHashCodeSet = new HashSet(); 240 } else { 241 Iterator<Entry<Integer, Long>> connHashCodeIt = CONNECTION_OUT_TIME_MAP_POOL.entrySet().iterator(); 242 //先对比前后两次的连接,如果有相同的,再检测相同的连接 243 if(preConnHashCodeSet.size() == 0) { 244 while(connHashCodeIt.hasNext()) { 245 preConnHashCodeSet.add(connHashCodeIt.next().getKey()); 246 } 247 } else { 248 StringBuilder logSb = new StringBuilder(); 249 long timeFlag = (long) (System.currentTimeMillis() - forceReturnConnectionTimeHour * 3600 * 1000); 250 logSb.append("ConnectionLeakCheck---") 251 .append(",timeFlag:").append(timeFlag) 252 .append(",forceReturnConnectionTimeHour:").append(forceReturnConnectionTimeHour); 253 List<Integer> toCloseConnectionHashCodeList = new ArrayList(); 254 logSb.append(",toCloseConn,{"); 255 //过滤出两次集合重合,且已经超时的元素 256 while(connHashCodeIt.hasNext()) { 257 Entry<Integer, Long> connEntry = connHashCodeIt.next(); 258 int connHashCode = connEntry.getKey(); 259 if(preConnHashCodeSet.contains(connHashCode) && connEntry.getValue() < timeFlag) { 260 toCloseConnectionHashCodeList.add(connHashCode); 261 logSb.append(connHashCode).append(":").append(connEntry.getValue()).append(","); 262 } 263 } 264 logSb.append("}"); 265 if(toCloseConnectionHashCodeList.size() > 0) { 266 for(Integer connHashCode : toCloseConnectionHashCodeList) { 267 Connection conn = superGetByConnectionHashCode(connHashCode); 268 if(conn != null) { 269 try { 270 conn.close(); 271 } catch (SQLException e) { 272 log.error("closeConnectionException", e); 273 } 274 CONNECTION_OUT_TIME_MAP_POOL.remove(connHashCode); 275 superReturnConnection(conn); 276 } 277 } 278 } 279 log.info(logSb.toString()); 280 //进行过一次检测之后,对之前存储的进行初始化 281 preConnHashCodeSet = new HashSet(); 282 } 283 } 284 } 285 286 /** 287 * 归还连接检测 288 * @author simple 289 * 290 */ 291 class ReturnConnectionCheck implements Runnable { 292 @Override 293 public void run() { 294 while (true) { 295 if(ES.isShutdown()) { 296 break; 297 } 298 Connection toCheckConn = TO_CHECK_CONNECTION_POOL.poll(); 299 if (toCheckConn == null) { 300 try { 301 synchronized(TO_CHECK_CONNECTION_POOL) { 302 TO_CHECK_CONNECTION_POOL.wait(); 303 } 304 } catch (InterruptedException e) { 305 log.error("checkReturnConnectionWaitException", e); 306 } 307 } else { 308 boolean canUse = ConnectionUtil.isValidConnection(toCheckConn, checkConnectionValidationQuery, 309 QUERY_TIMEOUT_SECONDS); 310 if (!canUse) { 311 try { 312 toCheckConn.close(); 313 } catch (SQLException e) { 314 log.error("checkReturnConnectionCloseConnException", e); 315 } 316 } 317 superReturnConnection(toCheckConn); 318 } 319 } 320 } 321 } 322 323 /** 324 * 连接定时检测 325 * @author simple 326 * 327 */ 328 class ConnectionPeriodCheck implements Runnable { 329 int sleepTimeMs = periodCheckConnectionTimeMin * 60 * 1000; 330 @Override 331 public void run() { 332 while (true) { 333 if(ES.isShutdown()) { 334 break; 335 } 336 try { 337 Thread.sleep(sleepTimeMs); 338 } catch (Exception e) { 339 log.error("checkReturnConnectionSleepException", e); 340 } 341 while(true) { 342 //是否继续检测 343 boolean continueCheck = false; 344 Connection toCheckConn = null; 345 try { 346 toCheckConn = getConnection(); 347 if (toCheckConn != null) { 348 boolean canUse = ConnectionUtil.isValidConnection(toCheckConn, 349 checkConnectionValidationQuery, QUERY_TIMEOUT_SECONDS); 350 if (!canUse) { 351 toCheckConn.close(); 352 //连接不可用,继续检测其他连接是否正常 353 continueCheck = true; 354 log.info("oneConnectionCannotUse,closeIt....."); 355 } 356 } 357 } catch (Exception e) { 358 log.error("checkReturnConnectionException", e); 359 continueCheck = true; 360 } finally { 361 if (toCheckConn != null) { 362 superReturnConnection(toCheckConn); 363 } 364 } 365 if(continueCheck) { 366 log.info("checkOneConnectionCannotUse,beginToCheckOtherConnection...."); 367 try { 368 Thread.sleep(500); 369 } catch (InterruptedException e) { 370 log.error("checkReturnConnectionWaitException", e); 371 } 372 } else { 373 break; 374 } 375 } 376 } 377 } 378 } 379 }
3、主要思路
1)归还连接时检测连接的思路
归还连接的时候,如果不采用异步,那么归还连接的线程必须等待连接确认完毕之后才能继续执行,这样做感觉性能不是最优的。
所以引入了异步,归还连接时,连接直接放到一个待检测的容器里,不需要等待检测完之后再返回。
待检测连接由检测线程异步进行检测。检测现场从待检测容器里取连接进行检测,必然会出现空的情况。
出现了空的情况怎么做好呢,是在那里自旋等待?是休眠一段时间再检测?还是等待呢?
自旋等待,浪费计算资源;休眠的话,休眠时长不好确定,谁知道下一毫秒会发生什么?万一因为连接未及时检测出现了连接用尽,岂不是很尴尬?
所以,我选择了等待,当线程归还时,主动唤醒等待线程。
代码实现之后,测试的时候,我发现运行报错。wait()、notifyAll()方法不是那样用的,需要获取锁。
添加了锁之后,为了尽量减少同步带来的性能损失,我采取了写单例时经常提到的双重检查:我不需要每次都要拿锁、通知,只需要在待检测连接池是空的时候才需要进行拿锁、通知。、
2)连接泄露检测的思路
连接泄露检测,主要是为了防止连接被借出去之后,很久都没有归还的情景。
很久不归还,这个连接还占着连接池的坑,却没法被复用,所以需要进行检测。
检测需要确定的就是,取出多久算久?然后就是,多久检测一次?
具体实现的时候,还有一个问题,就是强制归还的连接应该怎么归还?直接放回连接池吗?万一连接真的还在被别的线程使用怎么办?
所以这里,我采取先把连接关闭了,然后再归还。
3)定时检测连接的思路
为了保持连接池的连接都是最终可用的,所以需要对连接池的连接进行定时的检测。
如果连接不可用,就把连接关闭,然后从连接池去除。
4、测试代码
1)配置
##驱动名称 driverName=com.mysql.jdbc.Driver ##连接url connectionUrl=jdbc:mysql://localhost:3306/simple?autoReconnect=true&useServerPrepStmts=false&rewriteBatchedStatements=true&connectTimeout=1000&useUnicode=true&characterEncoding=utf-8 ##用户名 userName=simple ##用户密码 userPassword=123456 ##初始化连接数 initConnectionNum=10 ##最大连接数 maxConnectionNum=50 ##最大查询等待时间 maxQueryTime=3 #########v03########## #归还连接时检测连接,true false checkConnectionWhenReturn=true #连接检测语句 checkConnectionValidationQuery=select 1 #定时检测连接间隔时长(分钟) periodCheckConnectionTimeMin=1 #连接泄露检测 connectionLeakCheck=false #连接泄露检测间隔时长(分钟) connectionLeakCheckPeriodTimeMin=1 #强制归还连接时长(小时) forceReturnConnectionTimeHour=0.01
2)测试代码
package me.lovegao.gdao.connpool; import java.sql.Connection; import java.util.Properties; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import me.lovegao.gdao.bean.SystemConstant; import me.lovegao.gdao.connection.IConnectionPool; import me.lovegao.gdao.connection.SimpleV2ConnectionPool; public class V2ConnectionPoolTest { private final static Logger log = LoggerFactory.getLogger(V2ConnectionPoolTest.class); public static void main(String[] args) throws Exception { String dbPath = "mysql2.properties"; log.info("hello-----------------"); log.warn("hello-----------------"); periodCheckConnection(dbPath); } /** * 连接泄露检测 * @param dbPath * @throws Exception */ public static void checkConnectionLeak(String dbPath) throws Exception { Properties dbProp = CommonUtil.loadProp(dbPath); dbProp.setProperty(SystemConstant.STR_PERIOD_CHECK_CONNECTION_TIME_MIN, "10"); dbProp.setProperty(SystemConstant.STR_CONNECTION_LEAK_CHECK, "true"); IConnectionPool connPool = new SimpleV2ConnectionPool(dbProp); Connection conn = connPool.getConnection(); Thread.sleep(240000); connPool.returnConnection(conn); Thread.sleep(61000); connPool.closeConnectionPool(); } /** * 连接定时检测 * @param dbPath * @throws Exception */ public static void periodCheckConnection(String dbPath) throws Exception { Properties dbProp = CommonUtil.loadProp(dbPath); dbProp.setProperty(SystemConstant.STR_PERIOD_CHECK_CONNECTION_TIME_MIN, "1"); IConnectionPool connPool = new SimpleV2ConnectionPool(dbProp); Thread.sleep(120000); Connection conn = connPool.getConnection(); connPool.returnConnection(conn); Thread.sleep(2061000); // connPool.closeConnectionPool(); } /** * 归还连接检测 * @param dbPath * @throws Exception */ public static void checkWhenReturn(String dbPath) throws Exception { Properties dbProp = CommonUtil.loadProp(dbPath); IConnectionPool connPool = new SimpleV2ConnectionPool(dbProp); Connection conn = connPool.getConnection(); conn.close(); connPool.returnConnection(conn); Thread.sleep(20000); connPool.closeConnectionPool(); } }
我是分布执行的测试,通过debug来校验的流程,面对这种项目,不知道单测该如何写。
我是通过中间把数据库关了,又打开,来连接定时检测的。结果证明没有问题。
框架优化版本已提交到git:https://github.com/shuimutong/gdao.git,欢迎指点
相关测试代码:https://github.com/shuimutong/useDemo.git ./gdao-demo下。
-----------------------本文完------------------------------