Analyse du code source du pool de connexions Hikari

HikariCP est actuellement reconnu comme le pool de connexions à base de données le plus performant, et il est également le pool de connexions par défaut utilisé par SpringBoot 2.0 et ses versions ultérieures.

I. Utilisation de Hikari

1.1. Configuration de Hikari

Étant donné que SpringBoot 2.0 utilise par défaut HikariCP, il n'est pas nécessaire d'ajouter des dépendances Maven supplémentaires spécifiques à Hikari. Il suffit d'ajouter la configuration correspondante dans le fichier application.yml, comme suit :

spring.datasource.driver-class-name=com.mysql.jdbc.Driver
spring.datasource.url=jdbc:mysql://127.0.0.1:3306/test?useUnicode=true&characterEncoding=UTF-8&autoReconnect=true&useSSL=false&zeroDateTimeBehavior=convertToNull
spring.datasource.username=admin
spring.datasource.password=admin
spring.datasource.type=com.zaxxer.hikari.HikariDataSource
spring.datasource.hikari.minimum-idle=5
spring.datasource.hikari.maximum-pool-size=15
spring.datasource.hikari.auto-commit=true
spring.datasource.hikari.idle-timeout=30000
spring.datasource.hikari.pool-name=DatebookHikariCP
spring.datasource.hikari.max-lifetime=1800000
spring.datasource.hikari.connection-timeout=30000
spring.datasource.hikari.connection-test-query=SELECT 1

1.2. Détail des paramètres de configuration

Paramètre Valeur Description
autoCommit true Validation automatique des transactions
connectionTimeout 30000 Délai d'attente maximal pour l'obtention d'une connexion
idleTimeout 60000 Durée maximale d'inactivité d'une connexion
maxLifetime 1800000 Durée de vie maximale d'une connexion
minimumIdle 1 Nombre minimal de connexions inactives dans le pool
maximumPoolSize 10 Nombre maximal de connexions dans le pool
readOnly false Les connexions obtenues sont-elles en lecture seule
validationTimeout 5000 Délai de validation d'une connexion
leadDetectionThreshold 60000 Délai avant la récupération forcée d'une connexion

II. Analyse du code source de Hikari

2.1. Obtention d'une connexion

  1. La classe centrale de Hikari est HikariDataSource, qui représente la source de données du pool de connexions. Elle implémente l'interface DataSource et sa méthode getConnection est la suivante :
 1 /** Objet du pool de connexions
 2      * fastPathPool est créé lors de l'initialisation
 3      * pool est créé lors de l'obtention d'une connexion
 4      * Le mot-clé volatile appliqué à pool entraîne un chargement depuis la mémoire principale à chaque lecture
 5      * et une écriture vers la mémoire principale à chaque écriture, ce qui est moins performant que fastPathPool
 6      * */
 7     private final HikariPool fastPathPool;
 8     private volatile HikariPool pool;
 9 
10     /** Obtenir une connexion */
11     public Connection getConnection() throws SQLException
12     {
13         if (isClosed()) {
14             throw new SQLException("HikariDataSource " + this + " a été fermé.");
15         }
16         /** Si fastPathPool existe, obtenir directement une connexion */
17         if (fastPathPool != null) {
18             return fastPathPool.getConnection();
19         }
20         /** Si fastPathPool n'existe pas, créer un objet HikariPool */
21         HikariPool result = pool;
22         if (result == null) {
23             synchronized (this) {
24                 result = pool;
25                 if (result == null) {
26                     validate();
27                     LOGGER.info("{} - Démarrage...", getPoolName());
28                     try {
29                         /** Initialisation et création de l'objet HikariPool */
30                         pool = result = new HikariPool(this);
31                         this.seal();
32                     }
33                     catch (PoolInitializationException pie) {
34                         //
35                     }
36                 }
37             }
38         }
39         /** Appeler la méthode getConnection() du pool pour obtenir une connexion */
40         return result.getConnection();
41     }

 1 public HikariDataSource(HikariConfig configuration)
 2    {
 3       configuration.validate();
 4       configuration.copyStateTo(this);
 5 
 6       LOGGER.info("{} - Démarrage...", configuration.getPoolName());
 7       pool = fastPathPool = new HikariPool(this);
 8       LOGGER.info("{} - Démarrage terminé.", configuration.getPoolName());
 9 
10       this.seal();
11    }

