Qu'est-ce que la piscine hikari ?
Cette simple question dans un post sur BlueSky m'a amené à une explication que j'ai trouvée vraiment cool. Je suis venu ici pour le terminer.
Dans le contexte précis, on parlait de Hikari Connection Pool. Mais si Hikari est un pool de connexions, que serait un « pool » ?
Avant d'expliquer ce qu'est HikariCP, nous devons expliquer ce qu'est un pool de connexions. Et pour expliquer le pool de connexions, nous devons expliquer le pool.
Devrions-nous utiliser une analogie économique pour cela ? Une analogie économique historique pleine de défauts et d’inexactitudes avec le monde réel, mais allez, suspendez vite votre incrédulité juste pour l’explication ! Il est autonome.
Imaginez que vous êtes un seigneur/une dame à l'époque médiévale. Vous détenez les outils pour effectuer le travail des paysans. Et vous voulez qu'ils fonctionnent. Alors comment garantir cela ? Si les outils sont les vôtres ? Il vous faudra livrer les outils aux paysans, simple.
Alors imaginez la situation : votre paysan a besoin d'une houe pour désherber la terre, alors il s'y rend et vous demande une houe. Vous lui donnerez la houe et vous vivrez. Mais s'il ne le rend pas, qu'en est-il de son stock de putes ? Un jour, ça finira...
Une alternative à la remise de la houe est de faire fabriquer une houe. Vous êtes le seigneur/dame de ces terres, vous avez donc accès au forgeron pour fondre le métal en forme de houe et le mettre dans un manche. Mais ce n’est pas quelque chose que l’on peut produire immédiatement sans que le paysan soit assis dans une salle d’attente. Pour créer cette nouvelle fonctionnalité, vous avez besoin d'une énorme quantité de temps et d'énergie.
Désormais, si le paysan rend la houe à la fin de la journée, elle devient disponible pour qu'un autre paysan puisse l'utiliser le lendemain.
Ici, vous contrôlez le pool de putes. Le pool est un modèle de conception qui indique que vous pouvez effectuer les actions suivantes :
Autres éléments courants à avoir dans les pools d'objets :
Eh bien, rapprochons-nous de HikariCP. Parlons ici des connexions aux bases de données en Java.
En java, on demande d'établir une connexion à la base de données. Il existe l'option de connexion directe, dont vous avez besoin pour comprendre directement quelles classes appeler et certains détails, ou bien simplement profiter de l'option de découverte de service.
A priori, pour utiliser la découverte de services, le fournisseur de services fait un moyen d'enregistrer ce qu'il fournit, puis la « découverte de services » le suit pour voir qui pourrait répondre à cette demande.
J'ai eu un cas où je devais établir des connexions JDBC pour parler à la base de données. Cependant, mon pilote JDBC n'acceptait pas l'utilisation de valeurs nulles comme valeurs, uniquement des valeurs nulles directement dans les requêtes. Alors qu'est-ce que j'ai fait ? Un chauffeur au dessus d'un chauffeur !
L'idée générale était la suivante. Imaginez que j'ai cette requête dans laquelle je souhaite insérer des valeurs :
INSERT INTO some_table (id, content, parent) VALUES (?, ?, ?)
Imaginez maintenant que je m'occupe de la première insertion de cette valeur dans la banque. Pour ce faire, je dois le laisser avec ID=1, CONTENT=first et PARENT=null car, après tout, il n'y a pas d'enregistrement parent comme celui-là (c'est le premier, après tout).
Ce qui serait fait naturellement :
try (final var pstmt = conn.prepareStatement( """ INSERT INTO some_table (id, content, parent) VALUES (?, ?, ?) """)) { pstmt.setInt(1, 1); pstmt.setString(2, "first"); pstmt.setNull(3, Types.INTGEGER); // java.sql.Types pstmt.executeUpdate(); // de fato altere o valor }
Je veux continuer à l'utiliser de cette façon, après tout, c'est la manière idiomatique de l'utiliser. Et selon CUPID, le I vient de « idiomatique ». L'idée d'avoir un code idiomatique est justement de "réduire la charge mentale inutile".
Pour résoudre ce problème, mon choix était : laisser préparerStatement jusqu'au dernier moment avant d'exécuterexecuteUpdate. Je stocke donc toutes les valeurs nulles à appliquer et, quand je réalise que j'ai réellement besoin d'une valeur nulle, j'exécute une substitution de chaîne et génère une nouvelle requête, et cette nouvelle requête sera effectivement exécutée.
Dans ce cas, je commence par :
INSERT INTO some_table (id, content, parent) VALUES (?, ?, ?)
Donc, je dois saisir ces valeurs :
INSERT INTO some_table (id, content, parent) VALUES (?, ?, ?) -- 1, 'first', NULL
Mais je ne peux pas réellement utiliser le nul, alors je crée une clé pour identifier que la troisième place est un nul :
-- (value, value, NULL) INSERT INTO some_table (id, content, parent) VALUES (?, ?, NULL) -- 1, 'first'
Et dans ce cas je prépare cette nouvelle chaîne et place les arguments selon ce qui a été demandé.
D'accord, cela dit, comment pourrais-je indiquer à mon application que je devais utiliser mon pilote JDBC ? Comment je me suis inscrit ?
Le projet en question est Pstmt Null Safe. Fondamentalement, il existe une magie dans le chargeur de classe Java qui, lors du chargement d'un fichier jar, recherche un dossier de métadonnées appelé META-INF. Et dans le cas du pilote JDBC, META-INF/services/java.sql.Driver, et je l'ai noté avec la classe qui implémente java.sql.Driver : br.com.softsite.pstmtnullsafe.jdbc.PstmtNullSafeDriver.
Selon la documentation java.sql.Driver, chaque pilote doit créer une instance de lui-même et s'enregistrer auprès de DriverManager. Je l'ai implémenté comme ceci :
INSERT INTO some_table (id, content, parent) VALUES (?, ?, ?)
Le bloc statique se charge tout seul. Et comment savoir quelle connexion doit être gérée par mon chauffeur ? L'appel est effectué via DriverManager#getConnection(String url). Nous avons l'URL pour demander au chauffeur s'il accepte la connexion. La convention (là encore, la manière idiomatique de l'utiliser) est de le préfixer au schéma d'URL. Comme je veux que mon chauffeur se connecte au-dessus d'un autre chauffeur, je l'ai fait en utilisant ce schéma :
try (final var pstmt = conn.prepareStatement( """ INSERT INTO some_table (id, content, parent) VALUES (?, ?, ?) """)) { pstmt.setInt(1, 1); pstmt.setString(2, "first"); pstmt.setNull(3, Types.INTGEGER); // java.sql.Types pstmt.executeUpdate(); // de fato altere o valor }
Donc, pour effectuer les tests, je me suis connecté à SQLite, et j'ai utilisé l'indicateur Xerial pour demander une connexion en mémoire via l'URI de connexion :
INSERT INTO some_table (id, content, parent) VALUES (?, ?, ?)
Pour "envelopper" la connexion, ma convention indique que je ne répète pas le jdbc:, donc :
INSERT INTO some_table (id, content, parent) VALUES (?, ?, ?) -- 1, 'first', NULL
Disséquer l'URI ci-dessus :
-- (value, value, NULL) INSERT INTO some_table (id, content, parent) VALUES (?, ?, NULL) -- 1, 'first'
D'accord, et comment indiquez-vous cela ? Le Driver#acceptsURL doit renvoyer true si je peux ouvrir la connexion. Je pourrais juste faire ça :
public static final PstmtNullSafeDriver instance; static { instance = new PstmtNullSafeDriver(); try { DriverManager.registerDriver(instance); } catch (SQLException e) { e.printStackTrace(); } }
Mais qu'est-ce que cela indiquerait si j'essayais de charger un pilote inexistant ? Rien, cela poserait un problème à un autre moment. Et ce n'est pas bien, l'idéal serait de planter dès le début. Donc pour ça, je vais essayer de charger le pilote par le bas, et si je n'y arrive pas, je renvoie false :
jdbc:pstmt-nullsafe:<url de conexão sem jdbc:> \__/ \____________/ | | | Nome do meu driver Padrão para indicar JDBC
Le code du pilote actuel contient d'autres points qui ne sont pas pertinents pour la discussion ici sur HikariCP, ni sur DataSource, ni sur JDBC ou sur les sujets abordés dans cet article.
Ainsi, lorsque vous demandez une connexion "null safe" à DriverManager, il trouve d'abord mon pilote et mon pilote essaie de manière récursive de vérifier s'il y a la possibilité d'une connexion sous le capot. Confirmé qu'il existe un chauffeur capable de gérer cela, je dis oui, c'est possible.
L'interface de connexion implémente l'interface AutoCloseable. Cela signifie que vous prenez la connexion, l'utilisez comme vous le souhaitez, puis vous fermez la connexion. Il est tout à fait standard que vous utilisiez une certaine indirection avec ceci ou, si vous utilisez la connexion directement, utilisez-la dans un bloc try-with-resources:
jdbc:sqlite::memory:
Maintenant, le processus de création de connexions est un processus coûteux. Et de plus, le processus de découverte de services n'est pas vraiment gratuit. L'idéal serait donc de sauvegarder le driver pour ensuite générer les connexions. Développons cela petit à petit.
Tout d'abord, nous aurons besoin d'un objet que nous pouvons lancer avec le pilote. Il peut facilement s'agir d'un objet global, d'un composant Spring injecté ou quelque chose comme ça. Appelons-le JdbcConnector :
jdbc:pstmt-nullsafe:sqlite::memory:
Une implémentation possible pour getJdbcConnection() consiste à s'appuyer sur un état englobé par cette fonction :
INSERT INTO some_table (id, content, parent) VALUES (?, ?, ?)
Tout va bien jusqu’à présent. Mais... vous vous souvenez de l'exemple initial où le paysan demande une houe dans la piscine à outils ? Alors... Devons-nous en tenir compte ? Au lieu de fermer réellement la connexion, nous pouvons rétablir la connexion au pool. Par souci d'exactitude, je me protégerai contre plusieurs accès simultanés, mais je ne me soucierai pas ici de l'efficacité.
Supposons ici que j'ai une classe appelée ConnectionDelegator. Il implémente toutes les méthodes Connection, mais il ne fait rien par lui-même, il délègue uniquement à une connexion qui lui est transmise en tant que constructeur. Par exemple, pour la méthode isClosed():
try (final var pstmt = conn.prepareStatement( """ INSERT INTO some_table (id, content, parent) VALUES (?, ?, ?) """)) { pstmt.setInt(1, 1); pstmt.setString(2, "first"); pstmt.setNull(3, Types.INTGEGER); // java.sql.Types pstmt.executeUpdate(); // de fato altere o valor }
Et ainsi de suite pour les autres méthodes. C'est abstrait du simple fait que j'ai envie de me forcer à faire autre chose qu'une simple délégation lorsque je l'utilise.
Eh bien, allons-y. L'idée est qu'une connexion sera demandée, qui peut exister ou non. S'il existe, je l'enveloppe dans cette nouvelle classe pour pouvoir ensuite le renvoyer au pool lorsque je ferme la connexion. Hmmm, donc je vais faire quelque chose dans la méthode close()... Ok, finissons-en d'abord. Laissons getConnection() comme synchronisé pour éviter les problèmes de concurrence :
INSERT INTO some_table (id, content, parent) VALUES (?, ?, ?)
Ok, si j'ai des éléments dans le pool de connexions je les utilise jusqu'à ce qu'il soit vide. Mais il n'est jamais rempli ! Alors allons-nous résoudre ce problème ? A la fermeture, nous pourrons le remettre à la piscine !
INSERT INTO some_table (id, content, parent) VALUES (?, ?, ?) -- 1, 'first', NULL
Ok, maintenant, lorsque vous avez fini d'utiliser la connexion, elle est renvoyée à
la piscine. Cela ne satisfait pas à la documentation de la méthode Connection#close(), car dans la documentation, il est mentionné qu'elle libère toutes les ressources JDBC liées à cette connexion. Cela signifie que je devrais conserver une trace de toutes les instructions, ResultSets, PreparedStatements, etc. Nous pouvons gérer cela en créant une méthode protégée sur ConnectionDelegator appelée closeAllInnerResources(). Et appelez-le close() :
-- (value, value, NULL) INSERT INTO some_table (id, content, parent) VALUES (?, ?, NULL) -- 1, 'first'
Et avec ça nous avons quelque chose qui me renvoie des connexions à la demande et qui a la capacité de former un pool de ressources.
Savez-vous quel nom Java donne à un objet qui fournit des connexions ? Source de données. Et savez-vous ce que Java a à dire d'autre sur les DataSources ? Qu'il existe certains types, conceptuellement parlant. Et parmi ces types, les 2 plus pertinents sont :
Et ici nous passons par le processus de toujours créer des connexions (type de base) ainsi que d'évoluer vers une DataSource
mutué.
HikariCP est une source de données. Plus précisément, une DataSource regroupée. Mais il a une particularité : il est le plus rapide de tous. Pour garantir cette rapidité, dans son pool de connexions à utiliser pendant le cycle de vie de l'application, HikariCP fait un secret : il crée déjà toutes les connexions disponibles. Ainsi, lorsqu'un getConnection arrive, HikariCP n'aura qu'à vérifier le pool de connexions.
Si vous souhaitez approfondir le sujet, vous pouvez consulter cet article sur Baeldung sur le sujet, et également consulter le référentiel sur github.
Ce qui précède est le contenu détaillé de. pour plus d'informations, suivez d'autres articles connexes sur le site Web de PHP en chinois!