Analysis of the implementation principle of jedis connection pool jedisPool

 foreword

jedisPool is based on the abstract object pool (objectPool) technology of Apache-commons-pool2. In jedisPool, a connection is a jedisObject object instance. The core of jedisPool is: according to the set poolConfig, around the connection pool container LinkedBlockingDeque, in the related actions, do the logic of addition and deletion. Therefore, the following mainly describes the behavior of key configurations and key methods, which can basically spy on the realization of jedisPool.

Example of use

/**
 * @author : kl (http://kailing.pub)
 * @since : 2022-04-26 12:37
 */
public class KLMain1 {
    public static void main(String[] args) {
        JedisPoolConfig config = new JedisPoolConfig();
        config.setMinIdle(10);

        JedisPool pool = new JedisPool(config,Protocol.DEFAULT_HOST, Protocol.DEFAULT_PORT);
        try (Jedis jedis = pool.getResource()) {
            jedis.set("foo", "kl");
            System.out.println(jedis.get("foo"));
        }
    }
}

Overview of key configs

The jedis connection pool configuration class is GenericObjectPoolConfig and the subclass JedisPoolConfig. Generally, the subclass JedisPoolConfig is used to construct the connection pool parameters. It will initialize some configurations that manage the life cycle of the redis connection by default.

pool capacity

Control the pool size and configure the strategy for acquiring connections

name
Defaults 
Overview
maxTotal 8 Maximum number of connections
maxIdle 8 Maximum number of idle connections
minIdle 0 Minimum number of idle connections

lifo

true The policy when returning the connection, which is put back to the head of the queue by default
blockWhenExhausted true When getting a connection, whether to block waiting for a connection when the connection pool is empty
maxWaitDuration -1 It only takes effect when blockWhenExhausted = true, indicating how long to wait for blocking. -1 means to block until a connection is acquired
testOnCreate false Whether to check the validity of the connection when creating the connection, if the connection is invalid, return null
testOnBorrow false Whether to check the validity of the connection when renting the connection, the invalid connection will be directly ecstatic, and try to continue to obtain
testOnReturn false Whether to check the validity of the connection when returning the connection, the invalid link will be destroyed directly, and a new connection will be created and returned

life cycle

Controls whether to enable the thread named commons-pool-evictor for idle connection, invalid connection detection. and a policy to configure connection detection

name
GenericObjectPoolConfig default value
jedisPoolConfig default value
Overview

testWhileIdle

false true Whether to detect invalid links when the eviction line is enabled

durationBetweenEvictionRuns

-1 30s The time interval between each detection of the eviction thread, in milliseconds. -1 means do not start the eviction thread

numTestsPerEvictionRun

3 -1 How many links to check each time, -1 means check all

minEvictableIdleDuration

30min 60s When eviction connection, connection minimum idle time to live

softMinEvictableIdleDuration

-1 -1 Minimum idle time to live during soft eviction, -1 means intMax

key logic

The following codes are all in the GenericObjectPool class. Here are examples of key method definitions and configuration items directly used by each method. After reading this part, you can know how the parameters usually configured work. The source code posted below has been deleted due to space reasons, and only the key logic parts are retained and highlighted.

method definition
Configuration items used
Overview
PooledObject<T> create() maxTotal、testOnCreate Create a connection
T borrowObject(final long borrowMaxWaitMillis) blockWhenExhausted、maxWaitDuration、testOnBorrow leased connection
void returnObject(final T obj) maxIdle、lifo、testOnReturn return connection
void evict() testWhileIdle、durationBetweenEvictionRuns、numTestsPerEvictionRun、minEvictableIdleDuration、softMinEvictableIdleDuration、minIdle evict connection
void ensureMinIdle() lifo、minIdle keep minimum idle connections
void addObject() lifo Add a connection to the connection pool

create

    private PooledObject<T> create() throws Exception {
        int localMaxTotal = getMaxTotal();
        if (localMaxTotal < 0) {
            localMaxTotal = Integer.MAX_VALUE;
        }
        Boolean create = null;
        while (create == null) {
            synchronized (makeObjectCountLock) {
                final long newCreateCount = createCount.incrementAndGet();
                if (newCreateCount > localMaxTotal) {
                    // 池当前处于满负荷状态或正在制造足够的新对象以使其达到满负荷状态。
                    createCount.decrementAndGet();
                    if (makeObjectCount == 0) {
                        // 没有正在进行的 makeObject() 调用,因此池已满。不要尝试创建新对象。
                        create = Boolean.FALSE;
                    } else {
                        // 正在进行的 makeObject() 调用可能会使池达到容量。这些调用也可能会失败,因此请等待它们完成,然后重新测试池是否已满负荷
                        makeObjectCountLock.wait(localMaxWaitTimeMillis);
                    }
                } else {
                    // The pool is not at capacity. Create a new object.
                    makeObjectCount++;
                    create = Boolean.TRUE;
                }
            }

            // Do not block more if maxWaitTimeMillis is set.
            if (create == null &&
                    (localMaxWaitTimeMillis > 0 &&
                            System.currentTimeMillis() - localStartTimeMillis >= localMaxWaitTimeMillis)) {
                create = Boolean.FALSE;
            }
        }

        if (!create.booleanValue()) {
            return null;
        }

        final PooledObject<T> p;
        try {
            p = factory.makeObject();
            if (getTestOnCreate() && !factory.validateObject(p)) {
                createCount.decrementAndGet();
                return null;
            }
        }
        //...省略
        return p;
    }

