MVCC-3. Versiones de fila

Entonces, analizamos los problemas relacionados con el aislamiento e hicimos una digresión sobre la organización de los datos a un nivel bajo . Y finalmente llegó a lo más interesante: a la versión de las líneas.

Titular


Como ya hemos dicho, cada fila puede estar presente simultáneamente en la base de datos en varias versiones. Una versión debe distinguirse de la otra de alguna manera, para este propósito, cada versión tiene dos marcas que determinan el "tiempo" de la acción de esta versión (xmin y xmax). Entre comillas, porque no es el tiempo lo que se usa como tal, sino un contador incremental especial. Y este contador es el número de transacción.

(Como de costumbre, en realidad es más complicado: la cantidad de transacciones no puede aumentar todo el tiempo debido a la capacidad limitada del mostrador. Pero consideraremos estos detalles en detalle cuando lleguemos al congelamiento).

Cuando se crea la línea, xmin se establece en el número de la transacción que ejecutó el comando INSERT, y xmax no se rellena.

Cuando se elimina una fila, el valor xmax de la versión actual se marca con el número de transacción que realizó DELETE.

Cuando se modifica una línea con el comando ACTUALIZAR, se realizan dos operaciones: BORRAR e INSERTAR. En la versión actual de la línea, xmax se establece igual al número de la transacción que realizó ACTUALIZACIÓN. Luego se crea una nueva versión de la misma línea; su valor xmin coincide con el valor xmax de la versión anterior.

Los campos xmin y xmax se incluyen en el encabezado de la versión de la fila. Además de estos campos, el encabezado contiene otros, por ejemplo:

  • infomask: una serie de bits que definen las propiedades de esta versión. Hay bastantes de ellos; los principales los vamos a considerar gradualmente.
  • ctid: un enlace a la siguiente versión más nueva de la misma línea. En la versión más reciente y más actual de la cadena, ctid se refiere a esta versión en sí. El número tiene la forma (x, y), donde x es el número de página, y es el número de serie del puntero en la matriz.
  • mapa de bits de valores indefinidos: marca las columnas de esta versión que contienen un valor indefinido (NULL). NULL no es uno de los valores habituales de los tipos de datos, por lo que el atributo debe almacenarse por separado.

Como resultado, el encabezado es bastante grande: al menos 23 bytes por versión de la cadena, y generalmente más debido al mapa de bits NULL. Si la tabla es "estrecha" (es decir, contiene pocas columnas), la sobrecarga puede tomar más que información útil.

Insertar


Echemos un vistazo más de cerca a cómo se realizan las operaciones de cadena en un nivel bajo, y comencemos con la inserción.

Para los experimentos, cree una nueva tabla con dos columnas y un índice en una de ellas:

=> CREATE TABLE t( id serial, s text ); => CREATE INDEX ON t(s); 

Inserte una línea, después de comenzar la transacción.

 => BEGIN; => INSERT INTO t(s) VALUES ('FOO'); 

Aquí está el número de nuestra transacción actual:

 => SELECT txid_current(); 
  txid_current -------------- 3664 (1 row) 

Echa un vistazo a los contenidos de la página. La función heap_page_items de la extensión pageinspect proporciona información sobre punteros y versiones de fila:

 => SELECT * FROM heap_page_items(get_raw_page('t',0)) \gx 
 -[ RECORD 1 ]------------------- lp | 1 lp_off | 8160 lp_flags | 1 lp_len | 32 t_xmin | 3664 t_xmax | 0 t_field3 | 0 t_ctid | (0,1) t_infomask2 | 2 t_infomask | 2050 t_hoff | 24 t_bits | t_oid | t_data | \x0100000009464f4f 

Tenga en cuenta que la palabra montón (montón) en PostgreSQL se refiere a tablas. Este es otro uso extraño del término: el montón es una estructura de datos bien conocida que no tiene nada que ver con una tabla. Aquí, esta palabra se usa en el sentido de "todo está apilado en un montón", en contraste con los índices ordenados.

