Invalidación de caché consciente de las transacciones en PostgreSQL

Eliminar una entrada de caché no basta si una lectura anterior puede volver a llenarla después de la eliminación. Supón que un lector empieza a cargar una fila antigua, un escritor confirma un valor nuevo e invalida la clave y después ese loader anterior publica su resultado. La caché también debe rechazar esa publicación tardía.

La implementación 2.0 pone una valla a las claves o relaciones afectadas en el recorrido de escritura de la base de datos. Un llenado lleva información de generación para poder rechazarlo después de una invalidación. Las entradas positivas almacenadas también llevan información de visibilidad de la tupla. Una entrada no elegible vuelve a leer la tabla de origen. Consulta la referencia técnica para conocer el contrato.

Lo que ven los lectores durante una actualización Con READ COMMITTED, empieza con la revisión cero. Una transacción la actualiza a uno. El escritor ve uno mediante una lectura de la tabla de origen, mientras otra sesión sigue viendo cero. Tras COMMIT, una nueva sentencia de la otra sesión ve uno. Tras ROLLBACK, sigue viendo cero. Fila confirmadarevision = 0 BEGIN; UPDATE Transacción abierta El escritor lee 1 La otra sesión lee 0 COMMIT ROLLBACK Siguiente lecturarevision = 1 Siguiente lecturarevision = 0
READ COMMITTED, comenzando en la revisión 0. «Siguiente lectura» significa una nueva sentencia en la otra sesión después de COMMIT o ROLLBACK. El escritor lee su propio cambio mediante la tabla de origen.

Prueba con dos sesiones

Inicia la demo local. Abre este comando en dos terminales:

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

En la sesión A, lee la fila 42 y anota su revisión; después vuelve a leerla para calentarla:

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';

En la sesión B, actualiza la fila pero deja abierta la transacción:

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 ve su propio incremento. Esta lectura omite la caché. Repite la consulta de A mientras B siga abierta: A debe seguir viendo la revisión confirmada, no el valor no confirmado de B. En B, ejecuta ROLLBACK; otra consulta en A debe seguir devolviendo la revisión original.

Ahora ejecuta en B:

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

Una consulta iniciada en A después de ese commit debe devolver la revisión incrementada. Este es el límite relevante: una sentencia antigua que ya está en ejecución no tiene que cambiar al snapshot tomado después de iniciarse. PostgreSQL documenta ese comportamiento bajo Read Committed.

La prueba ejecutable de Node.js afirma estas observaciones con conexiones separadas.

Casos que omiten deliberadamente la caché

REPEATABLE READ, SERIALIZABLE, la recuperación, la ejecución paralela y las transacciones que han escrito datos mapeados usan el recorrido de la tabla de origen. Una fila sobredimensionada puede devolverse correctamente sin almacenarse en caché. Una tasa de aciertos cercana a cero no implica necesariamente una instalación fallida: comprueba la carga de trabajo y los contadores de omisiones.

Cuando la aplicación necesite SELECT ... FOR UPDATE, usa la operación normal de PostgreSQL; mget no sustituye el bloqueo de filas.

Inspecciona la causa de un fallo

Usa local_cache.stats() y local_cache.health() como administrador. Compara instantáneas de contadores antes y después de una prueba controlada. Mantén separados los contadores de SQL mget y los de RESP. Después de un DDL intencionado, sigue el procedimiento documentado reconcile_table o reconcile_all en vez de suponer que un mapeo asociado anteriormente aún describe la tabla modificada.

Actualizado .