MVCC-4. Instantanés de données

Après avoir examiné les problèmes liés à l' isolement et fait une digression sur l' organisation des données à un bas niveau , la dernière fois que nous avons parlé en détail des versions de ligne et tracé comment les informations de service dans l'en-tête de version ont changé au cours de diverses opérations.

Aujourd'hui, nous examinons comment des versions cohérentes des données sont obtenues à partir des versions de ligne.

Qu'est-ce qu'un instantané de données


Physiquement, les pages de données peuvent contenir plusieurs versions de la même ligne. De plus, chaque transaction ne devrait voir qu'une seule (ou pas une seule) version de chaque ligne afin qu'elles forment ensemble une image cohérente avec ACID des données à un certain moment.

L'isolement dans PostgreSQL est construit sur la base d'instantanés: chaque transaction fonctionne avec son propre instantané de données, qui "contient" les données qui ont été enregistrées avant la création de l'instantané et ne "contient" pas les données non encore fixées à ce moment. Nous avons déjà vu que l'isolement dans ce cas est plus rigoureux que ne l'exige la norme, mais pas sans anomalies.

Au niveau d'isolement de Read Committed, un instantané est créé au début de chaque instruction de transaction. Un tel instantané est actif pendant l'exécution de l'instruction. Sur la figure, le moment de la création de l'instantané (qui, comme nous le rappelons, est déterminé par le numéro de transaction) est indiqué en bleu.



Aux niveaux de lecture répétable et de sérialisation, un instantané est créé une fois au début de la première instruction de transaction. Un tel instantané reste actif jusqu'à la fin de la transaction.



Visibilité des versions de ligne dans l'instantané


Règles de visibilité


Bien sûr, un instantané n'est pas une copie physique de toutes les versions de ligne requises. En fait, un instantané est spécifié par plusieurs nombres et la visibilité des versions de ligne dans l'instantané est déterminée par les règles.

La visibilité ou non de cette version de la ligne dans l'instantané dépend des deux champs de son en-tête - xmin et xmax - c'est-à-dire du nombre de transactions qui les ont créées et supprimées. Ces intervalles ne se croisent pas, par conséquent, une ligne est représentée dans une image par un maximum de l'une de ses versions.

Les règles exactes de visibilité sont assez complexes et prennent en compte de nombreuses situations différentes et des cas extrêmes.
Cela peut être facilement vérifié en consultant src / backend / utils / time / tqual.c (dans la version 12, la vérification a été déplacée vers src / backend / access / heap / heapam_visibility.c).

Pour simplifier, nous pouvons dire que la version de la ligne est visible lorsque les modifications apportées par la transaction xmin sont visibles dans l'image, et les modifications apportées par la transaction xmax ne sont pas visibles (en d'autres termes, il est déjà visible que la version de la ligne est apparue, mais il n'est pas encore visible qu'elle a été supprimée).

À leur tour, les modifications de transaction sont visibles dans l'instantané, si c'est la même transaction qui a créé l'instantané (elle voit ses propres modifications), ou la transaction a été validée avant la création de l'instantané.

Il est possible de représenter graphiquement les transactions sous forme de segments (du début au moment de la validation):



Ici:

  • les modifications apportées à la transaction 2 seront visibles, car elles se sont terminées avant la création de l'instantané,
  • les modifications apportées à la transaction 1 ne seront pas visibles car elle était active au moment de la prise de l'instantané,
  • les modifications apportées à la transaction 3 ne seront pas visibles car elles ont commencé après la prise de l'instantané (peu importe si elle s'est terminée ou non).

Malheureusement, le moment de la validation des transactions est inconnu du système. Seul le moment de son début est connu (il est déterminé par le numéro de transaction et est indiqué par une ligne pointillée sur les figures ci-dessus), mais le fait de l'achèvement n'est enregistré nulle part.

Tout ce que nous pouvons faire est de connaître l'état actuel des transactions lors de la création d'un instantané. Ces informations se trouvent dans la mémoire partagée du serveur dans la structure ProcArray, qui contient une liste de toutes les sessions actives et de leurs transactions.

Et après coup, nous ne pourrons plus comprendre si une transaction était active au moment de l'instantané ou non. Par conséquent, la liste de toutes les transactions actuellement actives doit être mémorisée dans l'image.