La función muestra los datos "tal cual" en un formato que es difícil de leer. Para entenderlo, dejaremos solo una parte de la información y la descifraremos:

 => SELECT '(0,'||lp||')' AS ctid, CASE lp_flags WHEN 0 THEN 'unused' WHEN 1 THEN 'normal' WHEN 2 THEN 'redirect to '||lp_off WHEN 3 THEN 'dead' END AS state, t_xmin as xmin, t_xmax as xmax, (t_infomask & 256) > 0 AS xmin_commited, (t_infomask & 512) > 0 AS xmin_aborted, (t_infomask & 1024) > 0 AS xmax_commited, (t_infomask & 2048) > 0 AS xmax_aborted, t_ctid FROM heap_page_items(get_raw_page('t',0)) \gx 
 -[ RECORD 1 ]-+------- ctid | (0,1) state | normal xmin | 3664 xmax | 0 xmin_commited | f xmin_aborted | f xmax_commited | f xmax_aborted | t t_ctid | (0,1) 

Esto es lo que hicimos:

  • Agregamos un cero al número de índice para llevarlo a la misma forma que t_ctid: (número de página, número de índice).
  • Descifró el estado del puntero lp_flags. Aquí es "normal", esto significa que el puntero realmente se refiere a la versión de la cadena. Otros valores serán considerados más adelante.
  • De todos los bits de información, hasta ahora solo se han asignado dos pares. Los bits xmin_committed y xmin_aborted indican si la transacción con el número xmin está confirmada (cancelada). Dos bits similares se refieren al número de transacción xmax.

Que vemos Cuando inserta una fila en la página de la tabla, aparece un puntero con el número 1, que se refiere a la primera y única versión de la fila.

En la versión de la línea, el campo xmin se llena con el número de la transacción actual. La transacción aún está activa, por lo que ambos bits xmin_committed y xmin_aborted no están establecidos.

El campo ctid de la versión de la fila se refiere a la misma fila. Esto significa que no existe una versión más nueva.

El campo xmax se llena con un número ficticio 0, porque esta versión de la línea no se elimina y es relevante. Las transacciones no prestarán atención a este número, porque el bit xmax_aborted está establecido.

Tomemos un paso más para mejorar la legibilidad agregando bits de información a los números de transacción. Y crearemos una función, ya que necesitaremos la solicitud más de una vez:

 => CREATE FUNCTION heap_page(relname text, pageno integer) RETURNS TABLE(ctid tid, state text, xmin text, xmax text, t_ctid tid) AS $$ SELECT (pageno,lp)::text::tid AS ctid, CASE lp_flags WHEN 0 THEN 'unused' WHEN 1 THEN 'normal' WHEN 2 THEN 'redirect to '||lp_off WHEN 3 THEN 'dead' END AS state, t_xmin || CASE WHEN (t_infomask & 256) > 0 THEN ' (c)' WHEN (t_infomask & 512) > 0 THEN ' (a)' ELSE '' END AS xmin, t_xmax || CASE WHEN (t_infomask & 1024) > 0 THEN ' (c)' WHEN (t_infomask & 2048) > 0 THEN ' (a)' ELSE '' END AS xmax, t_ctid FROM heap_page_items(get_raw_page(relname,pageno)) ORDER BY lp; $$ LANGUAGE SQL; 

De esta forma, queda mucho más claro lo que está sucediendo en el encabezado de la versión de la cadena:

 => SELECT * FROM heap_page('t',0); 
  ctid | state | xmin | xmax | t_ctid -------+--------+------+-------+-------- (0,1) | normal | 3664 | 0 (a) | (0,1) (1 row) 

Se puede obtener información similar, pero sustancialmente menos detallada, de la tabla misma, utilizando las pseudocolumnas xmin y xmax:

 => SELECT xmin, xmax, * FROM t; 
  xmin | xmax | id | s ------+------+----+----- 3664 | 0 | 1 | FOO (1 row) 

Fijación


Al completar con éxito la transacción, debe recordar su estado; tenga en cuenta que está arreglado. Para hacer esto, use una estructura llamada XACT (y antes de la versión 10 se llamaba CLOG (commit log) y este nombre todavía se puede encontrar en diferentes lugares).

XACT no es una tabla de catálogo del sistema; Estos son los archivos en el directorio PGDATA / pg_xact. En ellos, para cada transacción, se asignan dos bits: comprometidos y anulados, exactamente lo mismo que en el encabezado de la versión de la línea. Esta información se divide en varios archivos únicamente por conveniencia, volveremos a este problema cuando consideremos la congelación. Y el trabajo con estos archivos se realiza página por página, como con todos los demás.