Creating a connection is a low-level method, which is used in leasing a connection, returning a connection, and keeping an idle connection. Before creating a connection, it will judge whether the current connection reaches maxTotal. If it reaches maxTotal and no connection is being created at this time, it means that the pool is full and returns null. If the connection is successfully created, according to the configuration of testOnCreate, if testOnCreate= true, the disconnection is available. If the connection is invalid, null is still returned. 

borrowObject

   public T borrowObject(final Duration borrowMaxWaitDuration) throws Exception {
        assertOpen();

        final AbandonedConfig ac = this.abandonedConfig;
        if (ac != null && ac.getRemoveAbandonedOnBorrow() && (getNumIdle() < 2) &&
                (getNumActive() > getMaxTotal() - 3)) {
            removeAbandoned(ac);
        }

        PooledObject<T> p = null;

        // Get local copy of current config so it is consistent for entire
        // method execution
        final boolean blockWhenExhausted = getBlockWhenExhausted();

        boolean create;
        final long waitTimeMillis = System.currentTimeMillis();

        while (p == null) {
            create = false;
            p = idleObjects.pollFirst();
            if (p == null) {
                p = create();
                if (p != null) {
                    create = true;
                }
            }
            if (blockWhenExhausted) {
                if (p == null) {
                    if (borrowMaxWaitDuration.isNegative()) {
                        p = idleObjects.takeFirst();
                    } else {
                        p = idleObjects.pollFirst(borrowMaxWaitDuration);
                    }
                }
                if (p == null) {
                    throw new NoSuchElementException(appendStats(
                            "Timeout waiting for idle object, borrowMaxWaitDuration=" + borrowMaxWaitDuration));
                }
            } else if (p == null) {
                throw new NoSuchElementException(appendStats("Pool exhausted"));
            }
            if (!p.allocate()) {
                p = null;
            }

            if (p != null) {
                try {
                    factory.activateObject(p);
                } catch (final Exception e) {
                    try {
                        destroy(p, DestroyMode.NORMAL);
                    } catch (final Exception e1) {
                        // Ignore - activation failure is more important
                    }
                    p = null;
                    if (create) {
                        final NoSuchElementException nsee = new NoSuchElementException(
                                appendStats("Unable to activate object"));
                        nsee.initCause(e);
                        throw nsee;
                    }
                }
                if (p != null && getTestOnBorrow()) {
                    boolean validate = false;
                    Throwable validationThrowable = null;
                    try {
                        validate = factory.validateObject(p);
                    } catch (final Throwable t) {
                        PoolUtils.checkRethrow(t);
                        validationThrowable = t;
                    }
                    if (!validate) {
                        try {
                            destroy(p, DestroyMode.NORMAL);
                            destroyedByBorrowValidationCount.incrementAndGet();
                        } catch (final Exception e) {
                            // Ignore - validation failure is more important
                        }
                        p = null;
                        if (create) {
                            final NoSuchElementException nsee = new NoSuchElementException(
                                    appendStats("Unable to validate object"));
                            nsee.initCause(validationThrowable);
                            throw nsee;
                        }
                    }
                }
            }
        }

        updateStatsBorrow(p, Duration.ofMillis(System.currentTimeMillis() - waitTimeMillis));

        return p.getObject();
    }

The leased connection is obtained from the head of the connection pool container list. If it is null, create() is called to create the connection. If it is still null, it is determined whether to wait for the connection according to the configuration of blockWhenExhausted=true, and maxWaitDuration=-1 to wait all the time. Otherwise, wait for the time of maxWaitDuration. If the connection is not obtained after waiting, the exception of [ Timeout waiting for idle object ] will be thrown. If blockWhenExhausted=false , when there is no available connection in the connection pool, the connection pool is full [ Pool exhausted ] exception will be thrown immediately. After the connection is successfully obtained, if testOnBorrow=true, continue to judge whether the connection is valid, the invalid link is directly destroyed, and repeat the above operations until an available connection is obtained.