Il découle de ce qui précède que dans PostgreSQL, vous ne pouvez pas créer un instantané montrant des données cohérentes à une date arbitraire, même si toutes les versions nécessaires des lignes existent dans les pages du tableau. On entend souvent la question de savoir pourquoi il n'y a pas de requêtes rétrospectives (ou temporelles; dans Oracle, cela s'appelle une requête flashback) dans PostgreSQL - c'est l'une des raisons.
C'est drôle qu'au départ, il y avait une telle fonctionnalité, mais plus tard, elle a été supprimée du SGBD. Vous pouvez lire à ce sujet dans un article de Joseph Hellerstein .
Ainsi, un instantané de données est déterminé par plusieurs paramètres:

  • le moment de la création de l'instantané, à savoir le numéro de la prochaine transaction qui n'existe pas dans le système ( snapshot.xmax );
  • une liste des transactions actives au moment de la prise de l'instantané ( snapshot.xip ).

Pour plus de commodité et d'optimisation, le numéro de la première transaction active ( snapshot.xmin ) est également stocké séparément. Cette valeur a une signification importante, que nous discuterons ci-dessous.

De plus, quelques paramètres supplémentaires sont enregistrés dans l'image, mais ils ne sont pas importants pour nous.



Exemple


Pour voir comment la visibilité est déterminée par l'instantané, nous reproduisons la situation avec les trois transactions décrites ci-dessus. Le tableau comportera trois lignes, avec:

  • le premier a été ajouté par une transaction qui a commencé avant la création de l'instantané, mais s'est terminée plus tard,
  • le second est ajouté par une transaction qui a commencé et s'est terminée avant la création de l'instantané,
  • le troisième a été ajouté après avoir pris la photo.

=> TRUNCATE TABLE accounts; 

Première transaction (pas encore terminée):

 => BEGIN; => INSERT INTO accounts VALUES (1, '1001', 'alice', 1000.00); => SELECT txid_current(); 
 => SELECT txid_current(); txid_current -------------- 3695 (1 row) 