Entonces, cuando se confirma una transacción en XACT, el bit comprometido se establece para esta transacción. Y eso es todo lo que sucede durante la confirmación (aunque todavía no estamos hablando del diario de pregrabación).

Cuando cualquier otra transacción acceda a la página de la tabla que acabamos de ver, tendrá que responder algunas preguntas.

  1. ¿Se ha completado la transacción xmin? De lo contrario, la versión generada de la cadena no debería ser visible.
    Dicha verificación se realiza observando otra estructura más, que se encuentra en la memoria compartida de la instancia y se llama ProcArray. Contiene una lista de todos los procesos activos, y para cada uno se indica el número de su transacción actual (activa).
  2. Si se completa, ¿cómo? ¿Por fijación o cancelación? Si se cancela, la versión de la cadena tampoco debería ser visible.
    Para eso es exactamente XACT. Pero, aunque las últimas páginas XACT se almacenan en memorias intermedias en la RAM, no es necesario verificar el XACT cada vez. Por lo tanto, el estado de una transacción una vez aclarado se registra en los bits xmin_committed y xmin_aborted de la versión de la fila. Si se establece uno de estos bits, el estado de la transacción xmin se considera conocido y la próxima transacción ya no tendrá que acceder a XACT.

¿Por qué estos bits no son establecidos por la transacción misma que realiza la inserción? Cuando se produce una inserción, la transacción aún no sabe si se completará con éxito. Y en el momento de la reparación ya no está claro en qué líneas se cambiaron las páginas. Puede haber muchas de esas páginas, y memorizarlas es una desventaja. Además, parte de las páginas se pueden expulsar de la memoria caché del búfer al disco; leerlos nuevamente para cambiar los bits significaría disminuir significativamente la confirmación.

La desventaja de los ahorros es que después de los cambios, cualquier transacción (incluso realizar una simple lectura - SELECCIONAR) puede comenzar a cambiar las páginas de datos en la memoria caché del búfer.

Entonces, arregla el cambio.

 => COMMIT; 

Nada ha cambiado en la página (pero sabemos que el estado de la transacción ya está registrado en XACT):

 => SELECT * FROM heap_page('t',0); 
  ctid | state | xmin | xmax | t_ctid -------+--------+------+-------+-------- (0,1) | normal | 3664 | 0 (a) | (0,1) (1 row) 

Ahora la transacción que primero accede a la página tendrá que determinar el estado de la transacción xmin y escribirla en los bits de información:

 => SELECT * FROM t; 
  id | s ----+----- 1 | FOO (1 row) 

 => SELECT * FROM heap_page('t',0); 
  ctid | state | xmin | xmax | t_ctid -------+--------+----------+-------+-------- (0,1) | normal | 3664 (c) | 0 (a) | (0,1) (1 row) 

Eliminar


Cuando se elimina una línea, el número de la transacción de eliminación actual se registra en el campo xmax de la versión actual, y el bit xmax_aborted se restablece.

Tenga en cuenta que el valor establecido de xmax correspondiente a la transacción activa actúa como un bloqueo de fila. Si otra transacción está a punto de actualizar o eliminar esta fila, se verá obligado a esperar a que se complete la transacción xmax. Hablaremos más sobre las cerraduras más tarde. Por ahora, solo notamos que el número de bloqueos de fila es ilimitado. No ocupan un lugar en la RAM y el rendimiento del sistema no se ve afectado por su cantidad. Es cierto que las transacciones "largas" tienen otras desventajas, pero más sobre eso más adelante.

Eliminar la línea.

 => BEGIN; => DELETE FROM t; => SELECT txid_current(); 
  txid_current -------------- 3665 (1 row) 

Vemos que el número de transacción se registra en el campo xmax, pero los bits de información no están establecidos:

 => SELECT * FROM heap_page('t',0); 
  ctid | state | xmin | xmax | t_ctid -------+--------+----------+------+-------- (0,1) | normal | 3664 (c) | 3665 | (0,1) (1 row) 

Cancelar


