Pooling Technology for Java Data Persistence Series

In the last article Java Data Persistence Series JDBC In, we learned that creating a Connection using JDBC executes the corresponding SQL, but creating a Connection consumes a lot of resources, so JDBC is often not used directly in the Java persistence framework, but rather a database connection pool layer on it.

Today we will first understand the necessity and principle of pooling technology; then we will use Apache-common-Pool2 to implement a simple database connection pool; then through experiments, compare the performance data of simple connection pool, HikariCP, Druid and other database connection pools, analyze the key to achieve high performance database connection pool; and finally, analyze the specific source code implementation of Pool2.

Objects are not what you want, they can be what you want

You and my single dogs often joke that you can create an object and throw it away when you're finished.But some objects are expensive to create, such as threads, tcp connections, database connections, and so on.For these objects that take a long time to create or occupy a large amount of resources (such as threads, network connections, etc.), pooling is often introduced to manage them, reducing the number of objects created frequently, avoiding the time-consuming creation of objects, and improving performance.

Let's take the Database Connection object as an example and explain in detail the time and resources it takes to create it.Below is MySQL Driver's method of creating a Connection object. When the Connection method is called to create a Connection, it will communicate with MySQL over the network and establish a TCP connect ion, which is extremely time consuming.

connection = driver.connect(URL, props);

Simple database connection pooling using Apache-Common-Pool2

Next, let's take Apache-Common-Pool2 as an example to look at the abstract structure associated with pooling technology.

First, look at the three-dimensional ObjectPool, PooledObject, and PooledObjectFactory in Pool2 and explain them as follows:

  • An ObjectPool is an object pool that provides a series of functions such as borrowObject and returnObject.
  • PooledObject is an encapsulated class of pooled objects that records additional information, such as object state, object creation time, object idle time, object last used time, and so on.
  • PooledObjectFactory is a factory class responsible for managing the life cycle of pooled objects, providing a series of functions such as makeObject, destroyObject, activateObject, and validateObject.

All three of these have their underlying implementation classes, GenericObjectPool, DefaultPooledObject, and BasePooledObjectFactory.The impleDatasource in the previous section was implemented using the above classes.

First, you'll implement a factory class that inherits BasePooledObjectFactory, providing a specific way to manage the life cycle of pooled objects:

  • makeObject: Create a pooled object instance and encapsulate it using PooledObject.
  • validateObject: Verify that the object instance is secure or available, such as if Connection still saves the connection state.
  • ActateObject: Reinitialize the object instance returned by the pool, such as setting whether Connection defaults AutoCommit, etc.
  • passivateObject: Object instances returned to the pool are de-initialized, such as Rollback for uncommitted transactions in Connection.
  • destroyObject: Destroy an object instance that is no longer needed by the pool, such as calling its close method when Connection is no longer needed.

The specific implementation source is shown below, with detailed comments for each method.

public class SimpleJdbcConnectionFactory extends BasePooledObjectFactory<Connection> {
    ....
    @Override
    public Connection create() throws Exception {
        // Used to create pooled objects
        Properties props = new Properties();
        props.put("user", USER_NAME);
        props.put("password", PASSWORD);
        Connection connection = driver.connect(URL, props);
        return connection;
    }

    @Override
    public PooledObject<Connection> wrap(Connection connection) {
        // Encapsulate the pooled object and return DefaultPooledObject, where you can also return your own PooledObject implementation
        return new DefaultPooledObject<>(connection);
    }

    @Override
    public PooledObject<Connection> makeObject() throws Exception {
        return super.makeObject();
    }

    @Override
    public void destroyObject(PooledObject<Connection> p) throws Exception {
        p.getObject().close();
    }

    @Override
    public boolean validateObject(PooledObject<Connection> p) {
        // Use SELECT 1 or other sql statements to verify that Connection s are available, and the ConnUtils code details the items in Github
        try {
            ConnUtils.validateConnection(p.getObject(), this.validationQuery);
        } catch (Exception e) {
            return false;
        }
        return true;
    }


    @Override
    public void activateObject(PooledObject<Connection> p) throws Exception {
        Connection conn = p.getObject();
        // Object is loaned and needs to be initialized to set its autoCommit
        if (conn.getAutoCommit() != defaultAutoCommit) {
            conn.setAutoCommit(defaultAutoCommit);
        }
    }