La méthode getConnection est simple, elle appelle principalement la méthode getConnection() de HikariPool. HikariDataSource contient deux objets HikariPool : fastPathPool est créé dans le constructeur paramétré de HikariPool, et si fastPathPool n'existe pas, l'objet pool est créé dans la méthode getConnection.

Il est évident que l'objet pool est modifié par le mot-clé volatile, tandis que fastPathPool est de type final, donc fastPathPool est plus efficace que pool. Par conséquent, il est recommandé d'utiliser le constructeur paramétré de HikariDataSource pour l'initialisation.

  1. Comme indiqué précédemment, la logique d'obtention d'une connexion se trouve dans la méthode getConnection de HikariPool. Analysons cette méthode :
 1 /** Obtenir une connexion */
 2     public Connection getConnection(final long hardTimeout) throws SQLException
 3     {
 4         /** Obtenir un verrou */
 5         suspendResumeLock.acquire();
 6         final long startTime = currentTime();
 7 
 8         try {
 9             long timeout = hardTimeout;
10             do {
11                 /** Emprunter un objet PoolEntry depuis ConcurrentBag */
12                 PoolEntry poolEntry = connectionBag.borrow(timeout, MILLISECONDS);
13                 if (poolEntry == null) {
14                     break; // Délai d'attente dépassé... on sort et on lève une exception
15                 }
16 
17                 final long now = currentTime();
18                 /** Vérifier si la connexion est marquée pour être rejetée ou inactivée depuis trop longtemps */
19                 if (poolEntry.isMarkedEvicted() || (elapsedMillis(poolEntry.lastAccessed, now) > aliveBypassWindowMs && !isConnectionAlive(poolEntry.connection))) {
20                     closeConnection(poolEntry, poolEntry.isMarkedEvicted() ? EVICTED_CONNECTION_MESSAGE : DEAD_CONNECTION_MESSAGE);
21                     timeout = hardTimeout - elapsedMillis(startTime);
22                 }
23                 else {
24                     metricsTracker.recordBorrowStats(poolEntry, startTime);
25                     /** Créer une connexion proxy via Javassist */
26                     return poolEntry.createProxyConnection(leakTaskFactory.schedule(poolEntry), now);
27                 }
28             } while (timeout > 0L);
29             metricsTracker.recordBorrowTimeoutStats(startTime);
30             throw createTimeoutException(startTime);
31         }
32         catch (InterruptedException e) {
33             Thread.currentThread().interrupt();
34             throw new SQLException(poolName + " - Interruption pendant l'acquisition de la connexion", e);
35         }
36         finally {
37             /** Libérer le verrou */
38             suspendResumeLock.release();
39         }
40     }

Il y a deux étapes principales : appeler la méthode borrow de ConcurrentBag pour emprunter un objet PoolEntry, puis appeler la méthode createProxyConnection de PoolEntry pour générer dynamiquement un objet connexion proxy.

Cela implique deux classes centrales : ConcurrentBag et PoolEntry.

  1. PoolEntry

Comme son nom l'indique, PoolEntry est un nœud du pool de connexions, qui peut être considéré comme un encapsulation d'un objet Connection. Les connexions stockées dans le pool sont stockées sous forme d'objets PoolEntry.

Les attributs internes de PoolEntry sont les suivants :

Attribut Type Description
connection Connection Connexion à la base de données
lastAccessed long Dernière date d'accès
lastBorrowed long Dernière date d'emprunt
state volatile int État actuel
evict volatile boolean Doit-il être rejeté
openStatements FastList Collection d'ouverts statements
hikariPool HikariPool Objet HikariPool associé
isReadOnly boolean Mode lecture seule
isAutoCommit boolean Validation automatique
  1. ConcurrentBag

ConcurrentBag est essentiellement une collection concurrente, le corps principal du pool de connexions qui stocke les objets PoolEntry encapsulés. Il effectue également un contrôle de concurrence pour résoudre les problèmes de concurrence du pool de connexions.

Les attributs internes de ConcurrentBag sont les suivants :