Revertir los cambios funciona de manera similar a commit, solo en XACT para la transacción se establece el bit anulado. La cancelación es tan rápida como la confirmación. Aunque el comando se llama ROLLBACK, el cambio no se revierte: todo lo que la transacción logró cambiar en las páginas de datos permanece sin cambios.

 => ROLLBACK; => SELECT * FROM heap_page('t',0); 
  ctid | state | xmin | xmax | t_ctid -------+--------+----------+------+-------- (0,1) | normal | 3664 (c) | 3665 | (0,1) (1 row) 

Al acceder a la página, se comprobará el estado y se establecerá el bit de sugerencia xmax_aborted en la versión de la línea. El número xmax en sí permanece en la página, pero nadie lo verá.

 => SELECT * FROM t; 
  id | s ----+----- 1 | FOO (1 row) 

 => SELECT * FROM heap_page('t',0); 
  ctid | state | xmin | xmax | t_ctid -------+--------+----------+----------+-------- (0,1) | normal | 3664 (c) | 3665 (a) | (0,1) (1 row) 

Actualización


La actualización funciona como si primero estuviera eliminando la versión actual de la fila y luego insertara una nueva.

 => BEGIN; => UPDATE t SET s = 'BAR'; => SELECT txid_current(); 
  txid_current -------------- 3666 (1 row) 

La solicitud produce una línea (nueva versión):

 => SELECT * FROM t; 
  id | s ----+----- 1 | BAR (1 row) 

Pero en la página vemos ambas versiones:

 => SELECT * FROM heap_page('t',0); 
  ctid | state | xmin | xmax | t_ctid -------+--------+----------+-------+-------- (0,1) | normal | 3664 (c) | 3666 | (0,2) (0,2) | normal | 3666 | 0 (a) | (0,2) (2 rows) 

La versión remota está marcada con el número de transacción actual en el campo xmax. Además, este valor se escribe sobre el anterior, ya que se canceló la transacción anterior. Y el bit xmax_aborted se restablece, porque el estado de la transacción actual aún se desconoce.

La primera versión de la línea ahora se refiere al segundo (campo t_ctid), como una más nueva.

Un segundo puntero y una segunda línea aparecen en la página de índice, vinculando a la segunda versión en la página de la tabla.

Al igual que con la eliminación, el valor xmax en la primera versión de la cadena es una señal de que la cadena está bloqueada.

Bueno, completa la transacción.

 => COMMIT; 

Índices


Hasta ahora, solo hemos hablado de páginas tabulares. ¿Y qué pasa dentro de los índices?

La información en las páginas de índice depende en gran medida del tipo particular de índice. E incluso un tipo de índice tiene diferentes tipos de páginas. Por ejemplo, el árbol B tiene una página con metadatos y páginas "normales".

Sin embargo, una página generalmente tiene una serie de punteros a las líneas y las líneas mismas (como en una página de tabla). Además, al final de la página hay un lugar para datos especiales.

Las filas en los índices también pueden tener una estructura muy diferente según el tipo de índice. Por ejemplo, para un árbol B, las filas relacionadas con páginas de hoja contienen el valor de la clave de índice y un enlace (ctid) a la fila correspondiente de la tabla. En general, un índice se puede organizar de una manera completamente diferente.

El punto más importante es que no hay versiones de fila en ningún tipo de índice. Bueno, o podemos suponer que cada línea está representada por exactamente una versión. En otras palabras, no hay campos xmin y xmax en el encabezado de la fila del índice. Podemos suponer que los enlaces del índice conducen a todas las versiones tabulares de las filas, por lo que solo puede averiguar qué versión verá la transacción si mira la tabla. (Como de costumbre, esta no es toda la verdad. En algunos casos, el mapa de visibilidad le permite optimizar el proceso, pero lo consideraremos con más detalle más adelante).

Al mismo tiempo, en la página de índice encontramos punteros a ambas versiones, tanto la actual como la anterior:

 => SELECT itemoffset, ctid FROM bt_page_items('t_s_idx',1); 
  itemoffset | ctid ------------+------- 1 | (0,2) 2 | (0,1) (2 rows) 

Transacciones virtuales


En la práctica, PostgreSQL usa optimizaciones para "guardar" los números de transacción.

