Jedis源码阅读之连接池

1. 什么是连接池

一般在程序中如果要和其他的系统创建连接进行交互并且连接的创建代价比较"昂贵"就需要用到连接池. 那怎么样才算是昂贵呢? 简单说来就是创建连接的时间接近甚至超过交互的时间. 所以连接池就是一个创建连接管理连接, 对连接进行缓存的技术. 最常见的连接池就是数据库连接池. 更加具体的关于连接池的介绍, 请移步连接池

2. Jedis的连接池

既然连接池的作用就是管理连接, 那Jedis的连接池也不例外, 它的作用就是缓存Jedisredis server之间的连接. Jedis 连接池的作用具体来说分为以下几个部分:

2.1 创建保存连接的容器

当初始化一个JedisPool的时候会创建一个LinkedBlockingDeque<PooledObject<T>> idleObjects队列, 这个队列用于存放已经和redis server建立连接********并且已经使用过********的连接对象(实际上存放的是DefaultPooledObject<Jedis>对象, 后面会看到).
同时还会创建一个Map<IdentityWrapper<T>, PooledObject<T>> allObjects对象, 这个Map用于没有可用的连接时新创建出来的连接.

2.2 发起请求获取连接

当发起请求从连接池中获取一个连接的时候, 连接池会先从idleObjects队列中获取连接, 如果获取不到则开始创建一个新的连接, 创建新的连接是首先判断当前已经存在的连接是否已经大于连接池可容纳的连接数量, 如果是则不予创建, 以阻塞等的方式从idleObjects中等待获取可用连接(默认是阻塞不超时等待即等待直到有可用的连接, 但是也可以配置超时时间). 如果可以创建, 则创建一个新的连接放入到allObjects对象中, 同时将连接返回.

2.3 连接使用完毕关闭连接

当使用完连接调用连接关闭的时候, 连接池会将归还连接从allObjects中拿出来放入到idleObjects中, 所以下一次再获取连接将从idleObjects直接获取.
但是这里要特别注意, 从allObjects中拿出来放入到idleObjects中时, 并没有将连接从allObjects中删除, 也就是说allObjectsidleObjects中的连接实际上是指向同一个对象实例的. 为什么要这么做, 而不是直接删除allObjects中的连接呢? 因为JedisPool会在指定的时间内对连接池中空闲对象进行删除, 这样可以减少资源的占用, 这个是JedisPool的单独线程自动完成的操作. 所以说, 如果有个连接创建出来长时间没有使用是会被自动销毁的, 而不是一直连接着占用资源.

3. 源码阅读

下面针对于上面提到的Jedis Pool的关键点来看看具体的代码实现

3.1 连接池的创建
 JedisPool pool = new JedisPool(new JedisPoolConfig(), hnp.getHost(), hnp.getPort(), 2000);

调用上面的代码就可以创建一个连接池了, 其中最关键的部分就是JedisPoolConfig对象的创建

// JedisPoolConfig.java

public class JedisPoolConfig extends GenericObjectPoolConfig {
  public JedisPoolConfig() {
    ...
    // 连接空闲的最小时间, 达到此值后空闲连接将会被移除. 负值(-1)表示不移除
    setMinEvictableIdleTimeMillis(60000);
    // "空闲链接"检测线程, 检测的周期, 毫秒数. 如果为负值, 表示不运行“检测线程”. 默认为-1
    setTimeBetweenEvictionRunsMillis(1000);
    ...
  }
}

在创建连接池的同时会创建idleObjects对象

// GenericObjectPool.java
public GenericObjectPool(PooledObjectFactory<T> factory,
            GenericObjectPoolConfig config) {

        ...other code...
        
        // 创建idleObjects对象
        idleObjects = new LinkedBlockingDeque<PooledObject<T>>(config.getFairness());

        setConfig(config);
   
        startEvictor(getTimeBetweenEvictionRunsMillis());
    }

