MVCC-3. Εκδόσεις χορδών

Έτσι, εξετάσαμε ζητήματα που σχετίζονται με μόνωση, και έκανε μια υποχώρηση περίπου οργάνωση δεδομένων σε χαμηλό επίπεδο. Και τελικά φτάσαμε στο πιο ενδιαφέρον μέρος - τις έγχορδα εκδόσεις.

Τίτλος

Όπως έχουμε ήδη πει, κάθε σειρά μπορεί να υπάρχει ταυτόχρονα σε πολλές εκδόσεις στη βάση δεδομένων. Η μία έκδοση πρέπει να διακρίνεται με κάποιο τρόπο από την άλλη.Για το σκοπό αυτό, κάθε έκδοση έχει δύο σημάδια που καθορίζουν τον «χρόνο» δράσης αυτής της έκδοσης (xmin και xmax). Σε εισαγωγικά - γιατί δεν χρησιμοποιείται ο χρόνος ως τέτοιος, αλλά ένας ειδικός αυξανόμενος μετρητής. Και αυτός ο μετρητής είναι ο αριθμός συναλλαγής.

(Ως συνήθως, η πραγματικότητα είναι πιο περίπλοκη: ο αριθμός συναλλαγής δεν μπορεί να αυξάνεται συνεχώς λόγω της περιορισμένης χωρητικότητας bit του μετρητή. Αλλά θα εξετάσουμε αυτές τις λεπτομέρειες λεπτομερώς όταν φτάσουμε στο πάγωμα.)

Όταν δημιουργείται μια σειρά, το xmin ορίζεται στον αριθμό συναλλαγής που εξέδωσε την εντολή INSERT και το xmax παραμένει κενό.

Όταν διαγράφεται μια σειρά, η τιμή xmax της τρέχουσας έκδοσης επισημαίνεται με τον αριθμό της συναλλαγής που πραγματοποίησε το DELETE.

Όταν μια σειρά τροποποιείται από μια εντολή UPDATE, εκτελούνται δύο λειτουργίες: DELETE και INSERT. Η τρέχουσα έκδοση της σειράς ορίζει το xmax ίσο με τον αριθμό της συναλλαγής που πραγματοποίησε την ΕΝΗΜΕΡΩΣΗ. Στη συνέχεια δημιουργείται μια νέα έκδοση της ίδιας συμβολοσειράς. Η τιμή xmin συμπίπτει με την τιμή xmax της προηγούμενης έκδοσης.

Τα πεδία xmin και xmax περιλαμβάνονται στην κεφαλίδα έκδοσης σειράς. Εκτός από αυτά τα πεδία, η κεφαλίδα περιέχει και άλλα, για παράδειγμα:

  • Το infomask είναι μια σειρά από bit που καθορίζουν τις ιδιότητες αυτής της έκδοσης. Υπάρχουν αρκετά από αυτά. Σταδιακά θα εξετάσουμε τα κύρια.
  • Το ctid είναι ένας σύνδεσμος προς την επόμενη, νεότερη έκδοση της ίδιας γραμμής. Για τη νεότερη, πιο πρόσφατη έκδοση μιας συμβολοσειράς, το ctid αναφέρεται σε αυτήν την ίδια την έκδοση. Ο αριθμός έχει τη μορφή (x,y), όπου x είναι ο αριθμός σελίδας, y είναι ο αριθμός ευρετηρίου στον πίνακα.
  • null bitmap - Επισημαίνει τις στήλες μιας δεδομένης έκδοσης που περιέχουν μια τιμή null (NULL). Το NULL δεν είναι μία από τις κανονικές τιμές τύπου δεδομένων, επομένως το χαρακτηριστικό πρέπει να αποθηκευτεί χωριστά.

Ως αποτέλεσμα, η κεφαλίδα είναι αρκετά μεγάλη - τουλάχιστον 23 byte για κάθε έκδοση της γραμμής και συνήθως περισσότερα λόγω του bitmap NULL. Εάν ο πίνακας είναι "στενός" (δηλαδή περιέχει λίγες στήλες), τα γενικά έξοδα ενδέχεται να καταλαμβάνουν περισσότερα από τις χρήσιμες πληροφορίες.