Si una transacción solo lee datos, entonces no afecta la visibilidad de las versiones de fila. Por lo tanto, al principio, el proceso de publicación emite una transacción de número virtual (xid virtual). El número consta de un identificador de proceso y un número secuencial.

La emisión de este número no requiere sincronización entre todos los procesos y, por lo tanto, es muy rápida. Conoceremos otra razón para usar números virtuales cuando hablemos de la congelación.

Los números virtuales no se tienen en cuenta en las instantáneas de datos.

En diferentes momentos, las transacciones virtuales con números que ya se han utilizado pueden aparecer en el sistema, y ​​esto es normal. Pero ese número no se puede escribir en las páginas de datos, porque la próxima vez que acceda a la página, puede perder todo significado.

 => BEGIN; => SELECT txid_current_if_assigned(); 
  txid_current_if_assigned -------------------------- (1 row) 

Si la transacción comienza a cambiar datos, se le da un número de transacción real y único.

 => UPDATE accounts SET amount = amount - 1.00; => SELECT txid_current_if_assigned(); 
  txid_current_if_assigned -------------------------- 3667 (1 row) 

 => COMMIT; 

Transacciones anidadas


Guardar puntos


SQL define los puntos de guardado que le permiten deshacer una parte de una transacción sin interrumpirla por completo. Pero esto no encaja en el esquema anterior, ya que el estado de una transacción es uno para todos sus cambios, y físicamente no se revierten los datos.

Para implementar dicha funcionalidad, una transacción con un punto de guardado se divide en varias transacciones anidadas separadas (subtransacción), cuyo estado se puede controlar por separado.

Las transacciones anidadas tienen su propio número (más alto que el número de transacción principal). El estado de las transacciones anidadas se registra de la manera habitual en XACT, sin embargo, el estado final depende del estado de la transacción principal: si se cancela, todas las transacciones anidadas también se cancelan.

La información sobre el anidamiento de transacciones se almacena en archivos en el directorio PGDATA / pg_subtrans. Se accede a los archivos a través de buffers en la memoria compartida de la instancia, organizados de la misma manera que los buffers XACT.

No confunda transacciones anidadas y transacciones autónomas. Las transacciones autónomas de ninguna manera dependen unas de otras, y las anidadas dependen. No hay transacciones autónomas en el PostgreSQL habitual y, tal vez, para mejor: en el caso de que se necesiten muy, muy raramente, y su presencia en otros DBMS provoca abuso, del cual todos sufren.

Borre la tabla, inicie la transacción e inserte la línea:

 => TRUNCATE TABLE t; => BEGIN; => INSERT INTO t(s) VALUES ('FOO'); => SELECT txid_current(); 
  txid_current -------------- 3669 (1 row) 

 => SELECT xmin, xmax, * FROM t; 
  xmin | xmax | id | s ------+------+----+----- 3669 | 0 | 2 | FOO (1 row) 

 => SELECT * FROM heap_page('t',0); 
  ctid | state | xmin | xmax | t_ctid -------+--------+------+-------+-------- (0,1) | normal | 3669 | 0 (a) | (0,1) (1 row) 

Ahora ponga un punto de guardado e inserte otra fila.

 => SAVEPOINT sp; => INSERT INTO t(s) VALUES ('XYZ'); => SELECT txid_current(); 
  txid_current -------------- 3669 (1 row) 

Tenga en cuenta que la función txid_current () devuelve el número de la transacción principal, no anidada.

 => SELECT xmin, xmax, * FROM t; 
  xmin | xmax | id | s ------+------+----+----- 3669 | 0 | 2 | FOO 3670 | 0 | 3 | XYZ (2 rows) 

 => SELECT * FROM heap_page('t',0); 
  ctid | state | xmin | xmax | t_ctid -------+--------+------+-------+-------- (0,1) | normal | 3669 | 0 (a) | (0,1) (0,2) | normal | 3670 | 0 (a) | (0,2) (2 rows) 