在上面的代码中创建了idleObjects对象, 但是还有一句很关键的代码startEvictor(getTimeBetweenEvictionRunsMillis());.
没错,这就是启动自动回收空闲连接的代码!

// BaseGenericObjectPool.java

final void startEvictor(long delay) {
        synchronized (evictionLock) {
            if (null != evictor) {
                EvictionTimer.cancel(evictor);
                evictor = null;
                evictionIterator = null;
            }
            if (delay > 0) {
                evictor = new Evictor();
                EvictionTimer.schedule(evictor, delay, delay);
            }
        }
    }

至于保存刚创建出来的连接的allObjects对象是GenericObjectPool的成员变量

 private final Map<IdentityWrapper<T>, PooledObject<T>> allObjects =
        new ConcurrentHashMap<IdentityWrapper<T>, PooledObject<T>>();
3.2 获取一个连接
 Jedis jedis = pool.getResource();

调用上面的代码就可以获取一个连接了

获取连接最关键的就是borrowObject

// GenericObjectPool.java

public T borrowObject(long borrowMaxWaitMillis) throws Exception {
        assertOpen();

        ...other code...

        PooledObject<T> p = null;

        // 没有空闲连接的时候是否阻塞的等待连接
        boolean blockWhenExhausted = getBlockWhenExhausted();

        boolean create;
        long waitTime = System.currentTimeMillis();

        while (p == null) {
            create = false;
            // 没有空闲连接时等待
            if (blockWhenExhausted) {
                // 先从idleObjects队列中获取
                p = idleObjects.pollFirst();
                if (p == null) {
                  // 如果队列中没有空闲的连接, 则创建一个连接
                    p = create();
                    if (p != null) {
                        create = true;
                    }
                }
                // 如果连接创建失败, 则继续从idleObjects中阻塞的获取连接
                if (p == null) {
                    if (borrowMaxWaitMillis < 0) {
                     // 无限制的等待, 不会超时
                        p = idleObjects.takeFirst();
                    } else {
                     // 有超时时间的等待
                        p = idleObjects.pollFirst(borrowMaxWaitMillis,
                                TimeUnit.MILLISECONDS);
                    }
                }
                if (p == null) {
                    throw new NoSuchElementException(
                            "Timeout waiting for idle object");
                }
                if (!p.allocate()) {
                    p = null;
                }
            } else { // 没有空闲连接时直接抛出异常
                p = idleObjects.pollFirst();
                if (p == null) {
                    p = create();
                    if (p != null) {
                        create = true;
                    }
                }
                if (p == null) {
                    throw new NoSuchElementException("Pool exhausted");
                }
                if (!p.allocate()) {
                    p = null;
                }
            }
    ...other code...
        }

        updateStatsBorrow(p, System.currentTimeMillis() - waitTime);
   
   // 返回连接
        return p.getObject();
    }

上面是获取连接时的一个完整的流程, 包括有空闲连接时直接返回, 没有空闲连接时创建空闲连接, 连接创建失败后是继续阻塞等待(包括是否超时)还是直接抛出异常. 下面看一下是如何创建一个连接的

// GenericObjectPool.java

private PooledObject<T> create() throws Exception {

   // 判断当前已经创建的连接是否已经超过设置的最大连接数(默认是8)
        int localMaxTotal = getMaxTotal();
        long newCreateCount = createCount.incrementAndGet();
        if (localMaxTotal > -1 && newCreateCount > localMaxTotal ||
                newCreateCount > Integer.MAX_VALUE) {
            createCount.decrementAndGet();
            return null;
        }

        final PooledObject<T> p;
        try {
          // 创建一个到redis server的连接
            p = factory.makeObject();
        } catch (Exception e) {
            createCount.decrementAndGet();
            throw e;
        }

       ...other code...

        createdCount.incrementAndGet();
        // 将新创建的连接放入到allObjects中
        allObjects.put(new IdentityWrapper<T>(p.getObject()), p);
        // 返回新创建的连接
        return p;
 }