    @Override
    public void passivateObject(PooledObject<Connection> p) throws Exception {
        // Objects are returned, recycled, or processed, such as rolling back uncommitted transactions
        Connection conn = p.getObject();
        if(!conn.getAutoCommit() && !conn.isReadOnly()) {
            conn.rollback();
        }
        conn.clearWarnings();
        if(!conn.getAutoCommit()) {
            conn.setAutoCommit(true);
        }

    }
}

Then you can use BasePool to get objects from the pool and return them to the pool.

Connection connection = pool.borrowObject(); // Get connection object instance from pool
Statement statement = connection.createStatement();
statement.executeQuery("SELECT * FROM activity");
statement.close();
pool.returnObject(connection); // Return connection object instances to pool after use

As mentioned above, we implemented a simple database connection pool using Apache Common Pool2.Next, let's use benchmark to verify the performance of this simple database connection pool, and then analyze the specific source implementation of Pool2.

performance test

Now that we have analyzed the principles and implementation of Pool2, we will test the performance of our proposed database connection pool by modifying Hikari-benchmark.The address of the modified benchmark is https://github.com/ztelur/HikariCP-benchmark.

You can see that the performance of the two database connection pools, Hikari and Druid, is optimal, while our simple database connection pool is at the end.In a subsequent series of articles, you'll analyze why Hikari and Druid perform better than our simple database.Let's first look at the implementation of a simple database connection pool.

Apache Common Pool2 Source Analysis

We will briefly analyze the source code (version 2.8.0) implementation of Pool 2, understand the basic principles of pooling technology, and lay the foundation for subsequent understanding and analysis of HikariCP and Druid. The three have common design ideas.

From the previous instances, we know that the state of the encapsulated instance PooledObject changes when objects are retrieved or returned from the object pool through borrowObject and returnObject. The code implementation follows the state change path of the PooledObject state machine.

The figure above is a state machine diagram of a PooledObject, with blue elements representing states and red representing the methods associated with the ObjectPool.The states of the PooledObject are IDLE, ALLOCATED, RETURNING, ABANDONED, INVALID, EVICTION, and EVICTION_RETURN_TO_HEAD (all states are defined in the PooledObjectState class, some of which are temporarily unused and are not described here).

It mainly involves three parts of the state change, namely the change of loan return state in 1, 2, 3, the change of marker discard delete state in 4, 5, and the change of detection and expulsion state in 6, 7, 8.Subsequent subsections will detail the state changes in these three sections.

During these state changes, not only are ObjectPool's methods involved, but also PooledObjectFactory's methods called for related operations.

The diagram above illustrates the methods of the PooledObjectFactory involved in the state change of the PooledObject.Following the previous description of the PooledObjectFactory method, it is easy to correspond.For example, when an object with number 1 is lent, invalidateObject is called to determine the availability of the object, and activeObject is called to initialize the default configuration of the object.

Changes in loan return status

We started with the borrowObject method of GenericObjectPool.This method can pass in a maximum wait time as a parameter, or use the configured default maximum wait time if not, with the source code for the borrowObject shown below (code is truncated for readability).

public T borrowObject(final long borrowMaxWaitMillis) throws Exception {
    // 1 Determine whether to call the removeAbandoned method for tag deletion based on abandonedConfig and other instrumentation
    ....
    PooledObject<T> p = null;
    // Blocking when object is temporarily unavailable
    final boolean blockWhenExhausted = getBlockWhenExhausted();
    while (p == null) {
        create = false;
        // 2 Get it from idleObjects queue first, pollFisrt is non-blocking
        p = idleObjects.pollFirst();
        // 3 Call the Create method to create a new object if none exists
        if (p == null) {
            p = create();
        }
        // 4 blockWhenExhausted is true, blocking is performed according to borrowMaxWaitMillis
        if (blockWhenExhausted) {
            if (p == null) {
                if (borrowMaxWaitMillis < 0) {
                    p = idleObjects.takeFirst(); // Block until you get the object
                } else {
                    p = idleObjects.pollFirst(borrowMaxWaitMillis,
                            TimeUnit.MILLISECONDS); // Block to maximum wait time or get object
                }
            }
        }
        // 5 Call allocate for state change
        if (!p.allocate()) {
            p = null;
        }
        if (p != null) {
            // 6 Call activateObject for default object initialization and destroy if problems occur 
            factory.activateObject(p);
            // 7 If TestOnBorrow is configured, validateObject is called for usability checking and destroy is called if it fails
            if (getTestOnBorrow()) {
                validate = factory.validateObject(p);
            }
        }
    }
    return p.getObject();
}