Attribut Type Description
sharedList CopyOnWriteArrayList Stocke les objets PoolEntry dans trois états : non utilisés, en cours d'utilisation et réservés
weakThreadLocals boolean Utilisation de références faibles
threadList ThreadLocal<List<Object>> Stocke les objets PoolEntry du thread actuel
listener IBagStateListener Écouteur d'ajout d'éléments
waiters AtomicInteger Nombre de threads en attente
closed volatile boolean Indicateur de fermeture
handoffQueue SynchronousQueue File d'attente de blocage sans capacité
  1. Emprunter un élément depuis ConcurrentBag

ConcurrentBag implémente la méthode borrow, qui signifie emprunter un élément de la collection concurrente. Pour le pool de connexions, cela signifie obtenir une connexion depuis le pool :

 1     /** Emprunter un objet */
 2     public T borrow(long timeout, final TimeUnit timeUnit) throws InterruptedException
 3     {
 4         /** 1. Obtenir la collection d'objets liés au thread actuel depuis ThreadLocal */
 5         final List<Object> list = threadList.get();
 6         /** 1.1. S'il existe des objets dans la variable de thread actuel, retourner directement un objet de la liste */
 7         for (int i = list.size() - 1; i >= 0; i--) {
 8             final Object entry = list.remove(i);
 9             @SuppressWarnings("unchecked")
10             final T bagEntry = weakThreadLocals ? ((WeakReference<T>) entry).get() : (T) entry;
11             if (bagEntry != null && bagEntry.compareAndSet(STATE_NOT_IN_USE, STATE_IN_USE)) {
12                 return bagEntry;
13             }
14         }
15 
16         /** 2. Incrémenter de 1 le nombre d'objets en attente */
17         final int waiting = waiters.incrementAndGet();
18         try {
19             /** 3. Parcourir la liste partagée mise en cache, si l'état est non utilisé, modifier en utilisé via CAS */
20             for (T bagEntry : sharedList) {
21                 if (bagEntry.compareAndSet(STATE_NOT_IN_USE, STATE_IN_USE)) {
22                     /** 4. S'il y a plus d'un thread en attente, ajouter une tâche à l'écouteur */
23                     if (waiting > 1) {
24                         listener.addBagItem(waiting - 1);
25                     }
26                     return bagEntry;
27                 }
28             }
29 
30             /** 4. Si la liste partagée en cache est vide ou toutes les connexions sont en cours d'utilisation, ajouter une tâche à l'écouteur */
31             listener.addBagItem(waiting);
32 
33             timeout = timeUnit.toNanos(timeout);
34             do {
35                 final long start = currentTime();
36                 /** 5. Attendre un élément avec délai d'attente depuis la file d'attente de blocage */
37                 final T bagEntry = handoffQueue.poll(timeout, NANOSECONDS);
38                 /** 6. Si l'obtention de l'élément échoue ou réussit, retourner */
39                 if (bagEntry == null || bagEntry.compareAndSet(STATE_NOT_IN_USE, STATE_IN_USE)) {
40                     return bagEntry;
41                 }
42 
43                 timeout -= elapsedNanos(start);
44             } while (timeout > 10_000);
45 
46             return null;
47         }
48         finally {
49             /** 6. Décrémenter de 1 le nombre de threads en attente */
50             waiters.decrementAndGet();
51         }
52     }

On peut voir dans le code source qu'il y a trois endroits où return bagEntry apparaît, donc l'emprunt d'éléments depuis ConcurrentBag provient de trois sources.

Étape 1 : Obtenir depuis ThreadLocal

Lorsqu'un thread emprunte une connexion depuis ConcurrentBag, il crée un objet ThreadLocal avec une valeur List (taille par défaut 16). Chaque fois que le client obtient une connexion depuis ConcurrentBag, il essaie d'abord d'obtenir une connexion depuis ThreadLocal. Si cela échoue, il passe à l'étape suivante.

Lorsque le client restitue une connexion à ConcurrentBag, il vérifie d'abord s'il y a d'autres clients en attente d'une connexion. Si oui, il donne la connexion à un autre client. S'il n'y a pas de client en attente, il stocke la connexion dans ThreadLocal. Chaque ThreadLocal peut stocker jusqu'à 50 connexions.