εισάγετε

Ας ρίξουμε μια πιο προσεκτική ματιά στο πώς εκτελούνται οι λειτουργίες συμβολοσειράς χαμηλού επιπέδου, ξεκινώντας με την εισαγωγή.

Για πειράματα, ας δημιουργήσουμε έναν νέο πίνακα με δύο στήλες και ένα ευρετήριο σε μία από αυτές:

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

Ας εισαγάγουμε μία σειρά μετά την έναρξη μιας συναλλαγής.

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

Εδώ είναι ο τρέχων αριθμός συναλλαγής μας:

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

Ας δούμε τα περιεχόμενα της σελίδας. Η συνάρτηση heap_page_items της επέκτασης pageinspect σάς επιτρέπει να λαμβάνετε πληροφορίες σχετικά με δείκτες και εκδόσεις σειρών:

=> 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

Σημειώστε ότι η λέξη heap στο PostgreSQL αναφέρεται σε πίνακες. Αυτή είναι μια άλλη περίεργη χρήση του όρου - ένας σωρός είναι γνωστός δομή δεδομένων, που δεν έχει τίποτα κοινό με τον πίνακα. Εδώ η λέξη χρησιμοποιείται με την έννοια του «όλα είναι μαζί», σε αντίθεση με τα διατεταγμένα ευρετήρια.

Η συνάρτηση εμφανίζει δεδομένα "ως έχουν", σε μορφή που είναι δύσκολο να κατανοηθεί. Για να το καταλάβουμε, θα αφήσουμε μόνο μέρος των πληροφοριών και θα τις αποκρυπτογραφήσουμε:

=> 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)

Να τι κάναμε:

  • Προστέθηκε ένα μηδέν στον αριθμό ευρετηρίου για να φαίνεται το ίδιο με το t_ctid: (αριθμός σελίδας, αριθμός ευρετηρίου).
  • Αποκρυπτογραφήθηκε η κατάσταση του δείκτη lp_flags. Εδώ είναι "κανονικό" - αυτό σημαίνει ότι ο δείκτης στην πραγματικότητα αναφέρεται στην έκδοση της συμβολοσειράς. Θα δούμε και άλλες έννοιες αργότερα.
  • Από όλα τα bits πληροφοριών, μόνο δύο ζεύγη έχουν αναγνωριστεί μέχρι στιγμής. Τα bit xmin_committed και xmin_aborted υποδεικνύουν εάν ο αριθμός συναλλαγής xmin έχει δεσμευτεί (ακυρώθηκε). Δύο παρόμοια bit αναφέρονται στον αριθμό συναλλαγής xmax.

Τι βλέπουμε; Όταν εισάγετε μια σειρά, θα εμφανιστεί ένας αριθμός ευρετηρίου 1 στη σελίδα του πίνακα, που δείχνει την πρώτη και μοναδική έκδοση της σειράς.

Στην έκδοση συμβολοσειράς, το πεδίο xmin συμπληρώνεται με τον αριθμό της τρέχουσας συναλλαγής. Η συναλλαγή είναι ακόμα ενεργή, επομένως δεν έχουν οριστεί και τα bit xmin_committed και xmin_aborted.

Το πεδίο ctid έκδοσης σειράς αναφέρεται στην ίδια σειρά. Αυτό σημαίνει ότι δεν υπάρχει νεότερη έκδοση.

Το πεδίο xmax είναι γεμάτο με έναν εικονικό αριθμό 0 επειδή αυτή η έκδοση της σειράς δεν έχει διαγραφεί και είναι τρέχουσα. Οι συναλλαγές δεν θα δώσουν προσοχή σε αυτόν τον αριθμό επειδή έχει οριστεί το bit xmax_aborted.

Ας κάνουμε ένα ακόμη βήμα προς τη βελτίωση της αναγνωσιμότητας προσθέτοντας bits πληροφοριών στους αριθμούς συναλλαγών. Και ας δημιουργήσουμε μια συνάρτηση, αφού θα χρειαστούμε το αίτημα περισσότερες από μία φορές:

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