The borrowObject method does five main steps:

  • The first step is to determine if the removeAbandoned method is to be called for tag deletion based on the configuration, which is described in more detail in this subsequent subsection.
  • The second step is to try to get or create an object, consisting of 2, 3, 4 steps in the source code.
  • The third step is to invoke allocate to make a state change to an ALLOCATED state, such as the five steps in the source code.
  • The fourth step is to call the activateObject of the factory to initialize the object, and if an error occurs, call the destroy method to destroy the object, such as the six steps in the source code.
  • The fifth step is to invoke validateObject of factory for object availability analysis based on the TestOnBorrow configuration, and if it is not available, call the destroy method to destroy the object, such as step 7 in the source code.

Let's make a detailed analysis of the second step.idleObjects are LinkedBlockingDeque objects that store all IDLE states (and possibly EVICTION states) of PooledObjects.In the second step, you first call its pollFirst method to get the PooledObject from the queue header, and if not, call the Create method to create a new one.

create may also not be created successfully, so when blockWhenExhausted is true, the object is not fetched and needs to be blocked all the time, so call the takeFirst or pollFirst(time) method based on the maximum wait time borrowMaxWaitMillis to get blocked; when blockWhenExhausted is false, throw an exception directly.

The create method determines whether new objects should be created under current conditions, primarily to prevent the number of objects created from exceeding the maximum number of pool objects.If you can create a new object, call the makeObject method of PooledObjectFactory to create the new object, and then use the testOnCreate configuration to determine whether to call the validateObject method for validation, as shown below.

private PooledObject<T> create() throws Exception {
    int localMaxTotal = getMaxTotal(); // Get maximum number of pool objects
    final long localStartTimeMillis = System.currentTimeMillis();
    final long localMaxWaitTimeMillis = Math.max(getMaxWaitMillis(), 0); // Get maximum wait time
    Boolean create = null;
    // Wait until the create is assigned, true means to create a new object, false means not to create
    while (create == null) {
        synchronized (makeObjectCountLock) {
            final long newCreateCount = createCount.incrementAndGet();
            if (newCreateCount > localMaxTotal) {
                // The pool is full or creating enough objects to reach the maximum number.
                createCount.decrementAndGet();
                if (makeObjectCount == 0) {
                    // No other makeObject methods are currently called, returning false directly
                    create = Boolean.FALSE;
                } else {
                    // There are other makeObject methods currently being called, but they may fail, so wait a while and try again
                    makeObjectCountLock.wait(localMaxWaitTimeMillis);
                }
            } else {
                // The pool is not full to create an object.
                makeObjectCount++;
                create = Boolean.TRUE;
            }
        }

        // Executing more than maxWaitTimeMillis returns false
        if (create == null &&
            (localMaxWaitTimeMillis > 0 &&
                System.currentTimeMillis() - localStartTimeMillis >= localMaxWaitTimeMillis)) {
            create = Boolean.FALSE;
        }
    }
    // If create is false, return NULL
    if (!create.booleanValue()) {
        return null;
    }

    final PooledObject<T> p;
    try {
        // Call factory's makeObject for object creation and call the validateObject method according to the testOnCreate configuration
        p = factory.makeObject();
        if (getTestOnCreate() && !factory.validateObject(p)) {
            // There's a problem with the code here. Didn't the object that failed the check be destroyed?
            createCount.decrementAndGet();
            return null;
        }
    } catch (final Throwable e) {
        createCount.decrementAndGet();
        throw e;
    } finally {
        synchronized (makeObjectCountLock) {
            // Reduce makeObjectCount
            makeObjectCount--;
            makeObjectCountLock.notifyAll();
        }
    }
    allObjects.put(new IdentityWrapper<>(p.getObject()), p);
    return p;
}

It is important to note that the object created by the create method does not join the idleObjects queue for the first time, and it will not join the queue until the returnObject method is called after use.