Conseil : L'utilisation de ThreadLocal peut présenter un risque de fuite de mémoire. ConcurrentBag a un attribut boolean weakThreadLocals. Lorsqu'il est true, les références dans ThreadLocal sont des références faibles, qui seront récupérées par le GC en cas de manque de mémoire, évitant ainsi les fuites de mémoire.

Étape 2 : Obtenir depuis sharedList

Si l'obtention de la connexion depuis ThreadLocal échoue, on essaie à nouveau d'obtenir depuis sharedList. sharedList contient les objets PoolEntry initialisés. ConcurrentBag initialise un nombre spécifié d'objets PoolEntry et les stocke dans sharedList lors de son initialisation.

Le constructeur de ConcurrentBag est le suivant :

 1    /** ConcurrentBag
 2      * IBagStateListener écouteur d'état du bag, HikariPool implémente l'interface IBagStateListener
 3      * donc l'écouteur passé dans le constructeur est en fait l'objet HikariPool
 4      * */
 5     public ConcurrentBag(final IBagStateListener listener)
 6     {
 7         this.listener = listener;
 8         // Utilisation de références faibles
 9         this.weakThreadLocals = useWeakThreadLocals();
10         // Initialisation de la file d'attente de blocage
11         this.handoffQueue = new SynchronousQueue<>(true);
12         // Initialisation du nombre de connexions en attente
13         this.waiters = new AtomicInteger();
14         // Initialisation de sharedList
15         this.sharedList = new CopyOnWriteArrayList<>();
16         if (weakThreadLocals) {
17             this.threadList = ThreadLocal.withInitial(() -> new ArrayList<>(16));
18         }
19         else {
20             this.threadList = ThreadLocal.withInitial(() -> new FastList<>(IConcurrentBagEntry.class, 16));
21         }
22     }

L'objet HikariPool contient un objet ConcurrentBag. Lors de l'initialisation de HikariPool, l'objet ConcurrentBag est créé. Le constructeur de HikariPool est le suivant :

 1 public HikariPool(final HikariConfig config)
 2     {
 3         super(config);
 4 
 5         // Initialisation de l'objet ConcurrentBag
 6         this.connectionBag = new ConcurrentBag<>(this);
 7         // Création de l'objet SuspendResumeLock
 8         this.suspendResumeLock = config.isAllowPoolSuspension() ? new SuspendResumeLock() : SuspendResumeLock.FAUX_LOCK;
 9         /** Initialisation du pool de threads, houseKeeping peut être compris comme le maintien de l'espace suffisant,
10         l'espace étant le pool de connexions. Ce pool de threads sert à maintenir un nombre approprié de connexions dans le pool */
11         this.houseKeepingExecutorService = initializeHouseKeepingExecutorService();
12 
13         /** Configuration des propriétés */
14         checkFailFast();
15 
16         if (config.getMetricsTrackerFactory() != null) {
17             setMetricsTrackerFactory(config.getMetricsTrackerFactory());
18         }
19         else {
20             setMetricRegistry(config.getMetricRegistry());
21         }
22 
23         setHealthCheckRegistry(config.getHealthCheckRegistry());
24 
25         handleMBeans(this, true);
26 
27         ThreadFactory threadFactory = config.getThreadFactory();
28         /** Créer une file d'attente de blocage de type chaîne liée basée sur la taille maximale de connexion configurée */
29         LinkedBlockingQueue<Runnable> addQueue = new LinkedBlockingQueue<>(config.getMaximumPoolSize());
30         this.addConnectionQueue = unmodifiableCollection(addQueue);
31         /** Initialiser le pool de threads pour la création de connexions */
32         this.addConnectionExecutor = createThreadPoolExecutor(addQueue, poolName + " connection adder", threadFactory, new ThreadPoolExecutor.DiscardPolicy());
33         /** Initialiser le pool de threads pour la fermeture de connexions */
34         this.closeConnectionExecutor = createThreadPoolExecutor(config.getMaximumPoolSize(), poolName + " connection closer", threadFactory, new ThreadPoolExecutor.CallerRunsPolicy());
35 
36         this.leakTaskFactory = new ProxyLeakTaskFactory(config.getLeakDetectionThreshold(), houseKeepingExecutorService);
37         /** Créer une tâche pour maintenir le nombre de connexions dans le pool */
38         this.houseKeeperTask = houseKeepingExecutorService.scheduleWithFixedDelay(new HouseKeeper(), 100L, housekeepingPeriodMs, MILLISECONDS);
39 
40         if (Boolean.getBoolean("com.zaxxer.hikari.blockUntilFilled") && config.getInitializationFailTimeout() > 1) {
41             addConnectionExecutor.setCorePoolSize(Runtime.getRuntime().availableProcessors());
42             addConnectionExecutor.setMaximumPoolSize(Runtime.getRuntime().availableProcessors());
43 
44             final long startTime = currentTime();
45             while (elapsedMillis(startTime) < config.getInitializationFailTimeout() && getTotalConnections() < config.getMinimumIdle()) {
46                 quietlySleep(MILLISECONDS.toMillis(100));
47             }
48 
49             addConnectionExecutor.setCorePoolSize(1);
50             addConnectionExecutor.setMaximumPoolSize(1);
51         }
52     }

