Invalidation du cache PostgreSQL tenant compte des transactions

Supprimer une entrée du cache ne suffit pas si une lecture antérieure peut la remplir à nouveau après la suppression. Supposons qu’un lecteur commence à charger une ancienne ligne, qu’un écrivain valide une nouvelle valeur et invalide la clé, puis que cet ancien chargeur publie son résultat. Le cache doit aussi rejeter cette publication tardive.

L’implémentation 2.0 clôt les clés ou relations concernées sur le chemin d’écriture de la base. Un remplissage transporte des informations de génération afin de pouvoir être rejeté après une invalidation. Les entrées positives du cache transportent aussi des informations de visibilité du tuple. Une entrée inéligible revient à une lecture de la table source. Consultez la référence technique pour le contrat.

Ce que voient les lecteurs pendant une mise à jour Sous READ COMMITTED, commencez avec la révision zéro. Une transaction la met à jour à un. L'écrivain voit un par une lecture de la table source, tandis qu'une autre session voit encore zéro. Après COMMIT, une nouvelle instruction de l'autre session voit un. Après ROLLBACK, elle voit toujours zéro. Ligne validéerevision = 0 BEGIN; UPDATE Transaction ouverte L'écrivain lit 1 L'autre session lit 0 COMMIT ROLLBACK Lecture suivanterevision = 1 Lecture suivanterevision = 0
READ COMMITTED, à partir de la révision 0. « Lecture suivante » désigne une nouvelle instruction dans l'autre session après COMMIT ou ROLLBACK. L'écrivain lit sa propre modification via la table source.

Tester avec deux sessions

Démarrez la démo locale. Ouvrez cette commande dans deux terminaux :

docker compose -f examples/compose.yaml exec postgres \
  psql -X -v ON_ERROR_STOP=1 -U demo -d pglc_demo

Dans la session A, lisez la ligne 42 et notez sa révision, puis relisez-la pour la réchauffer :

SELECT (local_cache.mget('public.items'::regclass, ARRAY[42]::bigint[]))[1]::jsonb ->> 'revision';
SELECT (local_cache.mget('public.items'::regclass, ARRAY[42]::bigint[]))[1]::jsonb ->> 'revision';

Dans la session B, mettez à jour la ligne mais laissez la transaction ouverte :

BEGIN;
UPDATE public.items SET revision = revision + 1 WHERE id = 42;
SELECT (local_cache.mget('public.items'::regclass, ARRAY[42]::bigint[]))[1]::jsonb ->> 'revision';

B voit sa propre incrémentation. Cette lecture contourne le cache. Répétez la requête de A pendant que B reste ouverte : A doit toujours voir la révision validée, et non la valeur non validée de B. Dans B, exécutez ROLLBACK ; une autre requête dans A doit encore renvoyer la révision originale.

Exécutez maintenant dans B :

BEGIN;
UPDATE public.items SET revision = revision + 1 WHERE id = 42;
COMMIT;

Une requête lancée dans A après ce commit doit renvoyer la révision incrémentée. C’est la limite importante : une instruction déjà en cours n’a pas à basculer vers un snapshot pris après son démarrage. PostgreSQL documente ce comportement pour Read Committed.

Le test Node.js exécutable vérifie ces observations avec des connexions séparées.

Cas qui contournent délibérément le cache

REPEATABLE READ, SERIALIZABLE, la récupération, l’exécution parallèle et les transactions ayant écrit des données mappées utilisent le chemin de la table source. Une ligne trop volumineuse peut être renvoyée avec succès sans être mise en cache. Un taux de hits proche de zéro ne signifie pas forcément que l’installation a échoué : vérifiez la charge et les compteurs de contournement.

Lorsque l’application a besoin de SELECT ... FOR UPDATE, utilisez l’opération PostgreSQL ordinaire ; mget ne remplace pas le verrouillage de lignes.

Inspecter la cause d’un miss

Utilisez local_cache.stats() et local_cache.health() en tant qu’administrateur. Comparez les snapshots de compteurs avant et après un test contrôlé. Gardez les compteurs SQL mget séparés des compteurs RESP. Après un DDL intentionnel, suivez la procédure documentée reconcile_table ou reconcile_all au lieu de supposer qu’un mapping précédemment attaché décrit encore la table modifiée.

Mis à jour .