上面就是创建一个到redis server连接的过程. 但是上面没有深究是如何与redis server创建连接的, 因为这次介绍的主题是JedisPool, 所以客户端与redis server创建连接的具体细节会在之后的文章中介绍.

3.3 关闭一个连接

在使用完一个连接之后就要将一个连接关闭. 其实上面创建一个连接之后还有一个比较重要的步骤

  @Override
  public Jedis getResource() {
    // 获取连接
    Jedis jedis = super.getResource();
   // 将连接池放入到连接中, 这里这么做的目的其实就是为关闭连接的时候作准备的
    jedis.setDataSource(this);
    return jedis;
  }

调用下面的代码就可以关闭一个连接了

 jedis.close();

我们知道连接池的作用就是为了缓存连接而生的, 所以这里的关闭连接肯定不能是直接和redis server断开连接, 所以让我们看看这里的关闭连接到底是做了什么操作从而实现连接的复用

// Jedis.java
public void close() {
  
   // 如果连接池存在就调用连接池的返回资源(这里的资源就是连接)的方法
    if (dataSource != null) {
      if (client.isBroken()) {
        this.dataSource.returnBrokenResource(this);
      } else {
        this.dataSource.returnResource(this);
      }
    } else { // 如果连接池不存在就直接关闭连接
      client.close();
    }
  }

所以当关闭一个连接的时候如果连接存在其实是将资源还给了连接池. 其中最核心的方法就是returnObject

// GenericObjectPool.java
@Override
public void returnObject(T obj) {
   
   // 从allObjects中获取要归还的连接
        PooledObject<T> p = allObjects.get(new IdentityWrapper<T>(obj));
        
        ...other code...

        int maxIdleSave = getMaxIdle();
        // 如果idleObjects队列中连接的数据已经>=允许的最大连接数或者连接池已经关闭就直接销毁这个连接
        if (isClosed() || maxIdleSave > -1 && maxIdleSave <= idleObjects.size()) {
            try {
                destroy(p);
            } catch (Exception e) {
                swallowException(e);
            }
        } else { // 将连接放入到idleObjects队列中, 一旦将连接放入到idleObjects中如果连接长时间不被使用就会被自动回收
            if (getLifo()) { // 默认是使用last in first out机制
                idleObjects.addFirst(p);
            } else {
                idleObjects.addLast(p);
            }
            if (isClosed()) {
                // Pool closed while object was being added to idle objects.
                // Make sure the returned object is destroyed rather than left
                // in the idle object pool (which would effectively be a leak)
                clear();
            }
        }
        updateStatsReturn(activeTime);
    }

4. 总结

  • 使用了这么久的连接池自从看了Jedis Pool的源码之后才对连接池有了一个直观的认识, 之后可以看看数据库的连接池, 比较一下两个对于连接池实现的异同
  • Jedis的连接池使用上是对apache common pool2的一个实现, 有了Jedis Pool这个例子以后要是要实现自己的连接池也方便许多
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 162,475评论 4 372
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 68,744评论 2 307
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 112,101评论 0 254
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,732评论 0 221
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 53,141评论 3 297
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 41,049评论 1 226
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 32,188评论 2 320
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,965评论 0 213
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,716评论 1 250
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,867评论 2 254
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,341评论 1 265
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,663评论 3 263
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,376评论 3 244
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,200评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,990评论 0 201
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 36,179评论 2 285
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,979评论 2 279

推荐阅读更多精彩内容

  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,111评论 18 139
  • 1 Redis介绍1.1 什么是NoSql为了解决高并发、高可扩展、高可用、大数据存储问题而产生的数据库解决方...
    克鲁德李阅读 5,181评论 0 36
  • 1.1 资料 ,最好的入门小册子,可以先于一切文档之前看,免费。 作者Antirez的博客,Antirez维护的R...
    JefferyLcm阅读 16,973评论 1 51
  • 心的尖端划过什么 手的风与眼的黑 谁说的传说 我会在时光中爱你 愿这隔了千年的痴情 在透过你眼角看到的明月中流连 ...
    Longczx阅读 204评论 2 3