Il y a une tâche planifiée houseKeeperTask. Cette tâche planifiée sert à détecter périodiquement le nombre de connexions dans le pool de connexions. Le contenu exécuté est la méthode run de HouseKeep, dont la logique est la suivante :

 1 private final class HouseKeeper implements Runnable
 2     {
 3         private volatile long previous = plusMillis(currentTime(), -housekeepingPeriodMs);
 4 
 5         @Override
 6         public void run()
 7         {
 8             try {
 9                 /** Lire la configuration du pool de connexions */
10                 connectionTimeout = config.getConnectionTimeout();
11                 validationTimeout = config.getValidationTimeout();
12                 leakTaskFactory.updateLeakDetectionThreshold(config.getLeakDetectionThreshold());
13                 catalog = (config.getCatalog() != null && !config.getCatalog().equals(catalog)) ? config.getCatalog() : catalog;
14 
15                 final long idleTimeout = config.getIdleTimeout();
16                 final long now = currentTime();
17 
18                 // Détection du temps rétrograde, autorisant +128ms selon la norme NTP.
19                 if (plusMillis(now, 128) < plusMillis(previous, housekeepingPeriodMs)) {
20                     logger.warn("{} - Changement d'horloge rétrograde détecté (delta du gardien={}), retrait des connexions du pool.",
21                             poolName, elapsedDisplayString(previous, now));
22                     previous = now;
23                     /** Fermer les connexions marquées pour rejet dans le pool */
24                     softEvictConnections();
25                     return;
26                 }
27                 else if (now > plusMillis(previous, (3 * housekeepingPeriodMs) / 2)) {
28                     // Pas de retrait pour le mouvement d'horloge avant, cela accélère simplement la retraite des connexions de toute façon
29                     logger.warn("{} - Famine de thread ou saut d'horloge détecté (delta du gardien={}).", poolName, elapsedDisplayString(previous, now));
30                 }
31 
32                 previous = now;
33 
34                 String afterPrefix = "Pool ";
35                 if (idleTimeout > 0L && config.getMinimumIdle() < config.getMaximumPoolSize()) {
36                     logPoolState("Avant le nettoyage ");
37                     afterPrefix = "Après le nettoyage  ";
38 
39                     /** Obtenir la collection de connexions non utilisées dans le pool */
40                     final List<PoolEntry> notInUse = connectionBag.values(STATE_NOT_IN_USE);
41                     int toRemove = notInUse.size() - config.getMinimumIdle();
42                     for (PoolEntry entry : notInUse) {
43                         /** Si la connexion inactive dépasse le délai d'inactivité maximal, fermer la connexion inactive */
44                         if (toRemove > 0 && elapsedMillis(entry.lastAccessed, now) > idleTimeout && connectionBag.reserve(entry)) {
45                             closeConnection(entry, "(la connexion a dépassé le délai d'inactivité)");
46                             toRemove--;
47                         }
48                     }
49                 }
50 
51                 logPoolState(afterPrefix);
52                 /** Remplir le pool, maintenir au moins le nombre minimum de connexions */
53                 fillPool(); // Essayer de maintenir les connexions minimales
54             }
55             catch (Exception e) {
56                 logger.error("Exception inattendue dans la tâche de maintenance", e);
57             }
58         }
59     }