La deuxième transaction (terminée avant la création de l'instantané):

 | => BEGIN; | => INSERT INTO accounts VALUES (2, '2001', 'bob', 100.00); | => SELECT txid_current(); 
 | txid_current | -------------- | 3696 | (1 row) 
 | => COMMIT; 

Créez un instantané dans une transaction dans une autre session.

 || => BEGIN ISOLATION LEVEL REPEATABLE READ; || => SELECT xmin, xmax, * FROM accounts; 
 || xmin | xmax | id | number | client | amount || ------+------+----+--------+--------+-------- || 3696 | 0 | 2 | 2001 | bob | 100.00 || (1 row) 

Nous terminons la première transaction après la création de l'instantané:

 => COMMIT; 

Et la troisième transaction (apparue plus tard sur l'instantané):

 | => BEGIN; | => INSERT INTO accounts VALUES (3, '2002', 'bob', 900.00); | => SELECT txid_current(); 
 | txid_current | -------------- | 3697 | (1 row) 
 | => COMMIT; 

De toute évidence, une ligne est toujours visible dans notre image:

 || => SELECT xmin, xmax, * FROM accounts; 
 || xmin | xmax | id | number | client | amount || ------+------+----+--------+--------+-------- || 3696 | 0 | 2 | 2001 | bob | 100.00 || (1 row) 

La question est de savoir comment PostgreSQL comprend cela.

Tout est déterminé par l'image. Regardons ça:

 || => SELECT txid_current_snapshot(); 
 || txid_current_snapshot || ----------------------- || 3695:3697:3695 || (1 row) 

Ici, les deux points répertorient snapshot.xmin, snapshot.xmax et snapshot.xip (dans ce cas, un numéro, mais en général - une liste).

Selon les règles énoncées ci-dessus, l'image doit montrer les modifications apportées par les transactions avec les nombres snapshot.xmin <= xid <snapshot.xmax, à l'exception de snapshot.xip. Regardons toutes les lignes du tableau (dans une nouvelle image):

 => SELECT xmin, xmax, * FROM accounts ORDER BY id; 
  xmin | xmax | id | number | client | amount ------+------+----+--------+--------+--------- 3695 | 0 | 1 | 1001 | alice | 1000.00 3696 | 0 | 2 | 2001 | bob | 100.00 3697 | 0 | 3 | 2002 | bob | 900.00 (3 rows) 

La première ligne n'est pas visible - elle a été créée par une transaction, qui est incluse dans la liste des actifs (xip).
La deuxième ligne est visible - elle est créée par une transaction qui se situe dans la plage de l'image.
La troisième ligne n'est pas visible - elle a été créée par une transaction qui n'est pas dans la plage de l'instantané.

 || => COMMIT; 

Propres changements


Un peu complique l'image est le cas de déterminer la visibilité de vos propres changements de transaction. Ici, vous devrez peut-être voir uniquement une partie de ces modifications. Par exemple, un curseur ouvert à un moment particulier ne devrait pas voir les modifications apportées après ce moment à n'importe quel niveau d'isolement.

Pour ce faire, dans l'en-tête de la version de ligne, il y a un champ spécial (qui est affiché dans les pseudo-colonnes cmin et cmax), montrant le numéro de séquence de l'opération à l'intérieur de la transaction. Cmin représente le nombre à insérer, cmax représente le nombre à supprimer, mais pour économiser de l'espace dans l'en-tête de ligne, il s'agit en fait d'un champ, pas de deux différents. On pense que l'insertion et la suppression de la même ligne dans une seule transaction est rare.

Si cela se produit toujours, un numéro spécial "combo" est inséré dans le même champ, à propos duquel le processus de service se souvient des cmin et cmax réels. Mais c'est complètement exotique.

Un exemple simple. Nous commençons la transaction et ajoutons la ligne au tableau:

 => BEGIN; => SELECT txid_current(); 
  txid_current -------------- 3698 (1 row) 
 INSERT INTO accounts(id, number, client, amount) VALUES (4, 3001, 'charlie', 100.00); 

Nous afficherons le contenu du tableau avec le champ cmin (mais uniquement pour les lignes ajoutées par notre transaction - pour d'autres cela n'a pas de sens):

 => SELECT xmin, CASE WHEN xmin = 3698 THEN cmin END cmin, * FROM accounts; 
  xmin | cmin | id | number | client | amount ------+------+----+--------+---------+--------- 3695 | | 1 | 1001 | alice | 1000.00 3696 | | 2 | 2001 | bob | 100.00 3697 | | 3 | 2002 | bob | 900.00 3698 | 0 | 4 | 3001 | charlie | 100.00 (4 rows) 

Ouvrez maintenant le curseur de la requête qui renvoie le nombre de lignes de la table.

 => DECLARE c CURSOR FOR SELECT count(*) FROM accounts; 

Et après cela, ajoutez une autre ligne:

 => INSERT INTO accounts(id, number, client, amount) VALUES (5, 3002, 'charlie', 200.00); 

La demande renverra 4 - la ligne ajoutée après l'ouverture du curseur ne tombera pas dans l'instantané des données:

 => FETCH c; 
  count ------- 4 (1 row) 

Pourquoi? Parce que dans l'instantané, seules les versions de ligne avec cmin <1 sont prises en compte.

 => SELECT xmin, CASE WHEN xmin = 3698 THEN cmin END cmin, * FROM accounts; 
  xmin | cmin | id | number | client | amount ------+------+----+--------+---------+--------- 3695 | | 1 | 1001 | alice | 1000.00 3696 | | 2 | 2001 | bob | 100.00 3697 | | 3 | 2002 | bob | 900.00 3698 | 0 | 4 | 3001 | charlie | 100.00 3698 | 1 | 5 | 3002 | charlie | 200.00 (5 rows) 
 => ROLLBACK; 

Horizon de l'événement


Le numéro de la première transaction active (snapshot.xmin) a une signification importante - il définit «l'horizon des événements» de la transaction. A savoir, au-delà de son horizon, une transaction ne voit toujours que les versions actuelles des lignes.

En effet, une version non pertinente n'a besoin d'être vue que si la version actuelle a été créée par une transaction qui n'a pas encore été finalisée et n'est donc pas encore visible. Mais au-delà de «l'horizon», toutes les transactions sont déjà garanties d'être réalisées.



L'horizon des événements de la transaction est visible dans le répertoire système:

 => BEGIN; => SELECT backend_xmin FROM pg_stat_activity WHERE pid = pg_backend_pid(); 
  backend_xmin -------------- 3699 (1 row) 

Vous pouvez également définir un «horizon d'événement» au niveau de la base de données. Pour ce faire, prenez tous les instantanés actifs et trouvez parmi eux le xmin le plus ancien. Il déterminera l'horizon au-delà duquel les versions non pertinentes des lignes de cette base de données ne seront jamais visibles pour aucune transaction. De telles versions de chaînes peuvent être effacées - c'est pourquoi le concept d'horizon est si important d'un point de vue pratique.

Si une transaction contient un instantané pendant une longue période, elle contiendra également l'horizon des événements de la base de données. De plus, une transaction inachevée tiendra l'horizon par le fait même de son existence, même si aucun instantané n'y figure.

Et cela signifie que les versions non pertinentes des lignes de cette base de données ne peuvent pas être effacées. Dans le même temps, une transaction «longue durée» peut ne pas chevaucher d'autres transactions dans les données - ce n'est absolument pas important, l'horizon de la base de données est le même pour tout le monde.

Si maintenant, pas une transaction, mais les instantanés (de snapshot.xmin à snapshot.xmax) sont représentés comme un segment, alors la situation peut être imaginée comme suit:



Dans cette figure, l'instantané le plus bas se réfère à une transaction incomplète et dans les instantanés snapshot.xmin restants ne peut pas être supérieur à son nombre.

Dans notre exemple, une transaction avec le niveau d'isolement Read Committed a été démarrée. Même s'il ne contient aucun instantané de données actif, il continue de tenir l'horizon:

 | => BEGIN; | => UPDATE accounts SET amount = amount + 1.00; | => COMMIT; 
 => SELECT backend_xmin FROM pg_stat_activity WHERE pid = pg_backend_pid(); 
  backend_xmin -------------- 3699 (1 row) 

Et seulement une fois la transaction terminée, l'horizon avance, vous permettant d'effacer les versions non pertinentes des lignes:

 => COMMIT; => SELECT backend_xmin FROM pg_stat_activity WHERE pid = pg_backend_pid(); 
  backend_xmin -------------- 3700 (1 row) 

Si la situation décrite crée vraiment des problèmes et qu'il n'y a aucun moyen de l'éviter au niveau de l'application, alors, à partir de la version 9.6, deux options sont disponibles:

  • old_snapshot_threshold définit la durée de vie maximale d'un instantané. Passé ce délai, le serveur obtient le droit de supprimer les versions non pertinentes des lignes, et s'il a besoin d'une transaction «longue durée», mais il recevra un instantané d'une erreur trop ancienne.
  • idle_in_transaction_session_timeout définit la durée de vie maximale d'une transaction inactive. Passé ce délai, la transaction est abandonnée.

Exporter un instantané de données


Il existe des situations où plusieurs transactions simultanées doivent être garanties pour voir la même image de données. Par exemple, nous pouvons utiliser l'utilitaire pg_dump, qui peut fonctionner en mode parallèle: tous les processus de travail doivent voir la base de données dans le même état afin que la copie de sauvegarde soit cohérente.

Bien sûr, vous ne pouvez pas vous fier au fait que les modèles de données coïncident simplement parce que les transactions sont lancées «simultanément». Il existe un mécanisme pour exporter et importer un instantané pour cela.

La fonction pg_export_snapshot renvoie l'identifiant d'un instantané qui peut être transféré (par des moyens externes au SGBD) vers une autre transaction.

 => BEGIN ISOLATION LEVEL REPEATABLE READ; => SELECT count(*) FROM accounts; --   
  count ------- 3 (1 row) 
 => SELECT pg_export_snapshot(); 
  pg_export_snapshot --------------------- 00000004-00000E7B-1 (1 row) 

Une autre transaction peut importer l'instantané à l'aide de la commande SET TRANSACTION SNAPSHOT avant d'y exécuter la première demande. Vous devez d'abord définir le niveau d'isolement comme Lecture répétable ou Sérialisable, car au niveau de lecture validée, les opérateurs utiliseront leurs propres instantanés.

 | => DELETE FROM accounts; | => BEGIN ISOLATION LEVEL REPEATABLE READ; | => SET TRANSACTION SNAPSHOT '00000004-00000E7B-1'; 

Maintenant, la deuxième transaction fonctionnera avec un instantané de la première et, en conséquence, affichera trois lignes (et non zéro):

 | => SELECT count(*) FROM accounts; 
 | count | ------- | 3 | (1 row) 

La durée de vie de l'instantané exporté est la durée de vie de la transaction d'exportation.

 | => COMMIT; => COMMIT; 

À suivre .

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


All Articles