人生需要总结

手写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下。 

 

-----------------------本文完------------------------------

posted @ 2019-08-25 16:39  水木桶  阅读(312)  评论(0编辑  收藏  举报