preface

HikariCP is already being used by many companies and has become the default connection pool for SpringBoot. With SpringBoot and microservices, HikariCP is set to become widespread.

Chen brings you from the source code perspective to analyze why HikariCP can be favored by Spring Boot, the article directory is as follows:

Zero, class diagram, and flowchart

Before we start, let’s understand the interaction process between classes when HikariCP obtains a connection, so that we can read the detailed process below.

Class interaction when getting a connection:

1. Main Process 1: Obtaining the connection process

HikariCP obtains a connection from the getConnection method in HikariDataSource. Here’s how this method works:

The above is the flowchart when HikariCP obtains the connection. As can be seen from Figure 1, each datasource object holds a HikariPool object, which is denoted as pool. The initialized datasource object pool is empty. GetConnection instantiates the pool attribute (see main process 1). When initializing the pool attribute, pass the config attribute from the current datasource to the pool attribute (sealed). The getConnection method is then called against the pool object (refer to Process 1.1), and the connection object is returned upon success.

Main Process 2: Initializing pool objects

This process is used to initialize the entire connection pool. This process initializes all properties in the connection pool. The main processes are shown in the figure above.

  1. usingconfigInitializes the various connection pool properties and generates a connection pool forProduction physical connectionThe data sourceDriverDataSource
  2. Initialize the core class that holds the connection objectconnectionBag
  3. Initializes an object of type deferred task thread poolhouseKeepingExecutorServiceFor subsequent delayed/scheduled tasks (such as connection leak check delayed tasks, seeThe process of 2.2As well asMain process 4In addition tomaxLifeTimeAfter the active recycling closed connection is also by the object to perform, this process can refer toMain process 3)
  4. Warm up the connection pool, HikariCP will be in the processcheckFailFastTo initialize a connection object into the pool, of course, triggering the process must be guaranteedinitializationTimeout > 0(default value 1), this configuration property represents the amount of time left for the warm-up operation (default value 1 does not occur if the warm-up fails). withDruidthroughinitialSizeTo control the number of preheated connection objects, HikariCP preheats only one connection object into the pool.
  5. Initialize a thread pool objectaddConnectionExecutorFor subsequent expansion of the connection object
  6. Initialize a thread pool objectcloseConnectionExecutor, which is used to close some connection objects. How to trigger the closing task? You can refer toProcess 1.1.2

Process 1.1: Obtain the connection object through HikariPool

As you can see from the original structure diagram, each HikariPool maintains a ConcurrentBag object, which is used to hold connection objects. As you can see from the figure above, HikariPool getConnection actually gets the connection from ConcurrentBag (call its Borrow method to get the connection), and checks the long connection section, unlike Druid, Live here long connection to check connection objects are not marked as “has been discarded,” as long as the distance from the last use of more than 500 ms check every time out (500 ms is the default value, can be configured. Com zaxxer. Hikari. AliveBypassWindowMs system parameters to control). Emmmm, HikariCP checks the activity of long connections more frequently, but the concurrency performance is still better than Druid, indicating that frequent long connection checks are not the key to the performance of the connection pool.

The author’s Spring Boot column and Mybatis column have been completed, follow the public number [code monkey technology column] reply keyword Spring Boot advanced, Mybatis advanced obtain.

This is due to the fact that HikariCP does not load the CPU as much as other connection pools at high concurrency. Even Druid locks the connection when it is acquired, generated, and returned. As you can see from the previous Druid article, connection pool resources in Druid are shared by multiple threads, and there will inevitably be lock contention. Lock contention means that thread state changes frequently, and thread state changes frequently means that CPU context switches will also be frequent.

Back to process 1.1, if the connection is empty, an error will be reported directly. If the connection is not empty, the corresponding check will be performed. If the check passes, it will be wrapped as a ConnectionProxy object and returned to the business side. The process triggers ConcurrentBag’s Remove method to discard the connection, and then hands the actual driver connection over to the closeConnectionExecutor thread pool, asynchronously closing the driver connection).

Iv. Process 1.1.1: Connection to live

Take over from process 1.1 and see how it works. First, the validation method (note that the connection object that this method accepts is not a poolEntry, but the actual driver connection object that poolEntry holds), as we saw in Druid earlier. Druid specifies whether or not ping is enabled in the driver, but HikariCP specifies whether or not ping is enabled based on whether or not connectionTestQuery is configured:

this.isUseJdbc4Validation = config.getConnectionTestQuery() == null;
Copy the code

Therefore, it is not recommended to configure this option if the driver is not a very early version. Otherwise, the createStatement+ Excute mode is used, which is less efficient than simply sending heartbeat data through ping.

In addition, the driver’s connection object resets networkTimeout to validationTimeout, indicating the timeout period of a validation. Why do you want to set this property again? When using ping, there is no setQueryTimeout like statement, so only the network communication timeout time can be controlled. This time can be controlled by the JDBC connection parameter socketTimeout:

JDBC: mysql: / / 127.0.0.1:3306 / XXX? socketTimeout=250Copy the code

This value is eventually assigned to the HikariCP networkTimeout field, which is why this field is used in the last step to restore the driver connection timeout property; Speaking of which, at the end of the day why do we have to restore again there? This is easy to understand, because at the end of validation, while the connection object is still alive, its networkTimeout value is still equal to validationTimeout (unexpected). Obviously, you need to recover the cost value before taking it out for use. That’s the networkTimeout property in HikariCP.