Σε αυτή τη μορφή, είναι πολύ πιο ξεκάθαρο τι συμβαίνει στην κεφαλίδα της έκδοσης σειράς:

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

Παρόμοιες, αλλά πολύ λιγότερο λεπτομερείς, πληροφορίες μπορούν να ληφθούν από τον ίδιο τον πίνακα, χρησιμοποιώντας τις ψευδό-στήλες xmin και xmax:

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

Στερέωση

Εάν μια συναλλαγή ολοκληρωθεί με επιτυχία, πρέπει να θυμάστε την κατάστασή της - σημειώστε ότι έχει δεσμευτεί. Για να γίνει αυτό, χρησιμοποιείται μια δομή που ονομάζεται XACT (και πριν από την έκδοση 10 ονομαζόταν CLOG (καταγραφή δεσμεύσεων) και αυτό το όνομα εξακολουθεί να βρίσκεται σε διαφορετικά μέρη).

Το XACT δεν είναι πίνακας καταλόγου συστήματος. Αυτά είναι τα αρχεία στον κατάλογο PGDATA/pg_xact. Έχουν δύο bit για κάθε συναλλαγή: δεσμευμένη και ματαιωμένη - ακριβώς όπως στην κεφαλίδα της έκδοσης σειράς. Αυτές οι πληροφορίες χωρίζονται σε πολλά αρχεία αποκλειστικά για διευκόλυνση· θα επανέλθουμε σε αυτό το ζήτημα όταν σκεφτούμε να παγώσουμε. Και η εργασία με αυτά τα αρχεία πραγματοποιείται σελίδα προς σελίδα, όπως και με όλα τα άλλα.

Έτσι, όταν μια συναλλαγή δεσμεύεται στο XACT, το δεσμευμένο bit ορίζεται για αυτήν τη συναλλαγή. Και αυτό είναι το μόνο που συμβαίνει κατά τη διάρκεια της δέσμευσης (αν και δεν μιλάμε ακόμα για το αρχείο προκαταγραφής).

Όταν μια άλλη συναλλαγή αποκτήσει πρόσβαση στη σελίδα του πίνακα που μόλις εξετάσαμε, θα πρέπει να απαντήσει σε πολλές ερωτήσεις.

  1. Έχει ολοκληρωθεί η συναλλαγή xmin; Εάν όχι, τότε η δημιουργημένη έκδοση της συμβολοσειράς δεν θα πρέπει να είναι ορατή.
    Αυτός ο έλεγχος πραγματοποιείται κοιτάζοντας μια άλλη δομή, η οποία βρίσκεται στην κοινόχρηστη μνήμη του στιγμιότυπου και ονομάζεται ProcArray. Περιέχει μια λίστα με όλες τις ενεργές διεργασίες και για κάθε μία υποδεικνύεται ο αριθμός της τρέχουσας (ενεργής) συναλλαγής της.
  2. Εάν ολοκληρωθεί, τότε πώς - με τη δέσμευση ή την ακύρωση; Εάν ακυρωθεί, τότε η έκδοση σειράς δεν θα πρέπει να είναι ορατή.
    Αυτό ακριβώς είναι το XACT. Όμως, παρόλο που οι τελευταίες σελίδες του XACT αποθηκεύονται σε buffer στη μνήμη RAM, εξακολουθεί να είναι ακριβό να ελέγχετε το XACT κάθε φορά. Επομένως, μόλις καθοριστεί η κατάσταση συναλλαγής, γράφεται στα bit xmin_committed και xmin_aborted της έκδοσης συμβολοσειράς. Εάν οριστεί ένα από αυτά τα bit, τότε η κατάσταση της συναλλαγής xmin θεωρείται γνωστή και η επόμενη συναλλαγή δεν θα χρειάζεται να έχει πρόσβαση στο XACT.