Volvemos al punto de guardado e insertamos la tercera fila.

 => ROLLBACK TO sp; => INSERT INTO t(s) VALUES ('BAR'); => SELECT xmin, xmax, * FROM t; 
  xmin | xmax | id | s ------+------+----+----- 3669 | 0 | 2 | FOO 3671 | 0 | 4 | BAR (2 rows) 

 => SELECT * FROM heap_page('t',0); 
  ctid | state | xmin | xmax | t_ctid -------+--------+----------+-------+-------- (0,1) | normal | 3669 | 0 (a) | (0,1) (0,2) | normal | 3670 (a) | 0 (a) | (0,2) (0,3) | normal | 3671 | 0 (a) | (0,3) (3 rows) 

En la página, seguimos viendo la fila agregada por la transacción anidada cancelada.

Arreglamos los cambios.

 => COMMIT; => SELECT xmin, xmax, * FROM t; 
  xmin | xmax | id | s ------+------+----+----- 3669 | 0 | 2 | FOO 3671 | 0 | 4 | BAR (2 rows) 

 => SELECT * FROM heap_page('t',0); 
  ctid | state | xmin | xmax | t_ctid -------+--------+----------+-------+-------- (0,1) | normal | 3669 (c) | 0 (a) | (0,1) (0,2) | normal | 3670 (a) | 0 (a) | (0,2) (0,3) | normal | 3671 (c) | 0 (a) | (0,3) (3 rows) 

Ahora puede ver claramente que cada transacción anidada tiene su propio estado.

Tenga en cuenta que las transacciones anidadas no se pueden usar explícitamente en SQL, es decir, no puede iniciar una nueva transacción sin completar la actual. Este mecanismo se usa implícitamente cuando se usan puntos de guardado, y también cuando se manejan excepciones PL / pgSQL y en una serie de otros casos más exóticos.

 => BEGIN; 
 BEGIN 
 => BEGIN; 
 WARNING: there is already a transaction in progress BEGIN 
 => COMMIT; 
 COMMIT 
 => COMMIT; 
 WARNING: there is no transaction in progress COMMIT 

Errores y atomicidad de las operaciones.


¿Qué sucede si se produce un error durante la operación? Por ejemplo, así:

 => BEGIN; => SELECT * FROM t; 
  id | s ----+----- 2 | FOO 4 | BAR (2 rows) 

 => UPDATE t SET s = repeat('X', 1/(id-4)); 
 ERROR: division by zero 

Ha ocurrido un error Ahora la transacción se considera abortada y no se permite una sola operación:

 => SELECT * FROM t; 
 ERROR: current transaction is aborted, commands ignored until end of transaction block 

E incluso si intenta confirmar los cambios, PostgreSQL informará la cancelación:

 => COMMIT; 
 ROLLBACK 

¿Por qué no puedo continuar la transacción después de una falla? El hecho es que podría ocurrir un error para que podamos acceder a parte de los cambios: la atomicidad de ni siquiera la transacción, pero el operador sería violado. Como en nuestro ejemplo, donde el operador logró actualizar una línea antes del error:

 => SELECT * FROM heap_page('t',0); 
  ctid | state | xmin | xmax | t_ctid -------+--------+----------+-------+-------- (0,1) | normal | 3669 (c) | 3672 | (0,4) (0,2) | normal | 3670 (a) | 0 (a) | (0,2) (0,3) | normal | 3671 (c) | 0 (a) | (0,3) (0,4) | normal | 3672 | 0 (a) | (0,4) (4 rows) 

Debo decir que en psql hay un modo que aún le permite continuar con la transacción después de una falla, como si las acciones del operador erróneo fueran revertidas.

 => \set ON_ERROR_ROLLBACK on => BEGIN; => SELECT * FROM t; 
  id | s ----+----- 2 | FOO 4 | BAR (2 rows) 

 => UPDATE t SET s = repeat('X', 1/(id-4)); 
 ERROR: division by zero 

 => SELECT * FROM t; 
  id | s ----+----- 2 | FOO 4 | BAR (2 rows) 

 => COMMIT; 

Es fácil adivinar que, en este modo, psql establece un punto de guardado implícito delante de cada comando y, en caso de falla, inicia un retroceso. Este modo no se usa de manera predeterminada, ya que establecer puntos de guardado (incluso sin retroceder a ellos) está asociado con una sobrecarga significativa.

Continuará

Source: https://habr.com/ru/post/445820/


All Articles