V. Process 1.1.2: Close the Connection Object

This process simply means that the connection object is removed from the ConnectionBag and the actual physical connection is handed over to a thread pool for asynchronous execution. This pool is the thread pool closeConnectionExecutor that is initialized when the pool is initialized in main process 2, and then the actual connection shutdown begins in the asynchronous task. Since one connection is actively closed, one connection is lost, so an expansion of the connection pool (see main process 5) is triggered.

Vi. Process 2.1: HikariCP monitoring setting

With so many different from Druid as monitoring index, HikariCP will expose a matter of great concern to us several indicators to us, such as the current connection pool idle connection number, total number of connection, a connection is used how long to return, to create a physical connection how long does it take, HikariCP connection pool monitoring we specially detailed breakdown of the section, For example, HikariCP comes with support for Prometheus, Micrometer, and DropWizard, but I don’t know much about the latter two. Prometheus:

Next, let’s take a look at the interface definition:

// The implementation of this interface is mainly responsible for collecting the time of some actions
public interface IMetricsTracker extends AutoCloseable
{
    // This method triggers when the actual physical connection is created (main flow 3) to record the time it takes for an actual physical connection to be created
    default void recordConnectionCreatedMillis(long connectionCreatedMillis) {}

    // This method triggers getConnection (main process 1) to record the actual time it takes to get a connection
    default void recordConnectionAcquiredNanos(final long elapsedAcquiredNanos) {}

    // This method triggers the reclaiming of the connection (main process 6), which is used to record the time elapsed from the time a connection was retrieved to the time it was reclaimed
    default void recordConnectionUsageMillis(final long elapsedBorrowedMillis) {}

    // This method triggers getConnection (main process 1), which is used to record the number of times a getConnection timeout occurs. Each time a getConnection timeout occurs, the method is invoked
    default void recordConnectionTimeout(a) {}

    @Override
    default void close(a) {}}Copy the code

The MetricsTrackerFactory interface is defined as follows:

// Create an IMetricsTracker instance and record the properties of the PoolStats object on demand. The properties of the PoolStats object are thread pool state indicators such as the number of idle connections in the pool.
public interface MetricsTrackerFactory
{
    // Return an IMetricsTracker object and pass PoolStats to it
    IMetricsTracker create(String poolName, PoolStats poolStats);
}
Copy the code

See the comments for the above interface usage, and for the new PoolStats class, let’s see what it does:

public abstract class PoolStats {
    private final AtomicLong reloadAt; // Trigger the next refresh (timestamp)
    private final long timeoutMs; // The frequency at which the following attributes are refreshed is 1s by default and cannot be changed

    // Total number of connections
    protected volatile int totalConnections;
    // Number of idle connections
    protected volatile int idleConnections;
    // Number of active connections
    protected volatile int activeConnections;
    // Number of business threads that blocked because they could not get an available connection
    protected volatile int pendingThreads;
    // Maximum number of connections
    protected volatile int maxConnections;
    // Minimum number of connections
    protected volatile int minConnections;

    public PoolStats(final long timeoutMs) {
        this.timeoutMs = timeoutMs;
        this.reloadAt = new AtomicLong();
    }

    // This is an example of getting the maximum number of connections
    public int getMaxConnections(a) {
        if (shouldLoad()) { // Whether to refresh
            update(); // Refresh the attribute values. Note that the implementation of this update is in HikariPool, because the direct or indirect source of the attribute values is HikariPool
        }

        return maxConnections;
    }
    
    protected abstract void update(a); // The implementation is mentioned above

    private boolean shouldLoad(a) { // Refresh attribute values based on the update frequency
        for(; ;) {final long now = currentTime();
            final long reloadTime = reloadAt.get();
            if (reloadTime > now) {
                return false;
            } else if (reloadAt.compareAndSet(reloadTime, plusMillis(now, timeoutMs))) {
                return true; }}}}Copy the code

This is actually where the properties are fetched and the refresh is triggered, so where is this object generated and thrown to the MetricsTrackerFactory’s create method? This is the main point of this section: the setup monitor process in Main Flow 2. Take a look at what happens there:

// Monitor set method (this method is in the HikariPool, the metricsTracker property is used by HikariPool to trigger the IMetricsTracker method call)
public void setMetricsTrackerFactory(MetricsTrackerFactory metricsTrackerFactory) {
    if(metricsTrackerFactory ! =null) {
        // The MetricsTrackerDelegate is a wrapper class. It is a static inner class of HikariPool. It is the class that actually holds the IMetricsTracker object, and it is the class that actually triggers the calls to the methods in IMetricsTracker
        // This will trigger the MetricsTrackerFactory class's create method to get the IMetricsTracker object, then initialize the PoolStat object with getPoolStats, and then pass it to the MetricsTrackerFactory
        this.metricsTracker = new MetricsTrackerDelegate(metricsTrackerFactory.create(config.getPoolName(), getPoolStats()));
    } else {
        // Without monitoring enabled, it is equivalent to an empty class with no implementation methods
        this.metricsTracker = newNopMetricsTrackerDelegate(); }}private PoolStats getPoolStats(a) {
    // Initialize the PoolStats object and specify the update method that triggers a refresh of the property values for 1s
    return new PoolStats(SECONDS.toMillis(1)) {
        @Override
        protected void update(a) {
            // Implement the PoolStat update method to refresh the value of each attribute
            this.pendingThreads = HikariPool.this.getThreadsAwaitingConnection();
            this.idleConnections = HikariPool.this.getIdleConnections();
            this.totalConnections = HikariPool.this.getTotalConnections();
            this.activeConnections = HikariPool.this.getActiveConnections();
            this.maxConnections = config.getMaximumPoolSize();
            this.minConnections = config.getMinimumIdle(); }}; }Copy the code