Γιατί αυτά τα bit δεν ορίζονται από την ίδια τη συναλλαγή που κάνει την εισαγωγή; Όταν εμφανίζεται ένα ένθετο, η συναλλαγή δεν γνωρίζει ακόμη εάν θα πετύχει. Και τη στιγμή της δέσμευσης, δεν είναι πλέον σαφές ποιες γραμμές σε ποιες σελίδες άλλαξαν. Μπορεί να υπάρχουν πολλές τέτοιες σελίδες και η απομνημόνευσή τους είναι ασύμφορη. Επιπλέον, ορισμένες σελίδες μπορούν να εξαχθούν από την προσωρινή μνήμη προσωρινής αποθήκευσης στο δίσκο. Αν τα διαβάσετε ξανά για να αλλάξετε τα bit θα επιβράδυνε σημαντικά τη δέσμευση.

Το μειονέκτημα της εξοικονόμησης είναι ότι μετά από αλλαγές, οποιαδήποτε συναλλαγή (ακόμη και μια που εκτελεί μια απλή ανάγνωση - ΕΠΙΛΟΓΗ) μπορεί να αρχίσει να αλλάζει σελίδες δεδομένων στην προσωρινή μνήμη buffer.

Λοιπόν, ας διορθώσουμε την αλλαγή.

=> COMMIT;

Δεν έχει αλλάξει τίποτα στη σελίδα (αλλά γνωρίζουμε ότι η κατάσταση συναλλαγής έχει ήδη καταγραφεί στο XACT):

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

Τώρα η συναλλαγή που έχει πρόσβαση στη σελίδα πρώτα θα πρέπει να καθορίσει την κατάσταση συναλλαγής xmin και να την γράψει στα bit πληροφοριών:

=> 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)

Μετακίνηση

Όταν διαγράφεται μια σειρά, ο αριθμός της τρέχουσας συναλλαγής διαγραφής γράφεται στο πεδίο xmax της τρέχουσας έκδοσης και το bit xmax_aborted διαγράφεται.

Σημειώστε ότι η καθορισμένη τιμή του xmax που αντιστοιχεί στην ενεργή συναλλαγή λειτουργεί ως κλείδωμα γραμμής. Εάν μια άλλη συναλλαγή θέλει να ενημερώσει ή να διαγράψει αυτήν τη σειρά, θα αναγκαστεί να περιμένει να ολοκληρωθεί η συναλλαγή xmax. Θα μιλήσουμε περισσότερο για τον αποκλεισμό αργότερα. Προς το παρόν, απλώς σημειώνουμε ότι ο αριθμός των κλειδαριών σειρών είναι απεριόριστος. Δεν καταλαμβάνουν χώρο στη μνήμη RAM και η απόδοση του συστήματος δεν υποφέρει από τον αριθμό τους. Είναι αλήθεια ότι οι «μακριές» συναλλαγές έχουν και άλλα μειονεκτήματα, αλλά περισσότερα για αυτό αργότερα.

Ας διαγράψουμε τη γραμμή.

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

Βλέπουμε ότι ο αριθμός συναλλαγής είναι γραμμένος στο πεδίο xmax, αλλά τα bit πληροφοριών δεν έχουν οριστεί:

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

Ακύρωση

Η ματαίωση των αλλαγών λειτουργεί παρόμοια με τη δέσμευση, μόνο στο XACT το ματαιωμένο bit ορίζεται για τη συναλλαγή. Η αναίρεση είναι τόσο γρήγορη όσο και η δέσμευση. Αν και η εντολή ονομάζεται ROLLBACK, οι αλλαγές δεν επαναφέρονται: ό,τι κατάφερε να αλλάξει η συναλλαγή στις σελίδες δεδομένων παραμένει αμετάβλητο.

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

Όταν γίνει πρόσβαση στη σελίδα, η κατάσταση θα ελεγχθεί και το bit υπόδειξης xmax_aborted θα οριστεί στην έκδοση σειράς. Ο ίδιος ο αριθμός xmax παραμένει στη σελίδα, αλλά κανείς δεν θα τον κοιτάξει.

=> 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)

Ενημέρωση