Cette tâche planifiée sert principalement à maintenir le nombre de connexions dans le pool. Elle doit d'abord fermer les connexions marquées pour rejet, puis fermer les connexions inactives ayant dépassé leur délai, et enfin, lorsque le nombre de connexions dans le pool est inférieur à la valeur minimale, compléter le pool de connexions. L'initialisation des connexions dans le pool est donc réalisée dans la méthode fillPool. Le code source de la méthode fillPool est le suivant :

 1 /** Remplir le pool de connexions */
 2     private synchronized void fillPool()
 3     {
 4         /**
 5          *  Calculer le nombre de connexions à ajouter
 6          *  config.getMaximumPoolSize - getTotalConnections() représente le nombre maximal de connexions - le nombre actuel de connexions = nombre maximal de connexions pouvant encore être créées
 7          *  config.getMinimumIdle() - getIdleConnections() représente la valeur minimale du pool - le nombre de connexions inactuelles actuelles = nombre de connexions pouvant être créées actuellement
 8          *  Math.min calcule le nombre minimum de connexions nécessaires - addConnectionQueue.size() = nombre de tâches de création de connexions encore nécessaires
 9          * */
10         final int connectionsToAdd = Math.min(config.getMaximumPoolSize() - getTotalConnections(), config.getMinimumIdle() - getIdleConnections())
11                 - addConnectionQueue.size();
12         for (int i = 0; i < connectionsToAdd; i++) {
13             /** Soumettre une tâche de création de connexion au pool de threads de création de connexions */
14             addConnectionExecutor.submit((i < connectionsToAdd - 1) ? poolEntryCreator : postFillPoolEntryCreator);
15         }
16     }

On calcule d'abord le nombre de connexions à créer, puis on soumet des tâches au pool de threads de création de connexions. La dernière tâche soumise est postFillPoolEntryCreator, qui n'a pas de différence essentielle avec poolEntryCreator, seulement les journaux imprimés sont différents.

La logique de création de l'objet PoolEntry par PoolEntryCreator est la suivante :

 1 /** Thread de création d'objets PoolEntry */
 2     private final class PoolEntryCreator implements Callable<Boolean> {
 3         /**
 4          * Préfixe de journalisation
 5          */
 6         private final String loggingPrefix;
 7 
 8         PoolEntryCreator(String loggingPrefix) {
 9             this.loggingPrefix = loggingPrefix;
10         }
11 
12         @Override
13         public Boolean call() {
14             long sleepBackoff = 250L;
15             /** 1. Le pool de connexions est dans un état normal et une connexion doit être créée */
16             while (poolState == POOL_NORMAL && shouldCreateAnotherConnection()) {
17                 /** 2. Créer un objet PoolEntry */
18                 final PoolEntry poolEntry = createPoolEntry();
19                 if (poolEntry != null) {
20                     /** 3. Ajouter l'objet PoolEntry à sharedList dans l'objet ConcurrentBag */
21                     connectionBag.add(poolEntry);
22                     logger.debug("{} - Connexion ajoutée {}", poolName, poolEntry.connection);
23                     if (loggingPrefix != null) {
24                         logPoolState(loggingPrefix);
25                     }
26                     return Boolean.TRUE;
27                 }
28                 /** Dormir pendant un certain temps */
29                 quietlySleep(sleepBackoff);
30                 sleepBackoff = Math.min(SECONDS.toMillis(10), Math.min(connectionTimeout, (long) (sleepBackoff * 1.5)));
31             }
32             // Pool suspendu ou arrêté ou à taille maximale
33             return Boolean.FALSE;
34         }
35     }