returnObject

   public void returnObject(final T obj) {
        final PooledObject<T> p = getPooledObject(obj);

        if (p == null) {
            if (!isAbandonedConfig()) {
                throw new IllegalStateException(
                        "Returned object not currently part of this pool");
            }
            return; // Object was abandoned and removed
        }

        markReturningState(p);

        final Duration activeTime = p.getActiveDuration();

        if (getTestOnReturn() && !factory.validateObject(p)) {
            try {
                destroy(p, DestroyMode.NORMAL);
            } catch (final Exception e) {
                swallowException(e);
            }
            try {
                ensureIdle(1, false);
            } catch (final Exception e) {
                swallowException(e);
            }
            updateStatsReturn(activeTime);
            return;
        }

        try {
            factory.passivateObject(p);
        } catch (final Exception e1) {
            swallowException(e1);
            try {
                destroy(p, DestroyMode.NORMAL);
            } catch (final Exception e) {
                swallowException(e);
            }
            try {
                ensureIdle(1, false);
            } catch (final Exception e) {
                swallowException(e);
            }
            updateStatsReturn(activeTime);
            return;
        }

        if (!p.deallocate()) {
            throw new IllegalStateException(
                    "Object has already been returned to this pool or is invalid");
        }

        final int maxIdleSave = getMaxIdle();
        if (isClosed() || maxIdleSave > -1 && maxIdleSave <= idleObjects.size()) {
            try {
                destroy(p, DestroyMode.NORMAL);
            } catch (final Exception e) {
                swallowException(e);
            }
            try {
                ensureIdle(1, false);
            } catch (final Exception e) {
                swallowException(e);
            }
        } else {
            if (getLifo()) {
                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);
    }

Returning a connection is to put the connection object back into the connection pool container. When returning the connection, if testOnReturn=true, the validity of the connection is verified, the invalid link is directly destroyed and at least one idle connection is ensured. Then judge whether maxIdle is full, destroy the current connection if it is full, and ensure that there is at least one idle connection, otherwise the connection will be returned to the head of the linked list according to lifo=true, otherwise it will be returned to the tail of the linked list. Here, lifo is used to control whether the leased connection is the latest connection or the oldest connection. Generally, the latest connection is used by default.

evict

    public void evict() throws Exception {
        assertOpen();

        if (!idleObjects.isEmpty()) {

            PooledObject<T> underTest = null;
            final EvictionPolicy<T> evictionPolicy = getEvictionPolicy();

            synchronized (evictionLock) {
                final EvictionConfig evictionConfig = new EvictionConfig(
                        getMinEvictableIdleDuration(),
                        getSoftMinEvictableIdleDuration(),
                        getMinIdle());

                final boolean testWhileIdle = getTestWhileIdle();

                for (int i = 0, m = getNumTests(); i < m; i++) {
                    if (evictionIterator == null || !evictionIterator.hasNext()) {
                        evictionIterator = new EvictionIterator(idleObjects);
                    }
                    if (!evictionIterator.hasNext()) {
                        // Pool exhausted, nothing to do here
                        return;
                    }

                    try {
                        underTest = evictionIterator.next();
                    } catch (final NoSuchElementException nsee) {
                        // Object was borrowed in another thread
                        // Don't count this as an eviction test so reduce i;
                        i--;
                        evictionIterator = null;
                        continue;
                    }

                    if (!underTest.startEvictionTest()) {
                        // Object was borrowed in another thread
                        // Don't count this as an eviction test so reduce i;
                        i--;
                        continue;
                    }

                    // User provided eviction policy could throw all sorts of
                    // crazy exceptions. Protect against such an exception
                    // killing the eviction thread.
                    boolean evict;
                    try {
                        evict = evictionPolicy.evict(evictionConfig, underTest,
                                idleObjects.size());
                    } catch (final Throwable t) {
                        // Slightly convoluted as SwallowedExceptionListener
                        // uses Exception rather than Throwable
                        PoolUtils.checkRethrow(t);
                        swallowException(new Exception(t));
                        // Don't evict on error conditions
                        evict = false;
                    }

                    if (evict) {
                        destroy(underTest, DestroyMode.NORMAL);
                        destroyedByEvictorCount.incrementAndGet();
                    } else {
                        if (testWhileIdle) {
                            boolean active = false;
                            try {
                                factory.activateObject(underTest);
                                active = true;
                            } catch (final Exception e) {
                                destroy(underTest, DestroyMode.NORMAL);
                                destroyedByEvictorCount.incrementAndGet();
                            }
                            if (active) {
                                boolean validate = false;
                                Throwable validationThrowable = null;
                                try {
                                    validate = factory.validateObject(underTest);
                                } catch (final Throwable t) {
                                    PoolUtils.checkRethrow(t);
                                    validationThrowable = t;
                                }
                                if (!validate) {
                                    destroy(underTest, DestroyMode.NORMAL);
                                    destroyedByEvictorCount.incrementAndGet();
                                    if (validationThrowable != null) {
                                        if (validationThrowable instanceof RuntimeException) {
                                            throw (RuntimeException) validationThrowable;
                                        }
                                        throw (Error) validationThrowable;
                                    }
                                } else {
                                    try {
                                        factory.passivateObject(underTest);
                                    } catch (final Exception e) {
                                        destroy(underTest, DestroyMode.NORMAL);
                                        destroyedByEvictorCount.incrementAndGet();
                                    }
                                }
                            }
                        }
                        if (!underTest.endEvictionTest(idleObjects)) {
                            // TODO - May need to add code here once additional
                            // states are used
                        }
                    }
                }
            }
        }
        final AbandonedConfig ac = this.abandonedConfig;
        if (ac != null && ac.getRemoveAbandonedOnMaintenance()) {
            removeAbandoned(ac);
        }
    }

The code posted here is not the whole picture, only the key logic is intercepted. When jedisPool is initialized, it will start a thread scheduling named commons-pool-evictor according to the configuration of durationBetweenEvictionRuns. If durationBetweenEvictionRuns=-1, it will not be enabled, and all the logic of evict will not run, otherwise the eviction logic will be run every durationBetweenEvictionRuns time. numTestsPerEvictionRun controls how many connections are checked each time. Each time to check whether the current connection should be expelled, there are two strategies as follows, one of which can be satisfied:

  • Policy A: The configured softMinEvictableIdleDuration is less than the idle time of the current connection [and] the configured minIdle is less than the total number of idle connections
  • Strategy B: The configured minEvictableIdleDuration is less than the idle time of the current connection

Generally, under the default parameters, strategy A will not be triggered. It is easy to trigger strategy B. If the current connection is evicted, continue to the next connection judgment. Otherwise, if testWhileIdle=true, continue to judge the validity of the current connection, the invalid link will be destroyed and the connection pool will be removed. After a round of detection, ensureMinIdle() will be called to ensure the minimum idle connection in the connection pool.

ensureMinIdle

    private void ensureIdle(final int idleCount, final boolean always) throws Exception {
        if (idleCount < 1 || isClosed() || (!always && !idleObjects.hasTakeWaiters())) {
            return;
        }

        while (idleObjects.size() < idleCount) {
            final PooledObject<T> p = create();
            if (p == null) {
                // Can't create objects, no reason to think another call to
                // create will work. Give up.
                break;
            }
            if (getLifo()) {
                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();
        }
    }

ensureMinIdle will eventually call the ensureIdle() method, and the idleCount input parameter is the configured minIdle. Here, the current idle connection will be compared with the incoming idleCount until the idle connection of the connection pool reaches the idleCount value.

addObject

    public void addObject() throws Exception {
        assertOpen();
        if (factory == null) {
            throw new IllegalStateException("Cannot add objects without a factory.");
        }
        addIdleObject(create());
    }

Create a connection and add it to the connection pool. This method is generally used for connection pool initialization. For example, the connection pool is preheated in advance.

Epilogue

  • Most people generally only focus on the configuration of connection pool capacity, ignoring the configuration of connection life cycle

Including me, there has always been a wrong perception that the connection pool will ensure the minIdle value, and as long as the currently applied connection is less than the minIdle connection pool, it will not scale to create a new connection. It was not until I looked at the source code that I found that as long as the eviction connection thread is enabled, as long as the idle survival time is greater than the configured value minEvictableIdleDuration, it will be expelled, and then a new connection will be rebuilt according to minIdle. And because the minEvictableIdleDuration of jedisPoolConfig defaults to 60s, when the minIdle configuration is much larger than the actual need, because the default connection acquisition strategy is lifo (always take the latest connection), it will always get the newly created connection, destroy it and continue to create it. in cycle. When the traffic suddenly increases, the number of connections in minIdle may not be kept in the connection pool, and a large number of connections need to be created suddenly, which will inevitably have a certain impact on the application. It is recommended here that when the eviction thread is enabled, the value of minEvictableIdleDuration is set slightly larger, or -1 directly, so that the created connection is kept alive.

 
{{o.name}}
{{m.name}}

Guess you like

Origin my.oschina.net/klblog/blog/5520235
Recommended