Next, let's look at the implementation of the returnObject method.This method mainly does six steps:

  • The first step is to call the markReturningState method to change the state to RETURNING.
  • The second step is to invoke the validateObject method of PooledObjectFactory based on the testOnReturn configuration for usability checking.If the check fails, destroy is called to consume the object, then ensure Idle is called to ensure that IDLE state objects are available in the pool, and if not, the Create method is called to create a new object.
  • The third step is to invoke the passivateObject method of the PooledObjectFactory for the de-initialization operation.
  • The fourth step is to call deallocate to change the state to IDLE.
  • The fifth step is to detect if the maximum number of IDLE objects has been exceeded and, if so, destroy the current object.
  • The sixth step is to place the object at the beginning or end of the queue according to the LIFO (last in, first out) configuration.
public void returnObject(final T obj) {
    final PooledObject<T> p = allObjects.get(new IdentityWrapper<>(obj));
    // 1 Convert state to RETURNING
    markReturningState(p);

    final long activeTime = p.getActiveTimeMillis();
    // 2 Check availability of instances based on configuration
    if (getTestOnReturn() && !factory.validateObject(p)) {
        destroy(p);
        // Because an object was deleted, you need to make sure there are objects in the pool, and if you don't change the method a new object will be created
        ensureIdle(1, false); 
        updateStatsReturn(activeTime);
        return;
    }
    // 3 Call passivateObject to de-initialize the object.
    try {
        factory.passivateObject(p);
    } catch (final Exception e1) {
         .... // The same operation as the previous validateObject check failed.
    }
    // 4 Change status to IDLE
    if (!p.deallocate()) {
        throw new IllegalStateException(
                "Object has already been returned to this pool or is invalid");
    }

    final int maxIdleSave = getMaxIdle();
    // 5 Destroy if the maximum number of IDLE s is exceeded
    if (isClosed() || maxIdleSave > -1 && maxIdleSave <= idleObjects.size()) {
        .... // The same operation as the validateObject check above failed.
    } else {
        // 6 Depending on the LIFO configuration, place the returned objects at the beginning or end of the queue.This source code is misspelled.
        if (getLifo()) {
            idleObjects.addFirst(p);
        } else {
            idleObjects.addLast(p);
        }
    }
    updateStatsReturn(activeTime);
}

The following figure illustrates two scenarios for the sixth step of queuing, one at the top of the queue when LIFO is true and the other at the end when LIFO is false.Choose different scenarios based on different pooled objects.However, placing it at the tail avoids concurrency hot spots because both borrowed and returned objects need to operate on the queue header and require concurrency control.

Tag Delete State Change

The tag deletion state change operation is mainly implemented through removeAbandoned, which checks whether the lent objects need to be deleted to prevent the pool objects from being exhausted due to long periods of unused lend or return.

ReveAbandoned may be called under AbandonedConfig when a borrowObject or evict method that detects a removal object is executed.

public T borrowObject(final long borrowMaxWaitMillis) throws Exception {
    
    final AbandonedConfig ac = this.abandonedConfig;
    // When removeAbandonedOnBorrow is configured and the current number of idle objects is less than 2, the number of active objects is only 3 less than the maximum number of objects.
    if (ac != null && ac.getRemoveAbandonedOnBorrow() &&
            (getNumIdle() < 2) &&
            (getNumActive() > getMaxTotal() - 3) ) {
        removeAbandoned(ac);
    }
    ....
}

public void evict() throws Exception {
    ....
    final AbandonedConfig ac = this.abandonedConfig;
        // Set removeAbandonedOnMaintenance
        if (ac != null && ac.getRemoveAbandonedOnMaintenance()) {
            removeAbandoned(ac);
        }
}

ReveAbandoned uses a typical tag deletion strategy: the tag phase traverses all objects first, changes their state to ABANDONED if the object is ALLOCATED and the last usage time has exceeded the timeout, and joins the deletion queue; the deletion phase traverses the deletion queue, calling the invalidateObject method in turn to delete and destroy the pairsElephant.