La méthode createPoolEntry est la suivante :

 1 /** Créer un objet PoolEntry */
 2     private PoolEntry createPoolEntry()
 3     {
 4         try {
 5             /** 1. Initialiser l'objet PoolEntry, d'abord créer un objet Connection et le passer au constructeur de PoolEntry */
 6             final PoolEntry poolEntry = newPoolEntry();
 7             /** 2. Obtenir la durée de vie maximale de la connexion */
 8             final long maxLifetime = config.getMaxLifetime();
 9             if (maxLifetime > 0) {
10                 /** 3. Obtenir une valeur aléatoire pour éviter que les PoolEntry ne soient créés et supprimés en même temps, ajouter un décalage aléatoire pour écarter le temps */
11                 final long variance = maxLifetime > 10_000 ? ThreadLocalRandom.current().nextLong( maxLifetime / 40 ) : 0;
12                 final long lifetime = maxLifetime - variance;
13                 /** 4. Ajouter une tâche planifiée à PoolEntry, lorsque l'objet PoolEntry atteint la durée de vie maximale, déclencher la tâche planifiée pour marquer la connexion comme rejetée */
14                 poolEntry.setFutureEol(houseKeepingExecutorService.schedule(
15                         () -> {
16                             /** 5. Atteindre la durée de vie maximale, rejeter la connexion */
17                             if (softEvictConnection(poolEntry, "(la connexion a dépassé sa durée de vie maximale)", false /* pas propriétaire */)) {
18                                 /** 6. Après avoir rejeté une connexion, appeler addBagItem pour compléter avec un nouvel objet PoolEntry */
19                                 addBagItem(connectionBag.getWaitingThreadCount());
20                             }
21                         },
22                         lifetime, MILLISECONDS));
23             }
24 
25             return poolEntry;
26         }
27         /** Capture d'exception */
28         catch (ConnectionSetupException e) {
29             if (poolState == POOL_NORMAL) { // on vérifie POOL_NORMAL pour éviter un déluge de messages si shutdown() s'exécute en parallèle
30                 logger.error("{} - Erreur levée lors de l'acquisition d'une connexion depuis la source de données", poolName, e.getCause());
31                 lastConnectionFailure.set(e);
32             }
33             return null;
34         }
35         catch (SQLException e) {
36             if (poolState == POOL_NORMAL) { // on vérifie POOL_NORMAL pour éviter un déluge de messages si shutdown() s'exécute en parallèle
37                 logger.debug("{} - Impossible d'acquérir une connexion depuis la source de données", poolName, e);
38                 lastConnectionFailure.set(new ConnectionSetupException(e));
39             }
40             return null;
41         }
42         catch (Exception e) {
43             if (poolState == POOL_NORMAL) { // on vérifie POOL_NORMAL pour éviter un déluge de messages si shutdown() s'exécute en parallèle
44                 logger.error("{} - Erreur levée lors de l'acquisition d'une connexion depuis la source de données", poolName, e);
45                 lastConnectionFailure.set(new ConnectionSetupException(e));
46             }
47             return null;
48         }
49     }

On crée d'abord un nouvel objet PoolEntry. Lors de la création de PoolEntry, un objet Connection est créé. De plus, si une durée de vie maximale est définie pour la connexion, chaque PoolEntry reçoit une tâche planifiée. Pour éviter que plusieurs PoolEntry ne soient créés et supprimés en même temps, la durée de vie maximale de chaque PoolEntry est différente. Lorsqu'un PoolEntry atteint sa durée de vie maximale, la méthode softEvictConnection est déclenchée pour marquer le PoolEntry comme devant être rejeté. De plus, comme un PoolEntry a été rejeté, il faut appeler la méthode addBagItem pour compléter avec un nouvel objet PoolEntry.

Étape 3 : Créer un nouvel élément via IBagStateListener

Comme indiqué à l'étape 2, IBagStateListener a principalement une méthode addBagItem. HikariPool implémente la méthode addBagItem, dont le code source est le suivant :

1 public void addBagItem(final int waiting)
2     {
3         /** Vérifier si une connexion doit être créée */
4         final boolean shouldAdd = waiting - addConnectionQueue.size() >= 0; // Oui, >= est intentionnel.
5         if (shouldAdd) {
6             /** Soumettre une tâche de création de connexion au pool de threads de création de connexions */
7             addConnectionExecutor.submit(poolEntryCreator);
8         }
9     }

En résumé :

L'obtention d'une connexion depuis ConcurrentBag se fait en trois étapes. D'abord, obtenir depuis le ThreadLocal du thread actuel ; s'il n'y en a pas, obtenir depuis sharedList ; sharedList peut être considéré comme le pool de connexions mis en cache par ConcurrentBag. Chaque fois qu'un objet PoolEntry est créé, il est ajouté à sharedList. Si aucune connexion dans sharedList n'est disponible, il faut soumettre une tâche de création de connexion via IBagStateListener au pool de threads de création de connexions pour exécuter la création de nouvelles connexions.