Here HikariCP monitor is registered, so to achieve their own monitor to get the above indicators, to go through the following steps:

  1. Create a new class implementationIMetricsTrackerInterface, we’ll call this classIMetricsTrackerImpl
  2. Create a new class implementationMetricsTrackerFactoryInterface, we’ll call this classMetricsTrackerFactoryImpl, and will the aboveIMetricsTrackerImplIn itsThe create methodThe instantiation
  3. willMetricsTrackerFactoryImplTo call HikariPool after instantiationsetMetricsTrackerFactoryMethod to register with the Hikari connection pool.

This does not mention how the PoolStats properties are monitored. In this case, since the create method is called once, the create method only receives an instance of the PoolStats object. If you do not process it, the instance will be lost to the monitoring module after the create call. If you want to retrieve the PoolStats properties, you need to start a daemon thread that holds an instance of the PoolStats object, periodically fetch its internal properties, and push them to the monitoring system. If you want to retrieve the PoolStats properties, you need to start a daemon thread that holds an instance of the PoolStats object, periodically fetch its internal properties, and push them to the monitoring system. You can customize a Collector object to receive PoolStats instances, similar to HikariCP’s native Prometheus monitoring implementation, so that Prometheus can periodically pull, Such as HikariCP according to own definition MetricsTrackerFactory Prometheus monitoring system to achieve corresponding PrometheusMetricsTrackerFactory in figure 2 (class) :

@Override
public IMetricsTracker create(String poolName, PoolStats poolStats) {
    getCollector().add(poolName, poolStats); // Pass the received PoolStats object directly to the Collector, so that every time the Prometheus server triggers a collection interface call, PoolStats executes an internal property acquisition process
    return new PrometheusMetricsTracker(poolName, this.collectorRegistry); // Return the implementation class of the IMetricsTracker interface
}