private void removeAbandoned(final AbandonedConfig ac) {
    // Collect objects that require abandoned
    final long now = System.currentTimeMillis();
    // 1 Calculate timeout based on configured time
    final long timeout =
            now - (ac.getRemoveAbandonedTimeout() * 1000L);
    final ArrayList<PooledObject<T>> remove = new ArrayList<>();
    final Iterator<PooledObject<T>> it = allObjects.values().iterator();
    while (it.hasNext()) {
        final PooledObject<T> pooledObject = it.next();
        // 2 Traverse through all objects if it is already assigned and the object's last usage time is less than the timeout
        synchronized (pooledObject) {
            if (pooledObject.getState() == PooledObjectState.ALLOCATED &&
                    pooledObject.getLastUsedTime() <= timeout) {
                // 3 Change object state to ABANDONED and join delete queue
                pooledObject.markAbandoned();
                remove.add(pooledObject);
            }
        }
    }

    // 4 Traverse Delete Queue
    final Iterator<PooledObject<T>> itr = remove.iterator();
    while (itr.hasNext()) {
        final PooledObject<T> pooledObject = itr.next();
        // 5 Call invalidateObject method to delete object
        invalidateObject(pooledObject.getObject());
    }
}

The invalidateObject method directly calls the destroy method, which is also repeated in the source code analysis above. It mainly performs four steps:

  • 1 Change object state to INVALID.
  • 2 Delete objects from queues and collections.
  • 3 Call the destroyObject method of PooledObjectFactory to destroy the object.
  • 4 Update statistics
private void destroy(final PooledObject<T> toDestroy) throws Exception {
    // 1 Change status to INVALID
    toDestroy.invalidate();
    // 2 Delete from Queue and Pool
    idleObjects.remove(toDestroy);
    allObjects.remove(new IdentityWrapper<>(toDestroy.getObject()));
    // 3 Call destroyObject to recycle objects
    try {
        factory.destroyObject(toDestroy);
    } finally {
        // 4 Update statistics
        destroyedCount.incrementAndGet();
        createCount.decrementAndGet();
    }
}

Detecting Exclusion State Changes

Detecting the change of the state of the removal is mainly operated by the evict method and is done independently in the background thread. It mainly detects whether the idle objects in the IDLE state in the pool need to be removed, and the timeout is configured by EvictionConfig.

Evictor, defined in BaseGenericObjectPool, is essentially a fixed-time task defined by java.util.TimerTask.

final void startEvictor(final long delay) {
    synchronized (evictionLock) {
        if (delay > 0) {
            // Timed evictor thread execution
            evictor = new Evictor();
            EvictionTimer.schedule(evictor, delay, delay);
        }
    }
}

The evict method is invoked in the Evictor thread, which essentially traverses through all IDLE objects and then performs a detect-and-remove operation on each object, with the following source code:

  • Call the startEvictionTest method to change the state to EVICTED.
  • The expulsion policy and object timeout determine whether to expel or not.
  • If you need to be expelled, call the destroy method to destroy the object.
  • If the testWhileIdle is set, the validateObject of the PooledObject is called for usability checking.
  • Call endEvictionTest to change the state to IDLE.
public void evict() throws Exception {
    if (idleObjects.size() > 0) {
        ....
        final EvictionPolicy<T> evictionPolicy = getEvictionPolicy();
        synchronized (evictionLock) {
            for (int i = 0, m = getNumTests(); i < m; i++) {
                // 1 Traverse all idle objects
                try {
                    underTest = evictionIterator.next();
                } catch (final NoSuchElementException nsee) {
                }
                // 2 Call startEvictionTest to change state to EVICTED
                if (!underTest.startEvictionTest()) {
                    continue;
                }
                // 3 Determine whether to remove based on the removal strategy
                boolean evict = evictionPolicy.evict(evictionConfig, underTest,
                        idleObjects.size());

                if (evict) {
                    // 4 for expulsion
                    destroy(underTest);
                    destroyedByEvictorCount.incrementAndGet();
                } else {
                    // 5 Conduct usability detection if detection is required
                    if (testWhileIdle) {
                        factory.activateObject(underTest);
                        factory.validateObject(underTest));
                        factory.passivateObject(underTest);
                        }
                    // 5 Change status to IDLE
                    if (!underTest.endEvictionTest(idleObjects)) {
                    }
                }
            }
        }
    }
    .... // abandoned related operations
}

Postnote

We will analyze the implementation of Hikari and Druid database connection pools in the future, so please pay more attention.

Personal blog, welcome to play

Reference resources

Tags: Java Database Apache Druid

Posted on Mon, 03 Feb 2020 21:32:17 -0500 by RealDrift