Η ενημέρωση λειτουργεί σαν να διέγραψε πρώτα την τρέχουσα έκδοση της σειράς και μετά να εισαγάγει μια νέα.

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

Το ερώτημα παράγει μία γραμμή (νέα έκδοση):

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

Αλλά στη σελίδα βλέπουμε και τις δύο εκδόσεις:

=> 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)

Η διαγραμμένη έκδοση επισημαίνεται με τον αριθμό της τρέχουσας συναλλαγής στο πεδίο xmax. Επιπλέον, αυτή η τιμή γράφεται πάνω από την παλιά, αφού η προηγούμενη συναλλαγή ακυρώθηκε. Και το bit xmax_aborted διαγράφεται επειδή η κατάσταση της τρέχουσας συναλλαγής δεν είναι ακόμη γνωστή.

Η πρώτη έκδοση της γραμμής αναφέρεται τώρα στη δεύτερη (πεδίο t_ctid) ως νεότερη.

Ένα δεύτερο ευρετήριο εμφανίζεται στη σελίδα ευρετηρίου και μια δεύτερη σειρά αναφέρεται στη δεύτερη έκδοση στη σελίδα του πίνακα.

Όπως και με τη διαγραφή, η τιμή xmax στην πρώτη έκδοση της σειράς είναι ένδειξη ότι η σειρά είναι κλειδωμένη.

Λοιπόν, ας ολοκληρώσουμε τη συναλλαγή.

=> COMMIT;

Индексы

Μέχρι στιγμής έχουμε μιλήσει μόνο για επιτραπέζιες σελίδες. Τι συμβαίνει μέσα στους δείκτες;

Οι πληροφορίες στις σελίδες ευρετηρίου ποικίλλουν σημαντικά ανάλογα με τον συγκεκριμένο τύπο ευρετηρίου. Και ακόμη και ένας τύπος ευρετηρίου έχει διαφορετικούς τύπους σελίδων. Για παράδειγμα, ένα δέντρο B έχει μια σελίδα μεταδεδομένων και "κανονικές" σελίδες.

Ωστόσο, η σελίδα έχει συνήθως μια σειρά δεικτών προς τις σειρές και τις ίδιες τις σειρές (όπως ακριβώς μια σελίδα πίνακα). Επιπλέον, στο τέλος της σελίδας υπάρχει χώρος για ειδικά δεδομένα.

Οι σειρές σε ευρετήρια μπορούν επίσης να έχουν πολύ διαφορετικές δομές ανάλογα με τον τύπο του ευρετηρίου. Για παράδειγμα, για ένα δέντρο Β, οι σειρές που σχετίζονται με τις σελίδες φύλλων περιέχουν την τιμή του κλειδιού ευρετηρίου και μια αναφορά (ctid) στην αντίστοιχη γραμμή πίνακα. Γενικά, ο δείκτης μπορεί να δομηθεί με εντελώς διαφορετικό τρόπο.

Το πιο σημαντικό σημείο είναι ότι δεν υπάρχουν εκδόσεις σειρών σε ευρετήρια οποιουδήποτε τύπου. Λοιπόν, ή μπορούμε να υποθέσουμε ότι κάθε γραμμή αντιπροσωπεύεται από ακριβώς μία έκδοση. Με άλλα λόγια, δεν υπάρχουν πεδία xmin και xmax στην κεφαλίδα της γραμμής ευρετηρίου. Μπορούμε να υποθέσουμε ότι οι σύνδεσμοι από το ευρετήριο οδηγούν σε όλες τις εκδόσεις πίνακα των σειρών - έτσι μπορείτε να καταλάβετε ποια έκδοση θα δει η συναλλαγή μόνο κοιτάζοντας τον πίνακα. (Όπως πάντα, αυτή δεν είναι όλη η αλήθεια. Σε ορισμένες περιπτώσεις, ο χάρτης ορατότητας μπορεί να βελτιστοποιήσει τη διαδικασία, αλλά θα το δούμε με περισσότερες λεπτομέρειες αργότερα.)