Une fois la nouvelle connexion créée avec succès, l'objet PoolEntry est ajouté à la file d'attente de blocage sans capacité handoffQueue, et les threads en attente essaient continuellement d'obtenir une connexion depuis la file d'attente jusqu'à l'obtention réussie ou le dépassement du délai d'attente.

2.2. Libération d'une connexion

Lorsque le client libère une connexion, il appelle la méthode close de la connexion. Dans Hikari, Connection utilise un objet ProxyConnection. Lors de l'appel à la méthode close, la méthode de récupération de l'objet PoolEntry associé est appelée. La méthode recycle de PoolEntry est la suivante :

1 void recycle(final long lastAccessed)
2     {
3         if (connection != null) {
4             this.lastAccessed = lastAccessed;
5             /** Appeler la méthode recycle de HikariPool pour récupérer l'objet PoolEntry actuel */
6             hikariPool.recycle(this);
7         }
8     }

1 void recycle(final PoolEntry poolEntry)
2     {
3         metricsTracker.recordConnectionUsage(poolEntry);
4         /** Appeler la méthode de récupération de ConcurrentBag */
5         connectionBag.requite(poolEntry);
6     }

 1 /** Méthode de récupération d'un élément */
 2     public void requite(final T bagEntry)
 3     {
 4         /** 1. Définir l'état comme non utilisé */
 5         bagEntry.setState(STATE_NOT_IN_USE);
 6 
 7         /** 2. S'il y a des threads en attente, donner la priorité à l'élément aux threads en attente */
 8         for (int i = 0; waiters.get() > 0; i++) {
 9             /** 2.1. Ajouter l'élément à la file d'attente de blocage sans bornes, attendre que d'autres threads l'obtiennent */
10             if (bagEntry.getState() != STATE_NOT_IN_USE || handoffQueue.offer(bagEntry)) {
11                 return;
12             }
13             else if ((i & 0xff) == 0xff) {
14                 parkNanos(MICROSECONDS.toNanos(10));
15             }
16             else {
17                 /** Le thread actuel ne continue pas son exécution */
18                 yield();
19             }
20         }
21         /** 3. Si la connexion n'est pas utilisée par d'autres threads, l'ajouter à la variable de thread local ThreadLocal */
22         final List<Object> threadLocalList = threadList.get();
23         if (threadLocalList.size() < 50) {
24             threadLocalList.add(weakThreadLocals ? new WeakReference<>(bagEntry) : bagEntry);
25         }
26     }

La récupération d'une connexion appelle finalement la méthode requite de ConcurrentBag. La logique de la méthode n'est pas complexe. Elle définit d'abord l'état de l'élément PoolEntry comme non utilisé. Ensuite, elle vérifie s'il y a des threads en attente d'une connexion. Si oui, elle ajoute la connexion à la file d'attente de blocage sans bornes pour que les threads en attente puissent l'obtenir depuis la file d'attente.

Si aucun thread n'attend de connexion, la connexion est ajoutée à la variable de thread local ThreadLocal pour que le thread actuel puisse l'obtenir directement la prochaine fois.

III. Pourquoi HikariCP est-il si performant ?

  1. Il utilise FastList personnalisé pour remplacer ArrayList. La méthode get de FastList supprime la vérification de plage rangeCheck, et la méthode remove scanne à partir de la fin plutôt que du début, car l'ouverture et la fermeture des connexions se font généralement dans l'ordre inverse.
  2. À l'initialisation, deux objets HikariPool sont créés. L'un est défini avec le mot-clé final pour éviter l'initialisation lors de l'obtention d'une connexion, car l'initialisation lors de l'obtenir nécessiterait un traitement synchrone.
  3. Hikari crée des connexions via la technologie de génération de bytecode dynamique Javassist, ce qui est plus performant.
  4. Lors de l'obtention d'une connexion depuis le pool, un cache est ajouté dans threadLocal pour le même thread, évitant les opérations concurrentes pour le même thread lors de l'obtention d'une connexion.
  5. La plus grande caractéristique de Hikari est de réduire autant que possible la concurrence des verrous en situation de forte concurrence.

Étiquettes: Java pool de connexions HikariCP SpringBoot performance

Publié le 22 juin à 19h35