首先抛出个常见的长连接问题:
1 都知道连接MySQL的应用中大多会使用框架例如 c3p0 ,dbcp proxool 等来管理数据库连接池。 数据库连接池毫无疑问都是采用长连接方式。 那么MySQL经典八小时问题为何产生?
我一开始的疑惑是既然是长连接必然有不停的心跳检测机制一直不停的骚扰者服务端, 那么服务端怎么还能检测到一个八个小时毫无动静的连接呢? 无非是因为八个小时内client端没有发送心跳包给服务器,不禁会问长连接会没有心跳包么?
2 又或者我们常用的SecureCRT工具,当我们想要保持终端一直畅通就需要设置以下参数,意味着60秒发一次心跳
3 再者用client list 观察我们的redis 服务器,会发现大量的ping请求, 那么这些心跳ping请求由谁来控制触发? 什么情况下会触发?
-------------------------------------------------------那么接下来是揭开谜底的序幕-------------耐心分析---------------------------------------------------------------------------------------------
我们常用的jedis-XXX.jar以及c3p0XX.jar底层都是使用 commons-pool来作为对象池的实现。下面以租车公司为例子说明这张图,介绍commons pool的基本工作方式:
图1
GenericObjectPool(租车公司)
作为租车公司,需要提供租车和收回归还的车辆的两个服务,同时它还要管理着所有的那些车辆,随着业务发展壮大,需要买新车;对于已经不能安全驾驶的车辆,需要将其销毁;同时还要定期对车辆进行安全检测等。
PooledObject(租车公司的所有车辆)
租车公司的车辆分为三类:空闲可租用的车辆(Idle Objects),已借出的车辆(Active Objects),认为已丢弃的车辆(Anandoned Objects)
Borrow Object(租车)
- A1: 世界那么大,一位年轻人想租辆车出去逛逛
- A2: 老板先看看有没有空闲的车
- A3.1: 如果有,则将最近归还的车借出去,并标记为已借出(Active),如果没有空闲的车了,就买辆,同时也标记为已借出(这是一家不差钱的公司)
- A3.2: 老板把标记好的车租给年轻人
Return Object(还车)
- B1: 世界那么大,年轻人终于逛完了,回来还车
- B2: 老板把车放回停车场,并把标记改为空闲状态(Idle),可以再被其他人租用。
TestOnBorrow/TestOnReturn(租出/归还时进行检查)
这家公司不仅不差钱,它对车辆的安全还很负责,对于租出去的车,不管是从空闲车辆里取出的,还是新买回的,都会先检查一遍这车的好坏,总不能坑了年轻人,如果发现有问题,立马再换一辆。归还的时候,也会检查一遍,如果有问题,就扔掉(真土豪),除此之外,公司还专门请了一位车辆安检员,定期对闲置了一段时间的车辆进行安全检测(Evict Thread),一有问题也扔掉。
有借有还,看上去一切都很美好。
然而现实里总有意外发生:
年轻人借走车后,发现世界越逛越大,久久不愿回家。 安检员定期检查时发现这车子都借出去大半年了,还没还回来,是不是丢了?于是掏出手机,”啪“的按了一下,远程将车子熄了火,标记为报废车辆(Abandoned),当作报废处理了。
Evict Thread(定期检查的安检人员)
- C1: 对于已归还标记为空闲的车辆,安检员定期对它们抽查,如果超过一段时间没有使用,看看是否坏掉,坏了就及时作废掉(C2).
- D1: 对于标记为已借出的对象,安检员定期检查时发现借出很久都未还,直接作废(D2)。
好了,故事讲完了,希望大家对Commons Pool都理解了。
------------------------------------------------------------------接下来剖析Commons-pool关键源码-----------------耐心分析----------------------------------------------------------------------------------------------------------------------------------------------
为了简单起见,该图只表现继承和实现关系
图2
上层的jar应用大部分情况下我们只使用ObjectPool和PoolableObjectFactory的相关实现类, 例如jedisFactory 继承的是BasePoolableObjectFactory(典型工厂设计模式), 其pool直接实例化GenricObjectPool类 。
下面以jedis为例简单说明JedisFactory干了啥事:
1 对象如何创建
jedis的JedisFactory继承了org.apache.commons.pool.BasePoolableObjectFactory实现了makeObject()方法 :
public Object makeObject() throws Exception { final Jedis jedis = new Jedis(this.host, this.port, this.timeout); // 开始进行socket连接 jedis.connect(); if (null != this.password) { jedis.auth(this.password); } if( database != 0 ) { jedis.select(database); } return jedis; }
这段代码很好懂,无非就是底层开启个socket 开始连接对应的服务器, 例如最底层的代码:
public void connect() { if (!isConnected()) { try { socket = new Socket(); //->@wjw_add socket.setReuseAddress(true); socket.setKeepAlive(true); //Will monitor the TCP connection is valid socket.setTcpNoDelay(true); //Socket buffer Whetherclosed, to ensure timely delivery of data socket.setSoLinger(true,0); //Control calls close () method, the underlying socket is closed immediately //<-@wjw_add socket.connect(new InetSocketAddress(host, port), timeout); socket.setSoTimeout(timeout); outputStream = new RedisOutputStream(socket.getOutputStream()); inputStream = new RedisInputStream(socket.getInputStream()); } catch (IOException ex) { throw new JedisConnectionException(ex); } } }
2 activateObject 激活对象
public void activateObject(Object obj) throws Exception { if (obj instanceof Jedis) { final Jedis jedis = (Jedis)obj; if (jedis.getDB() != database) { jedis.select(database); } } }
3 destroyObject 销毁对象
public void destroyObject(final Object obj) throws Exception { if (obj instanceof Jedis) { final Jedis jedis = (Jedis) obj; if (jedis.isConnected()) { try { try { jedis.quit(); } catch (Exception e) { } jedis.disconnect(); } catch (Exception e) { } } } }
4 validateObject 检测对象(对象借入借出时如果需要检测此对象的健康状况,或者定时对池对象进行evict)
public boolean validateObject(final Object obj) { if (obj instanceof Jedis) { final Jedis jedis = (Jedis) obj; try { return jedis.isConnected() && jedis.ping().equals("PONG"); } catch (final Exception e) { return false; } } else { return false; } }
只做了上述四件事情,告诉对象池(GenricObjectPool) 如何创建对象,如何激活对象, 如何销毁对象,以及用什么方式做心跳检测。结合图1的池对象流转图
接下来分析GenricObjectPoo里面的几个功能: 对象的借出:borrowObject(), 对象的归还:returnObject() ,以及对象池的后台管理:Evictor定时
这里参考一篇文章:http://deyimsf.iteye.com/blog/2119488
此文章基本上很好的论述了源码关键部位。 接下来分析下 idel 时间是怎么算出来的, 以及作用是什么
A : 一个对象的出生时打上个系统时间--------borrowObject()方法或者evict()方法的--->ensureMinIdle()方法(维持最小的idel个对象)
borrowObject方法示例:
boolean newlyCreated = false; if(null == latch.getPair()) { try { T obj = _factory.makeObject();
// 此刻创建一个新生命, new ObjectTimestampPair是拿当前的系统时间当此生命的生日 latch.setPair(new ObjectTimestampPair<T>(obj)); newlyCreated = true; } finally { if (!newlyCreated) { // object cannot be created synchronized (this) { _numInternalProcessing--; // No need to reset latch - about to throw exception } allocate(); } } }
ensureMinIdle()方法示例
private void ensureMinIdle() throws Exception { // this method isn't synchronized so the // calculateDeficit is done at the beginning // as a loop limit and a second time inside the loop // to stop when another thread already returned the // needed objects
// 上面英文注释很明确 int objectDeficit = calculateDeficit(false);
for ( int j = 0 ; j < objectDeficit && calculateDeficit(true) > 0 ; j++ ) { try {
// 跟进去会发现此处就是不断创建新生命,如果满足条件就放入池中 addObject(); } finally { synchronized (this) { _numInternalProcessing--; } allocate(); } } }
B : 一个对象使用完归来时再次打上个系统时间--------returnObject()方法的addObjectToPool方法代码片段
synchronized (this) { if (isClosed()) { shouldDestroy = true; } else { if((_maxIdle >= 0) && (_pool.size() >= _maxIdle)) { shouldDestroy = true; } else if(success) { // borrowObject always takes the first element from the queue, // so for LIFO, push on top, FIFO add to end if (_lifo) {
// 此处对归来的obj 对象打上系统时间并放入对首。 last in first out 算法是jedis 默认算法 _pool.addFirst(new ObjectTimestampPair<T>(obj)); } else { _pool.addLast(new ObjectTimestampPair<T>(obj)); } if (decrementNumActive) { _numActive--; } doAllocate = true; } } }
c : 当给minEvictableIdleTimeMillis这个参数配置上一个大于0 的数,意味着对象空闲了多久就被会干掉, 例如evict()代码片段
// 此处去算此对象空闲了多久,接下来判断需不需要把此对象干掉
final long idleTimeMilis = System.currentTimeMillis() - pair.tstamp; if ((getMinEvictableIdleTimeMillis() > 0) && (idleTimeMilis > getMinEvictableIdleTimeMillis())) { removeObject = true; } else if ((getSoftMinEvictableIdleTimeMillis() > 0) && (idleTimeMilis > getSoftMinEvictableIdleTimeMillis()) && ((getNumIdle() + 1)> getMinIdle())) { // +1 accounts for object we are processing removeObject = true; } if(getTestWhileIdle() && !removeObject) { boolean active = false; try { _factory.activateObject(pair.value); active = true; } catch(Exception e) { removeObject=true; } if(active) { if(!_factory.validateObject(pair.value)) { removeObject=true; } else { try { _factory.passivateObject(pair.value); } catch(Exception e) { removeObject=true; } } } }
OK 以上三段代码讲述完毕。 阐述了对象的idel 时间的由来以及用处。 个人建议把minEvictableIdleTimeMillis设置成-1
---------------------------------------------------来个使用总结吧-----------------------------------------------------------------------------------------------------------------------------------
redis 使用注意事项:
1 redis server 的time_out(单位是S清除idel多久的客户端连接)最好不要设置成一个大于0的时间。
设置成0即不加限制的隐患 :当客户端此刻与服务端断网, 而http请求依然不断,那么当一个请求从池中取得一个连接(连接池此刻remove一个连接), 接着调用validateObject()方法发现ping出错, 紧接着就会调用destroyObject 进行对象的销毁(由于断网客户端的quit命令压根发送不到服务器端), 服务器端就会出现一堆僵尸进程。
2 服务器端如果设置了idel>0的空闲时间, 那么客户端最好设置上对应的心跳频率即多久心跳一次,
timeBetweenEvictionRunsMillis=60000 // 多久跑一次任务
pool.minEvictableIdleTimeMillis=-1 // idel 多久的对象就会被清理掉 , -1意味着无论空着多久都没事。 实际上上面的那个定时任务没跑一次,被定位到的线程都要去ping一次服务器,所以对象的空闲时间也就是60S。
这里面还涉及到每一次清理定时任务去清理多少个对象的问题。 一次清理的对象不要太少(或者走默认配置隔一定时间, 对象池里面的空闲对象全部心跳一次)即以下配置(不配置会走默认配置即全部检查):
numTestsPerEvictionRun // 驱逐器每次运行时检查池中闲置对象的最大个数 ,(比如该值设置为3,此时池中有5个闲置对象,那么每次只会检查前三个闲置对象。
所以也就回答了mysql 八小时问题, 就是因为服务器端设置了一个超时时间,而客户端却没有配置上定时心跳检测的功能,导致八个小时之后被服务器端干掉。