Ταυτόχρονα, στη σελίδα ευρετηρίου βρίσκουμε δείκτες και για τις δύο εκδόσεις, τόσο την τρέχουσα όσο και την παλιά:

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

Εικονικές συναλλαγές

Στην πράξη, η PostgreSQL χρησιμοποιεί βελτιστοποιήσεις που της επιτρέπουν να «αποθηκεύει» αριθμούς συναλλαγών.

Εάν μια συναλλαγή διαβάζει μόνο δεδομένα, δεν έχει καμία επίδραση στην ορατότητα των εκδόσεων σειρών. Επομένως, η διαδικασία υπηρεσίας εκδίδει πρώτα ένα εικονικό xid στη συναλλαγή. Ο αριθμός αποτελείται από ένα αναγνωριστικό διεργασίας και έναν αριθμό σειράς.

Η έκδοση αυτού του αριθμού δεν απαιτεί συγχρονισμό μεταξύ όλων των διεργασιών και επομένως είναι πολύ γρήγορη. Θα γνωρίσουμε έναν άλλο λόγο χρήσης εικονικών αριθμών όταν μιλάμε για πάγωμα.

Οι εικονικοί αριθμοί δεν λαμβάνονται υπόψη σε καμία περίπτωση στα στιγμιότυπα δεδομένων.

Σε διαφορετικά χρονικά σημεία, ενδέχεται να υπάρχουν εικονικές συναλλαγές στο σύστημα με αριθμούς που έχουν ήδη χρησιμοποιηθεί, και αυτό είναι φυσιολογικό. Ωστόσο, ένας τέτοιος αριθμός δεν μπορεί να γραφτεί σε σελίδες δεδομένων, επειδή την επόμενη φορά που θα αποκτήσετε πρόσβαση στη σελίδα μπορεί να χάσει κάθε νόημα.

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

Εάν μια συναλλαγή αρχίσει να αλλάζει δεδομένα, της δίνεται ένας πραγματικός, μοναδικός αριθμός συναλλαγής.

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

=> COMMIT;

Ένθετες συναλλαγές

Αποθήκευση πόντων

Ορίζεται σε SQL αποθήκευση πόντων (savepoint), που σας επιτρέπουν να ακυρώσετε μέρος μιας συναλλαγής χωρίς να τη διακόψετε εντελώς. Αυτό όμως δεν ταιριάζει στο παραπάνω διάγραμμα, καθώς η συναλλαγή έχει την ίδια κατάσταση για όλες τις αλλαγές της και φυσικά κανένα στοιχείο δεν επαναφέρεται.

Για την υλοποίηση αυτής της λειτουργικότητας, μια συναλλαγή με ένα σημείο αποθήκευσης χωρίζεται σε πολλά ξεχωριστά ένθετες συναλλαγές (υποσυναλλαγή), η κατάσταση της οποίας μπορεί να διαχειρίζεται χωριστά.

Οι ένθετες συναλλαγές έχουν τον δικό τους αριθμό (μεγαλύτερο από τον αριθμό της κύριας συναλλαγής). Η κατάσταση των ένθετων συναλλαγών καταγράφεται με τον συνήθη τρόπο στο XACT, αλλά η τελική κατάσταση εξαρτάται από την κατάσταση της κύριας συναλλαγής: εάν ακυρωθεί, τότε ακυρώνονται και όλες οι ένθετες συναλλαγές.

Οι πληροφορίες σχετικά με την ένθεση συναλλαγών αποθηκεύονται σε αρχεία στον κατάλογο PGDATA/pg_subtrans. Η πρόσβαση στα αρχεία γίνεται μέσω buffer στην κοινόχρηστη μνήμη του στιγμιότυπου, οργανωμένα με τον ίδιο τρόπο όπως τα buffer XACT.