// Custom Collector
private HikariCPCollector getCollector(a) {
    if (collector == null) {
        // Register with the Prometheus Collection Center
        collector = new HikariCPCollector().register(this.collectorRegistry);
    }
    return collector;
Copy the code

The above explains how to customize your own monitor in HikariCP and how it differs from Druid’s monitor. Although our company also uses Prometheus monitoring, the name of monitoring index in the original Prometheus collector of HikariCP does not meet our standards, so we have a custom one. If you have similar problems, please try it.

🍁 no drawing of the section, pure code, because this part drawing is not very good explanation, this part has less relationship with the connection pool overall process, at best, access to some of the properties of the connection pool itself, the trigger in the connection pool also make myself clear in the above code segment of the comments, see some code definition may better understand.

Vii. Process 2.2: Detection and Alarm of connection leakage

This section, corresponding to subprocess 2.2 in main process 2, initializes the pool object with a property called leakTaskFactory. This section looks at what it does.

7.1: What does it do?

A connection that has been taken out for longer than leakDetectionThreshold (configurable, default 0) and not returned will trigger a connection leak alert informing the service party that there is a connection leak problem.

7.2: Process details

This attribute is ProxyLeakTaskFactory type object, and it will also hold houseKeepingExecutorService this thread pool object, used in the production of ProxyLeakTask object, Then use the above houseKeepingExecutorService delay run the run method of the object. The trigger point for this process is in process 1.1 above, where the ProxyConnection object is finally wrapped. Here’s a detailed flowchart:

Each time a ProxyConnection object is generated in Process 1.1, the above process is triggered. As you can see from the flowchart, the ProxyConnection object holds the objects of the PoolEntry and ProxyLeakTask, The leakTaskFactory object is used to initialize ProxyLeakTask. ProxyLeakTask is initialized by its schedule method, and the example is passed to ProxyConnection for initialization assignment (ps: The graph shows that ProxyConnection will actively cancel the leak check task when triggering the recycle event, which is why ProxyConnection needs to hold the ProxyLeakTask object.

As you can see from the above flowchart, a ProxyLeakTask object with an actual delayed task is generated only if leakDetectionThreshold is not equal to 0, otherwise an empty object with no real meaning is returned. So to enable connection leak checking, first set the leakDetectionThreshold configuration. This property indicates that the loaned-out connection has not been returned after that time, triggering the connection leak alarm.

ProxyConnection holds the ProxyLeakTask object because it can listen to whether the connection triggers a return operation, and if so, calls the cancel method to cancel the delayed task and prevent false alarms.

Compared with Druid, HikariCP is a simple implementation. It only prints a warning log when the connection is triggered, and does not take specific measures to force the reclamation.

Like Druid, this process is also closed by default, because the actual development is generally used in the third-party framework, the framework itself will ensure timely close connection, prevent connection object leakage, whether to open or not depends on whether the business needs, if you must open, How to set the size of leakDetectionThreshold is also something to consider. The author’s Spring Boot column and Mybatis column have been completed, follow the public number [code monkey technology column] reply keyword Spring Boot advanced, Mybatis advanced obtain.

Viii. Main Flow 3: Generate connection objects

This section looks at the createEntry method in main Process 2. This method uses the DriverDataSource object in PoolBase to generate an actual connection object (if you forget where DriverDataSource was initialized, Take a look at the PoolBase initializeDataSource method in main flow 2 and wrap the PoolEntry object with the PoolEntry class. Now look at the main properties of this wrapper class:

final class PoolEntry implements IConcurrentBagEntry {
    private static final Logger LOGGER = LoggerFactory.getLogger(PoolEntry.class);
    // Use cas to change the state attribute
    private static final AtomicIntegerFieldUpdater stateUpdater;

    Connection connection; // The actual physical connection object
    long lastAccessed; // Refresh the time when the recycle is triggered, indicating "last used time"
    long lastBorrowed; // Refresh the date when getConnection successfully borrows, indicating 'last borrowed time'

    @SuppressWarnings("FieldCanBeLocal")
    private volatile int state = 0; // Connection status. Enumerations: IN_USE, NOT_IN_USE, REMOVED, RESERVED
    private volatile boolean evict; // Whether the connection is marked as obsolete is used in many places (for example, process 1.1 uses this to determine whether the connection is obsolete, or main process 4 triggers direct obsoletization logic when the clock goes back).

    private volatileScheduledFuture<? > endOfLife;// A delayed task used to discontinue a connection after maxLifeTime. PoolEntry holds the object because the task needs to be cancelled when the object is actively closed (meaning it does not need to be actively disabled after maxLifeTime)

    private final FastList openStatements; // All statement objects that are currently generated on the connection object. This is used to actively close these objects when reclaiming the connection to prevent any missed statement
    private final HikariPool hikariPool; // Hold the pool object

    private final boolean isReadOnly; // Read-only or not
    private final boolean isAutoCommit; // Whether a transaction exists
}
Copy the code

All the properties in the above is the whole PoolEntry object, here again the endOfLife object, it is a use of houseKeepingExecutorService this thread pool objects do delay the task, This delayed task is usually triggered around maxLifeTime after the connection object is created.

private PoolEntry createPoolEntry(a) {

        final PoolEntry poolEntry = newPoolEntry(); // Generate the actual connection object

        final long maxLifetime = config.getMaxLifetime(); // Get maxLifetime configured
        if (maxLifetime > 0) { // If the value is <=0, the active expiration policy is disabled
            // Calculate the random number to be subtracted
            // Variance up to 2.5% of the maxLifetime
            final long variance = maxLifetime > 10 _000 ? ThreadLocalRandom.current().nextLong(maxLifetime / 40) : 0;
            final long lifetime = maxLifetime - variance; // Generate the actual delay time
            poolEntry.setFutureEol(houseKeepingExecutorService.schedule(
                    () -> { SoftEvictConnection will mark the connection object as obsolete and attempt to change its state to STATE_RESERVED. If this is successful, Triggers closeConnection (process 1.1.2)
                        if (softEvictConnection(poolEntry, "(connection has passed maxLifetime)".false /* not owner */)) {
                            addBagItem(connectionBag.getWaitingThreadCount()); // After the collection is complete, if a connection is lost in the connection pool, an attempt is made to add a new connection object
                        }
                    },
                    lifetime, MILLISECONDS)); // Assign a value to endOfLife and submit a delayed task, which is triggered after lifetime
        }

        return poolEntry;
    }

    // Trigger the new connection task
    public void addBagItem(final int waiting) {
        AddConnectionQueue (); addConnectionExecutor ()

        // When the number of submitted tasks in the queue for adding connections exceeds the number of threads that are blocked because they cannot get a connection, the task of submitting new connections is performed
        final boolean shouldAdd = waiting - addConnectionQueue.size() >= 0; // Yes, >= is intentional.
        if (shouldAdd) {
            // Submit the task to the addConnectionExecutor thread pool. PoolEntryCreator is a class that implements the Callable interfaceaddConnectionExecutor.submit(poolEntryCreator); }}Copy the code

Each connection is wrapped into a PoolEntry object. When the object is created, a delay task is submitted to close the connection. This time is configured as maxLifeTime. To ensure that it does not expire at the same time, HikariCP also subtracted a random number from maxLifeTime as the final delay time of the delayed task, and then triggered addBagItem to add a connection to the pool when a connection was discarded. This task is assigned to the addConnectionExecutor thread pool defined in main process 2. Now look at the process of asynchronously adding connection objects:

This process is used to add connections to the connection pool and is combined with createEntry because the two processes are closely related. In addition, the main process 5 (fillPool) also triggers this task.

9. Main Process 4: Connection pool Capacity reduction

HikariCP according to minIdle links regularly cleaned idle for too long, the timing task 2 initializes the connection pool in the main process object is enabled, keep up with the process, also is to use the thread pool houseKeepingExecutorService object to do the timing task executor.

See how this task is enabled in Main Process 2:

The default value of housekeepingPeriodMs is 30s, so the timed task interval is 30s
this.houseKeeperTask = houseKeepingExecutorService.scheduleWithFixedDelay(new HouseKeeper(), 100L, housekeepingPeriodMs, MILLISECONDS);
Copy the code

This section is mainly about the next class, this class implements the Runnable interface, the recovery logic is mainly in its RUN method, to see the run method logic flow chart:

The above process is the specific thing to do in the run method, because the system time callback will cause the scheduled task to recover some connection error, so there is the following judgment:

// Now is the current system time, previous is the time when the task was last triggered and housekeepingPeriodMs is how often the task was triggered
// That means plusMillis(previous, housekeepingPeriodMs) indicates the current time
// If the system time is not called back, then plusMillis(now, 128) must be greater than the current time, if the system time is called back
// If the callback time exceeds 128ms, then the following judgment is true, otherwise it will never be true
if (plusMillis(now, 128) < plusMillis(previous, housekeepingPeriodMs))
Copy the code

This is what hikariCP does when the system clock is called back. As you can see from the flow chart, it simply takes all the connected objects in the pool, one by one, and flags them as discarded, and tries to change the state value to STATE_RESERVED (I’ll explain these states later, but I won’t get into them here). If the system clock does not change (most of the time, it will hit this logic), all the connections in the pool that are idle (STATE_NOT_IN_USE) will be taken out, and then the scope of the connection to be checked will be calculated, and then the state of the connection will be changed in a loop:

// Get all the connections that are idle
final List notInUse = connectionBag.values(STATE_NOT_IN_USE);
// Count the number of idle objects that need to be checked. In simple terms, the pool needs to keep the minimum number of minIdle connections alive, so you need to count the number of idle objects beyond this range to check
int toRemove = notInUse.size() - config.getMinIdle();
for (PoolEntry entry : notInUse) {
  // If the connection object is within the check range and the idle time exceeds idleTimeout, the state of the connection object is successfully changed from STATE_NOT_IN_USE to STATE_RESERVED
  if (toRemove > 0 && elapsedMillis(entry.lastAccessed, now) > idleTimeout && connectionBag.reserve(entry)) {
    closeConnection(entry, "(connection has passed idleTimeout)"); // Close the connection if the above conditions are met
    toRemove--;
  }
}
fillPool(); // Because some connections may have been reclaimed, trigger the connection pool expansion process again to check whether new connections are needed.
Copy the code

The above code is the corresponding flow logic in the flowchart when there is no callback system time. This process can be enabled only when idleTimeout is greater than 0 (the default value is 0) and minIdle is smaller than maxPoolSize. This process is disabled by default. If you need to enable this process, you can configure it according to the conditions.

Main Process 5: Expanding the connection pool

This process relies on the fillPool method in HikariPool. This method has been used in many of the processes above. It is used to initiate an operation to increase the number of connections if the pool is insufficient. Made minor changes to the source code) :

// The PoolEntryCreator implementation of the call method is already seen in Main Flow 3, but there are two PoolEntryCreator objects.
// This is a bit of detail, which is used for logging. For the sake of understanding, just know that both objects execute the same call method
private final PoolEntryCreator poolEntryCreator = new PoolEntryCreator(null);
private final PoolEntryCreator postFillPoolEntryCreator = new PoolEntryCreator("After adding ");

private synchronized void fillPool(a) {
  // The number of connections to be expanded is calculated based on the current pool data.
  // Use the difference between the maximum number of connections and the total number of current connections, and the difference between the minimum number of connections and the number of idle connections in the current pool
  int needAdd = Math.min(maxPoolSize - connectionBag.size(),
  minIdle - connectionBag.getCount(STATE_NOT_IN_USE));

  // The number of new connections that need to be added is subtracted from the current queued tasks
  final int connectionsToAdd = needAdd - addConnectionQueue.size();
  for (int i = 0; i < connectionsToAdd; i++) {
    // The postFillPoolEntryCreator task will be printed the last time in the loop (ignore the interference logic)
    addConnectionExecutor.submit((i < connectionsToAdd - 1)? poolEntryCreator : postFillPoolEntryCreator); }}Copy the code

As you can see from this process, the task of creating a new connection is eventually handled by the addConnectionExecutor thread pool, and the subject of the task is also PoolEntryCreator, which can be referred to as main process 3.

Then needAdd’s calculations:

Math.min(Max connections - total number of current connections in the pool, min connections - number of idle connections in the pool)Copy the code

This ensures that the number of connections in the pool will never exceed maxPoolSize and will never be lower than minIdle. In case of tight connections, you can ensure that each trigger expands with the number of minIdle. Therefore, if maxPoolSize is set to the same value as minIdle, no expansion will occur when connections in the pool become tight.

Xi. Main process 6: Connection recovery

As we said in the beginning, eventually the actual physical connection object will be wrapped up as a PoolEntry object, stored in the ConcurrentBag, and then when retrieved, the PoolEntry object will be wrapped up again as a ProxyConnection object and exposed to the user, triggering the connection retraction. This actually triggers the close method in the ProxyConnection:

public final void close(a) throws SQLException {
  // 原 文 : Closing statements can cause connection eviction, so this must run before the conditional below
  closeStatements(); // Close all statement objects generated when the connection object is used by the business party
  if(delegate ! = ClosedConnection.CLOSED_CONNECTION) { leakTask.cancel();// Cancel the connection leak check task. Refer to Process 2.2
    try {
      if(isCommitStateDirty && ! isAutoCommit) {// If the transaction is open and the execution statement exists, the close call is required to actively roll back the transaction
        delegate.rollback(); / / rollback
        lastAccess = currentTime(); // Refresh "last use time"}}finally {
      delegate = ClosedConnection.CLOSED_CONNECTION;
      poolEntry.recycle(lastAccess); // Triggers reclamation}}}Copy the code

This is the close method in the ProxyConnection, and you can see that it eventually calls the recycle method in the PoolEntry. In addition, the last time that the connection object was used was when it was refreshed, which is a very important property. To determine how long a connection object has been idle, look at the recycle method in PoolEntry:

void recycle(final long lastAccessed) {
  if(connection ! =null) {
    this.lastAccessed = lastAccessed; // Refresh the last used time
    hikariPool.recycle(this); // Trigger the HikariPool's collection method to pass itself}}Copy the code

As mentioned earlier, each PoolEntry object holds a HikariPool object to trigger connection pool operations. As you can see from the code above, it will eventually trigger the Recycle method in the HikariPool. Let’s look at the recycle method in the HikariPool:

void recycle(final PoolEntry poolEntry) {
  metricsTracker.recordConnectionUsage(poolEntry); // The monitoring indicators are related, so ignore them
  connectionBag.requite(poolEntry); // Finally triggers the Requite method of the connectionBag to return the connection. This process refers to the Requite method section of the main connectionBag process
}
Copy the code

This is the logic for connecting the recycling part, which is relatively concise compared to other processes.

12. Main process of ConcurrentBag

This class is used to hold the final connection object of type PoolEntry. It provides basic add, delete, and check functions. It is held by HikariPool, where most of the operations are performed. Instead, the ConcurrentBag class is used to comb through the triggers for all the processes above:

  • Main process 2: Initialization when initializing HikariPoolConcurrentBag (constructor), pass when preheatingcreateEntryGet the connection object, callConcurrentBag.addAdd a connection to ConcurrentBag.
  • Process 1.1: When obtaining a connection through HikariPool, callConcurrentBag.borrowGet a connection object.
  • Main process 6: ApprovedConcurrentBag.requiteReturns a connection.
  • Process 1.1.2: When a connection is triggered to close, it passesConcurrentBag.removeRemove a connection object. Based on the previous process, you can see that the connection closure triggers are as follows: The connection is actively disused when the maxLifeTime exceeds, the health check does not pass, and the connection pool is reduced.
  • Main process 3: When adding a connection asynchronously, by callingConcurrentBag.addAdd a connection to ConcurrentBag. As can be seen from the previous process, the trigger point for adding a connection is: after the connection exceeds the maximum life cycle and maxLifeTime disuses the connection actively, the connection pool is expanded.
  • Main process 4: Connection pool reduction taskConcurrentBag.valuesFilter out the connection objects you need to do the operation on, and then pass throughConcurrentBag.reserveThe modification to the state of the connection object is complete and passesProcess 1.1.2Trigger close and remove connection operations.

By sorting out the trigger points, we can know the main method in the structure, that is, the part marked as the label color in the above trigger points, and then take a detailed look at the basic definition and the main method of this class:

public class ConcurrentBag<T extends IConcurrentBagEntry> implements AutoCloseable {

    private final CopyOnWriteArrayList<T> sharedList; // Where the PoolEntry object is finally stored, which is a CopyOnWriteArrayList
    private final boolean weakThreadLocals; // The default value is false. True allows a connection object to be in a weak reference state in the list of threadLists below, preventing memory leaks (see note 1).

    private final ThreadLocal<List<Object>> threadList; // Thread level caching. Connection objects obtained from sharedList will be cached in the current thread. If the link object is borrowed, it will be retrieved from the cache first, thus achieving a lockless pool implementation
    private final IBagStateListener listener; // The internal interface, implemented by HikariPool, is used by ConcurrentBag to proactively notify HikariPool to trigger the asynchronous operation of adding connection objects (the process triggered by addConnectionExecutor in main process 3).
    private final AtomicInteger waiters; // The number of business threads that are currently blocked because they cannot get a connection. This is also the case in the previous process. For example, in main process 3, addBagItem will determine whether to add a connection based on this metric
    private volatile boolean closed; // Mark whether the current ConcurrentBag is closed

    private final SynchronousQueue<T> handoffQueue; // This queue is used to retrieve the newly created connection object from the add method in case the connection is insufficient. See the code below for more details

    // Internal interface, which the PoolEntry class implements
    public interface IConcurrentBagEntry {

        // Connect the state of the object, which has been covered in many parts of the previous process, such as the downsizing of main process 4
        int STATE_NOT_IN_USE = 0; / / idle
        int STATE_IN_USE = 1; / / in use
        int STATE_REMOVED = -1; / / has been abandoned
        int STATE_RESERVED = -2; // The mark is reserved, an intermediate state between idle and discarded, where changes are triggered by downsizing

        boolean compareAndSet(int expectState, int newState); // Try to change the status value of the connection object using cas

        void setState(int newState); // Set the status value

        int getState(a); // Get the status value
    }

    // See the listener attribute above
    public interface IBagStateListener {
        void addBagItem(int waiting);
    }

    // Get the connection method
    public T borrow(long timeout, final TimeUnit timeUnit) {
        / / to omit...
    }

    // Recycle the connection method
    public void requite(final T bagEntry) {
        / / to omit...
    }

    // Add the connection method
    public void add(final T bagEntry) {
        / / to omit...
    }

    // Remove the connection method
    public boolean remove(final T bagEntry) {
        / / to omit...
    }

    // Obtain the set of all qualified connections in the current pool based on the connection status value
    public List values(final int state) {
        / / to omit...
    }

    // Get all connections in the current pool
    public List values(a) {
        / / to omit...
    }

    // Use cas to change the state of the incoming connection object from STATE_NOT_IN_USE to STATE_RESERVED
    public boolean reserve(final T bagEntry) {
        / / to omit...
    }

    // Get the number of connections in the current pool that match the incoming status value
    public int getCount(final int state) {
        / / to omit...}}Copy the code

From this basic structure, you can see how HikariCP optimizations traditional connection pool implementations. HikariCP is more of a lock-free implementation than Druid to avoid lock contention.

12.1: borrow

This method is used to get a available connection object. The trigger point is process 1.1. HikariPool uses this method to get the connection.

public T borrow(long timeout, final TimeUnit timeUnit) throws InterruptedException {
    Try the thread-local list first
    final List<Object> list = threadList.get(); // Retrieve the previously cached set of connection objects from the current thread cache
    for (int i = list.size() - 1; i >= 0; i--) {
        final Object entry = list.remove(i); // Remove it first, and then add it again in the recycle method
        final T bagEntry = weakThreadLocals ? ((WeakReference<T>) entry).get() : (T) entry; // Weak references are not enabled by default
        // After obtaining the object, the CAS tries to change its state from STATE_NOT_IN_USE to STATE_IN_USE.
        // If the attribute is successfully modified, the cas of the current thread will fail, and the loop will continue trying to get the next connection object
        if(bagEntry ! =null && bagEntry.compareAndSet(STATE_NOT_IN_USE, STATE_IN_USE)) {
            return bagEntry; // If cas is set successfully, it indicates that the current thread bypasses the interference of other threads, successfully obtains the connection object, and returns directly}}// 原 文 : Otherwise, scan the shared list... then poll the handoff queue
    final int waiting = waiters.incrementAndGet(); // We need to "work back" as a waiters+1 if we don't find a great connection in the cache
    try {
        for (T bagEntry : sharedList) {
            // Loop sharedList and try to change the connection state value from STATE_NOT_IN_USE to STATE_IN_USE
            if (bagEntry.compareAndSet(STATE_NOT_IN_USE, STATE_IN_USE)) {
                If we may have stolen another waiter's connection, request another bag add.
                if (waiting > 1) { // When the number of blocked threads is greater than 1, the addBagItem method of HikariPool needs to be triggered to add the connection to the pool. The implementation of this method is referred to in main flow 3
                    listener.addBagItem(waiting - 1);
                }
                return bagEntry; // If cas is set successfully, it means that the current thread bypasses the interference of other threads, successfully obtains the connection object, and returns directly}}// If the thread cache list does not compete for a connection object, the thread cache list does not compete for a connection object. If the thread cache list does not compete for a connection object, the thread cache list does not compete for a connection object
        listener.addBagItem(waiting);

        timeout = timeUnit.toNanos(timeout); // At this point, the timeout control is used
        do {
            final long start = currentTime();
            // Try to fetch the latest connection object from the handoffQueue. (Normally, new connection objects are offered to the queue in addition to being added to the sharedList.)
            final T bagEntry = handoffQueue.poll(timeout, NANOSECONDS);
            // If the connection object is still not available after the specified time, or the CAS setting succeeds after the object is obtained, in both cases, there is no need to retry, directly return the object
            if (bagEntry == null || bagEntry.compareAndSet(STATE_NOT_IN_USE, STATE_IN_USE)) {
                return bagEntry;
            }
            // If the cas fails to set the connection object, the cas fails to set the connection object. If the CAS fails to set the connection object, the CAS fails to set the connection object. If the CAS fails to set the connection object, the CAS fails to set the connection object, the CAS fails to set the connection object
            timeout -= elapsedNanos(start); // Timeout subtracted the elapsed time to indicate the time available for the next loop
        } while (timeout > 10 _000); // If the remaining time is greater than 10 seconds, the loop will only run once, because timeout is rarely larger than 10 seconds

        return null; // Timeout, null is still returned
    } finally {
        waiters.decrementAndGet(); // My work is great for my work. My work is great for my work}}Copy the code

If you look closely at the comments, the process breaks down into three main steps:

  1. Get the connection from the thread cache
  2. Can’t get fromsharedListget
  3. If none is available, the connection logic is added and an attempt is made to retrieve the newly generated connection object from the queue

12.2: the add

This process adds a connection object to the bag, usually triggered by the addBagItem method in main process 3 through the addConnectionExecutor asynchronous task.

public void add(final T bagEntry) {

    sharedList.add(bagEntry); // Add it directly to sharedList

    Spin until a thread takes it or none are waiting
    // With the reference to the Borrow process, a thread schedule is initiated when a thread is waiting to obtain an available connection, and the current new connection is still idle and there are no customers waiting in the queue
    while (waiters.get() > 0&& bagEntry.getState() == STATE_NOT_IN_USE && ! handoffQueue.offer(bagEntry)) {// Note that this will offer a connection object to the queueyield(); }}Copy the code

In conjunction with borrow, we add a connection object to the queue whenever a waiting thread exists, making it easier for the places in borrow where waits occur to poll the connection object.

12.3: to requite

This process will reclaim a connection. The trigger point of this method is in main process 6. The code is as follows:

public void requite(final T bagEntry) {
    bagEntry.setState(STATE_NOT_IN_USE); Change state to STATE_NOT_IN_USE

    for (int i = 0; waiters.get() > 0; i++) { // If there is a waiting thread, try to pass it to the queue for borrow to retrieve
        if(bagEntry.getState() ! = STATE_NOT_IN_USE || handoffQueue.offer(bagEntry)) {return;
        }
        else if ((i & 0xff) = =0xff) {
            parkNanos(MICROSECONDS.toNanos(10));
        }
        else{ yield(); }}final List<Object> threadLocalList = threadList.get();
    if (threadLocalList.size() < 50) { // The set of intra-thread connections can be cached up to 50 times. If the connection is reborrowed, it will be added to the cache of the current thread
        threadLocalList.add(weakThreadLocals ? new WeakReference<>(bagEntry) : bagEntry); // Weak references are not enabled by default. If they are enabled, connection objects in the cache collection are not at risk of memory leaks}}Copy the code

12.4: remove

This is responsible for removing a connection object from the pool. The trigger is in process 1.1.2, as follows:

public boolean remove(final T bagEntry) {
    // The following two CAS operations are changed from the other state to the removed state. If either of them is successful, the following WARN log will not be entered
    if(! bagEntry.compareAndSet(STATE_IN_USE, STATE_REMOVED) && ! bagEntry.compareAndSet(STATE_RESERVED, STATE_REMOVED) && ! closed) { LOGGER.warn("Attempt to remove an object from the bag that was not borrowed or reserved: {}", bagEntry);
        return false;
    }

    // Remove directly from sharedList
    final boolean removed = sharedList.remove(bagEntry);
    if(! removed && ! closed) { LOGGER.warn("Attempt to remove an object from the bag that does not exist: {}", bagEntry);
    }

    return removed;
}
Copy the code

It is important to note that only the objects in the sharedList are removed, and the corresponding objects in the cache of each thread are not removed. At this time, will the connection be retrieved from the cache again? Yes, but it will not return out, but will be removed directly. A closer look at the code of Borrow shows that if the state is not idle, it will be removed when it is taken out, and then it will not be taken out, so naturally it will not trigger the reclamation method.

12.5: values

This method has an overload method, which is used to return the set of connected objects in the current pool. The trigger point is in main process 4, and the code is as follows:

public List values(final int state) {
    // Filter out the collection of objects that match the state value and return out in reverse order
    final List list = sharedList.stream().filter(e -> e.getState() == state).collect(Collectors.toList());
    Collections.reverse(list);
    return list;
}

public List values(a) {
    // Return all connected objects (note that Clone is a shallow copy)
    return (List) sharedList.clone();
}
Copy the code

12.6: reserve

This method only changes the state value of the connection object from STATE_NOT_IN_USE to STATE_RESERVED. The trigger point is still main process 4, which is used for capacity reduction. The code is as follows:

public boolean reserve(final T bagEntry){
   return bagEntry.compareAndSet(STATE_NOT_IN_USE, STATE_RESERVED);
}
Copy the code

12.7: getCount

This method is used to return the total number of connections in the pool that match a certain state value. The trigger point is process 5. When expanding the connection pool, it is used to get the total number of idle connections.

public int getCount(final int state){
   int count = 0;
   for (IConcurrentBagEntry e : sharedList) {
      if(e.getState() == state) { count++; }}return count;
}
Copy the code

This is the main method of ConcurrentBag and the main process of handling the connection object.

Xiii. Summary

HikariCP is a very different implementation of the Druid. It is a very different implementation of the HikariCP. It is a very different implementation of the HikariCP. The FastList structure is rarely used from a connection management perspective. The FastList structure is mainly used to store statement objects generated by connection objects and to store connection objects cached within the thread. The author’s Spring Boot column and Mybatis column have been completed, follow the public number [code monkey technology column] reply keyword Spring Boot advanced, Mybatis advanced obtain.

In addition, HikariCP also uses Javassist technology to compile the initialization of ProxyConnection, which is not explained here. There are many articles on HikariCP optimization online. Most mentioned bytecode optimization, fastList, concurrentBag implementation, this article mainly through the in-depth analysis of HikariPool and concurrentBag implementation, to explain HikariCP compared to Druid specific do what is not the same operation.