Μην συγχέετε τις ένθετες συναλλαγές με τις αυτόνομες συναλλαγές. Οι αυτόνομες συναλλαγές δεν εξαρτώνται η μία από την άλλη με κανέναν τρόπο, αλλά οι ένθετες συναλλαγές εξαρτώνται. Δεν υπάρχουν αυτόνομες συναλλαγές στην κανονική PostgreSQL και, ίσως, προς το καλύτερο: χρειάζονται πολύ, πολύ σπάνια και η παρουσία τους σε άλλα DBMS προκαλεί κατάχρηση, από την οποία υποφέρουν όλοι.

Ας καθαρίσουμε τον πίνακα, ξεκινήσουμε μια συναλλαγή και εισάγουμε τη σειρά:

=> 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)

Τώρα ας βάλουμε ένα σημείο αποθήκευσης και ας εισάγουμε μια άλλη γραμμή.

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

Σημειώστε ότι η συνάρτηση txid_current() επιστρέφει τον αριθμό της κύριας συναλλαγής, όχι τον ένθετο αριθμό συναλλαγής.

=> 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)

Ας επιστρέψουμε στο σημείο αποθήκευσης και ας εισάγουμε την τρίτη γραμμή.

=> 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)

Στη σελίδα συνεχίζουμε να βλέπουμε τη σειρά που προστέθηκε από την ακυρωμένη ένθετη συναλλαγή.

Διορθώνουμε τις αλλαγές.

=> 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)

Τώρα μπορείτε να δείτε ξεκάθαρα ότι κάθε ένθετη συναλλαγή έχει τη δική της κατάσταση.

Σημειώστε ότι οι ένθετες συναλλαγές δεν μπορούν να χρησιμοποιηθούν ρητά στην SQL, δηλαδή, δεν μπορείτε να ξεκινήσετε μια νέα συναλλαγή χωρίς να ολοκληρώσετε την τρέχουσα. Αυτός ο μηχανισμός ενεργοποιείται έμμεσα κατά τη χρήση σημείων αποθήκευσης, καθώς και κατά το χειρισμό εξαιρέσεων PL/pgSQL και σε μια σειρά από άλλες, πιο εξωτικές περιπτώσεις.

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

Σφάλματα και ατομικότητα λειτουργιών

Τι συμβαίνει εάν παρουσιαστεί σφάλμα κατά την εκτέλεση μιας λειτουργίας; Για παράδειγμα, όπως αυτό:

=> 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;
ERROR:  current transaction is aborted, commands ignored until end of transaction block

Και ακόμα κι αν προσπαθήσετε να πραγματοποιήσετε τις αλλαγές, η PostgreSQL θα αναφέρει μια ματαίωση:

=> COMMIT;
ROLLBACK

Γιατί δεν μπορεί να συνεχιστεί μια συναλλαγή μετά από αποτυχία; Το γεγονός είναι ότι θα μπορούσε να προκύψει ένα σφάλμα με τέτοιο τρόπο ώστε να αποκτήσουμε πρόσβαση σε μέρος των αλλαγών - η ατομικότητα ούτε καν της συναλλαγής, αλλά ο χειριστής θα παραβιαζόταν. Όπως στο παράδειγμά μας, όπου ο χειριστής κατάφερε να ενημερώσει μία γραμμή πριν από το σφάλμα:

=> 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)

Πρέπει να ειπωθεί ότι το psql έχει μια λειτουργία που εξακολουθεί να επιτρέπει τη συνέχιση της συναλλαγής μετά από μια αποτυχία σαν να είχαν ανατραπεί οι ενέργειες του εσφαλμένου χειριστή.

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

Δεν είναι δύσκολο να μαντέψει κανείς ότι σε αυτήν τη λειτουργία, η psql βάζει στην πραγματικότητα ένα σιωπηρό σημείο αποθήκευσης πριν από κάθε εντολή και σε περίπτωση αποτυχίας ξεκινά μια επαναφορά σε αυτήν. Αυτή η λειτουργία δεν χρησιμοποιείται από προεπιλογή, καθώς η ρύθμιση σημείων αποθήκευσης (ακόμη και χωρίς επαναφορά σε αυτά) συνεπάγεται σημαντική επιβάρυνση.

Συνέχεια.

Πηγή: www.habr.com

Προσθέστε ένα σχόλιο