Verrous PostgreSQL: 2. Verrous de chaîne

La dernière fois, nous avons parlé des verrous au niveau de l'objet , en particulier des verrous sur les relations. Aujourd'hui, nous verrons comment les verrous de ligne sont organisés dans PostgreSQL et comment ils sont utilisés avec les verrous d'objet, parlons des files d'attente et de ceux qui sortent de leur tour.



Verrous de ligne


Périphérique


Permettez-moi de vous rappeler plusieurs conclusions importantes du dernier article.

  • Un verrou doit exister quelque part dans la mémoire partagée du serveur.
  • Plus la granularité des verrous est élevée, moins la concurrence (contention) entre les processus s'exécutant simultanément est élevée.
  • En revanche, plus la granularité est élevée, plus l'espace mémoire est occupé par les verrous.

Nous voulons certainement que le changement d'une ligne ne bloque pas les autres lignes de la même table. Mais nous ne pouvons pas nous permettre de démarrer chaque ligne avec notre propre cadenas.

Il existe différentes manières de résoudre ce problème. Dans certains SGBD, il y a une augmentation du niveau des verrous: s'il y a trop de verrous au niveau des lignes, ils sont remplacés par un autre verrou général (par exemple, au niveau de la page ou de la table entière).

Comme nous le verrons plus loin, PostgreSQL utilise également ce mécanisme, mais uniquement pour les verrous de prédicat. Les verrous de ligne sont différents.

Dans PostgreSQL, les informations selon lesquelles une ligne est verrouillée sont stockées uniquement et exclusivement dans la version de la ligne à l'intérieur de la page de données (et non dans la RAM). Autrement dit, ce n'est pas du tout un bloc au sens habituel, mais juste un signe. Ce signe est en fait le numéro de transaction xmax en combinaison avec des bits d'informations supplémentaires; un peu plus tard, nous verrons en détail comment cela fonctionne.

Le plus est que nous pouvons bloquer autant de lignes que nous voulons sans consommer de ressources.

Mais il y a un inconvénient : puisque les informations sur le verrou ne sont pas présentées dans la RAM, d'autres processus ne peuvent pas rester en ligne. Et il n'y a pas de possibilité de surveillance (pour calculer les verrous, vous devez lire l'intégralité du tableau).

Eh bien, la surveillance est très bien, mais quelque chose doit être fait avec la file d'attente. Pour ce faire, vous devez toujours utiliser des verrous "normaux". Si nous devons attendre que la ligne soit libérée, en fait, nous devons attendre la fin de la transaction de blocage - tous les verrous sont libérés lors de la validation ou de la restauration. Et pour cela, vous pouvez demander un numéro de blocage d'une transaction de blocage (qui, je le rappelle, est détenu par la transaction elle-même en mode exceptionnel). Ainsi, le nombre de verrous utilisés est proportionnel au nombre de processus exécutés simultanément et non au nombre de lignes modifiées.

Modes exceptionnels


Au total, il existe 4 modes dans lesquels vous pouvez verrouiller la ligne. Parmi ceux-ci, deux modes représentent des verrous exclusifs qu'une seule transaction peut contenir à la fois.

  • Le mode FOR UPDATE implique une modification (ou suppression) complète d'une ligne.
  • FOR NO KEY UPDATE - modifie uniquement les champs qui ne sont pas inclus dans des index uniques (en d'autres termes, avec une telle modification, toutes les clés étrangères restent inchangées).

La commande UPDATE elle-même sélectionne le mode de verrouillage minimum approprié; les lignes sont généralement verrouillées en mode FOR NO KEY UPDATE.

Comme vous vous en souvenez , lors de la suppression ou de la modification d'une ligne, le numéro de version de la transaction actuelle est écrit dans le champ xmax de la version actuelle. Il montre que la version de la ligne a été supprimée par cette transaction. Ainsi, le même nombre xmax est utilisé comme signe de blocage. En fait, si xmax dans la version de la ligne correspond à une transaction active (pas encore terminée) et que nous voulons mettre à jour cette ligne particulière, nous devons attendre la fin de la transaction, donc un signe supplémentaire n'est pas nécessaire.

Voyons voir. Créez un tableau des comptes, le même que dans l'article précédent.

=> CREATE TABLE accounts( acc_no integer PRIMARY KEY, amount numeric ); => INSERT INTO accounts VALUES (1, 100.00), (2, 200.00), (3, 300.00); 

Pour regarder les pages, bien sûr, nous avons besoin de l'extension de pageinspect déjà familière.

 => CREATE EXTENSION pageinspect; 

Pour plus de commodité, créez une vue affichant uniquement les informations qui nous intéressent: xmax et quelques bits d'information.

 => CREATE VIEW accounts_v AS SELECT '(0,'||lp||')' AS ctid, t_xmax as xmax, CASE WHEN (t_infomask & 128) > 0 THEN 't' END AS lock_only, CASE WHEN (t_infomask & 4096) > 0 THEN 't' END AS is_multi, CASE WHEN (t_infomask2 & 8192) > 0 THEN 't' END AS keys_upd, CASE WHEN (t_infomask & 16) > 0 THEN 't' END AS keyshr_lock, CASE WHEN (t_infomask & 16+64) = 16+64 THEN 't' END AS shr_lock FROM heap_page_items(get_raw_page('accounts',0)) ORDER BY lp; 

Ainsi, nous démarrons la transaction et mettons à jour le montant du premier compte (la clé ne change pas) et le numéro du deuxième compte (la clé change):

 => BEGIN; => UPDATE accounts SET amount = amount + 100.00 WHERE acc_no = 1; => UPDATE accounts SET acc_no = 20 WHERE acc_no = 2; 

Nous examinons la vue:

 => SELECT * FROM accounts_v LIMIT 2; 
  ctid | xmax | lock_only | is_multi | keys_upd | keyshr_lock | shr_lock -------+--------+-----------+----------+----------+-------------+---------- (0,1) | 530492 | | | | | (0,2) | 530492 | | | t | | (2 rows) 

Le mode de verrouillage est déterminé par le bit d'information mis à jour par les clés.

Le même champ xmax est également utilisé lors du verrouillage d'une ligne avec la commande SELECT FOR UPDATE, mais dans ce cas, un bit d'information supplémentaire (xmax_lock_only) est déposé, ce qui indique que la version de la ligne est uniquement verrouillée, mais pas supprimée et est toujours pertinente.

 => ROLLBACK; => BEGIN; => SELECT * FROM accounts WHERE acc_no = 1 FOR NO KEY UPDATE; => SELECT * FROM accounts WHERE acc_no = 2 FOR UPDATE; 

 => SELECT * FROM accounts_v LIMIT 2; 
  ctid | xmax | lock_only | is_multi | keys_upd | keyshr_lock | shr_lock -------+--------+-----------+----------+----------+-------------+---------- (0,1) | 530493 | t | | | | (0,2) | 530493 | t | | t | | (2 rows) 

 => ROLLBACK; 


Modes partagés


Deux autres modes représentent des verrous partagés qui peuvent être détenus par plusieurs transactions.

  • Le mode FOR SHARE est utilisé lorsque vous devez lire une chaîne, mais vous ne devez pas autoriser sa modification par une autre transaction.
  • Le mode FOR KEY SHARE permet de changer la chaîne, mais uniquement les champs non clés. Ce mode, en particulier, est automatiquement utilisé par PostgreSQL lors de la vérification des clés étrangères.

Voyons voir.

 => BEGIN; => SELECT * FROM accounts WHERE acc_no = 1 FOR KEY SHARE; => SELECT * FROM accounts WHERE acc_no = 2 FOR SHARE; 

Dans les versions en ligne, nous voyons:

 => SELECT * FROM accounts_v LIMIT 2; 
  ctid | xmax | lock_only | is_multi | keys_upd | keyshr_lock | shr_lock -------+--------+-----------+----------+----------+-------------+---------- (0,1) | 530494 | t | | | t | (0,2) | 530494 | t | | | t | t (2 rows) 

Dans les deux cas, le bit keyshr_lock est défini et le mode SHARE peut être reconnu en examinant un bit d'information supplémentaire.

Voici à quoi ressemble la matrice de compatibilité de mode général.

modePOUR PARTAGE CLÉPOUR PARTAGERPOUR AUCUNE MISE À JOUR CLÉPOUR MISE À JOUR
POUR PARTAGE CLÉX
POUR PARTAGERXX
POUR AUCUNE MISE À JOUR CLÉXXX
POUR MISE À JOURXXXX

Cela montre que:

  • les modes exceptionnels entrent en conflit;
  • les modes partagés sont compatibles entre eux;
  • le mode FOR KEY SHARE partagé est compatible avec le mode exclusif FOR NO KEY UPDATE (c'est-à-dire que vous pouvez mettre à jour les champs non clés en même temps et vous assurer que la clé ne change pas).

Multi-transaction


Jusqu'à présent, nous pensions que le verrou était représenté par le numéro de la transaction de blocage dans le champ xmax. Mais les verrous partagés peuvent être détenus par plusieurs transactions et plusieurs numéros ne peuvent pas être écrits dans le même champ xmax. Comment être

Pour les verrous partagés, ce que l'on appelle les multi- transactions (MultiXact) sont utilisées. Il s'agit d'un groupe de transactions auquel est affecté un numéro distinct. Ce numéro a la même dimension qu'un numéro de transaction ordinaire, mais les numéros sont attribués indépendamment (c'est-à-dire que le système peut avoir les mêmes numéros de transaction et de transaction multiple). Pour les distinguer les uns des autres, un autre bit d'information (xmax_is_multi) est utilisé, et des informations détaillées sur les membres d'un tel groupe et les modes de verrouillage se trouvent dans les fichiers du répertoire $ PGDATA / pg_multixact /. Naturellement, les dernières données utilisées sont stockées dans des tampons dans la mémoire partagée du serveur pour un accès plus rapide.

Ajoutez aux verrous existants un autre verrou exceptionnel exécuté par une autre transaction (nous pouvons le faire, car les modes FOR KEY SHARE et FOR NO KEY UPDATE sont compatibles entre eux):

 | => BEGIN; | => UPDATE accounts SET amount = amount + 100.00 WHERE acc_no = 1; 

 => SELECT * FROM accounts_v LIMIT 2; 
  ctid | xmax | lock_only | is_multi | keys_upd | keyshr_lock | shr_lock -------+--------+-----------+----------+----------+-------------+---------- (0,1) | 61 | | t | | | (0,2) | 530494 | t | | | t | t (2 rows) 

Dans la première ligne, nous voyons que le numéro habituel a été remplacé par un numéro multi-transactions - cela est indiqué par le bit xmax_is_multi.

Afin de ne pas plonger dans les aspects internes de l'implémentation de multitransaction, vous pouvez utiliser une autre extension qui vous permet de voir toutes les informations sur tous les types de verrous de ligne de manière pratique.

 => CREATE EXTENSION pgrowlocks; => SELECT * FROM pgrowlocks('accounts') \gx 
 -[ RECORD 1 ]----------------------------- locked_row | (0,1) locker | 61 multi | t xids | {530494,530495} modes | {"Key Share","No Key Update"} pids | {5892,5928} -[ RECORD 2 ]----------------------------- locked_row | (0,2) locker | 530494 multi | f xids | {530494} modes | {"For Share"} pids | {5892} 

 => COMMIT; 

 | => ROLLBACK; 

Réglage du gel


Étant donné que des nombres séparés sont alloués pour les transactions multiples, qui sont écrites dans le champ xmax des versions de ligne, en raison de la limite de la capacité en bits du compteur, ils rencontrent le même problème de bouclage xid qu'avec un nombre normal.

Par conséquent, pour les numéros multi-transactions, il est également nécessaire d'effectuer un analogue de gel - remplacer les anciens numéros par de nouveaux (ou par un numéro de transaction normal, si au moment du gel, le verrou est détenu par une seule transaction).

Notez que le gel des numéros de transaction ordinaires n'est effectué que pour le champ xmin (car si la version de la ligne a un champ xmax non vide, alors c'est soit une version non pertinente et elle sera effacée, soit la transaction xmax est annulée et son numéro ne nous intéresse pas). Mais pour les multi-transactions, nous parlons du champ xmax de la version actuelle de la ligne, qui peut rester pertinent, mais il est constamment bloqué par différentes transactions en mode partagé.

Pour le gel des transactions multiples, des paramètres similaires aux paramètres du gel habituel sont responsables : vacuum_multixact_freeze_min_age , vacuum_multixact_freeze_table_age , autovacuum_multixact_freeze_max_age .

Qui est l'extrême?


Approchez progressivement du sucré. Voyons quelle est l'image des verrous lorsque plusieurs transactions vont mettre à jour la même ligne.

Commençons par construire une vue sur pg_locks. Premièrement, nous ferons la conclusion un peu plus compacte, et deuxièmement, nous nous limiterons aux verrous intéressants (en fait, nous rejetons les verrous des numéros de transaction virtuels, l'index sur la table des comptes, pg_locks et la vue elle-même - en général, tout ce qui n'est pas pertinent et seulement distrayant).

 => CREATE VIEW locks_v AS SELECT pid, locktype, CASE locktype WHEN 'relation' THEN relation::regclass::text WHEN 'transactionid' THEN transactionid::text WHEN 'tuple' THEN relation::regclass::text||':'||tuple::text END AS lockid, mode, granted FROM pg_locks WHERE locktype in ('relation','transactionid','tuple') AND (locktype != 'relation' OR relation = 'accounts'::regclass); 

Maintenant, lancez la première transaction et mettez à jour la ligne.

 => BEGIN; => SELECT txid_current(), pg_backend_pid(); 
  txid_current | pg_backend_pid --------------+---------------- 530497 | 5892 (1 row) 
 => UPDATE accounts SET amount = amount + 100.00 WHERE acc_no = 1; 
 UPDATE 1 

Et les serrures?

 => SELECT * FROM locks_v WHERE pid = 5892; 
  pid | locktype | lockid | mode | granted ------+---------------+----------+------------------+--------- 5892 | relation | accounts | RowExclusiveLock | t 5892 | transactionid | 530497 | ExclusiveLock | t (2 rows) 

La transaction contient la table et possède des verrous numériques. Jusqu'à présent, tout est attendu.

Nous commençons la deuxième transaction et essayons de mettre à jour la même ligne.

 | => BEGIN; | => SELECT txid_current(), pg_backend_pid(); 
 | txid_current | pg_backend_pid | --------------+---------------- | 530498 | 5928 | (1 row) 
 | => UPDATE accounts SET amount = amount + 100.00 WHERE acc_no = 1; 

Qu'en est-il des deuxièmes verrous de transaction?

 => SELECT * FROM locks_v WHERE pid = 5928; 
  pid | locktype | lockid | mode | granted ------+---------------+------------+------------------+--------- 5928 | relation | accounts | RowExclusiveLock | t 5928 | transactionid | 530498 | ExclusiveLock | t 5928 | transactionid | 530497 | ShareLock | f 5928 | tuple | accounts:1 | ExclusiveLock | t (4 rows) 

Et ici, c'est plus intéressant. En plus de verrouiller la table et son propre numéro, nous voyons deux autres verrous. La deuxième transaction a révélé que la ligne était verrouillée en premier et «bloquée» en attendant son numéro (accordé = f). Mais d'où et pourquoi le verrouillage de la version en ligne (locktype = tuple) vient-il?

Ne confondez pas le verrouillage de version de ligne (verrouillage de tuple) et le verrouillage de ligne (verrouillage de ligne). Le premier est un verrou de type tuple normal, visible dans pg_locks. Le second est une marque dans la page de données: xmax et bits d'information.

Lorsqu'une transaction est sur le point de modifier une ligne, elle effectue la séquence d'actions suivante:

  1. Capture un verrou exclusif sur une version mutable d'une chaîne (tuple).
  2. Si xmax et les bits d'information indiquent que la ligne est verrouillée, alors il demande le verrouillage du numéro de transaction xmax.
  3. Prescrit son xmax et les bits d'information nécessaires.
  4. Libère le verrouillage de version de ligne.

Lorsque la ligne a été mise à jour par la première transaction, elle a également saisi le verrou de version de ligne (étape 1), mais l'a immédiatement libéré (étape 4).

Lorsque la deuxième transaction est arrivée, elle a capturé le verrou de la version de ligne (élément 1), mais a été forcée de demander un verrou sur le numéro de la première transaction (élément 2) et l'a accroché.

Que se passe-t-il si une troisième transaction similaire apparaît? Elle tentera de capturer le verrou de la version de la ligne (élément 1) et se bloquera déjà à cette étape. Vérifiez-le.

 || => BEGIN; || => SELECT txid_current(), pg_backend_pid(); 
 || txid_current | pg_backend_pid || --------------+---------------- || 530499 | 5964 || (1 row) 
 || => UPDATE accounts SET amount = amount + 100.00 WHERE acc_no = 1; 

 => SELECT * FROM locks_v WHERE pid = 5964; 
  pid | locktype | lockid | mode | granted ------+---------------+------------+------------------+--------- 5964 | relation | accounts | RowExclusiveLock | t 5964 | tuple | accounts:1 | ExclusiveLock | f 5964 | transactionid | 530499 | ExclusiveLock | t (3 rows) 

Les quatrième, cinquième, etc. transactions qui souhaitent mettre à jour la même ligne ne seront pas différentes de la transaction 3 - elles «se bloqueront toutes» sur le même verrou de version de ligne.

Ajoutez une autre transaction au tas.

 ||| => BEGIN; ||| => SELECT txid_current(), pg_backend_pid(); 
 ||| txid_current | pg_backend_pid ||| --------------+---------------- ||| 530500 | 6000 ||| (1 row) 
 ||| => UPDATE accounts SET amount = amount - 100.00 WHERE acc_no = 1; 

 => SELECT * FROM locks_v WHERE pid = 6000; 
  pid | locktype | lockid | mode | granted ------+---------------+------------+------------------+--------- 6000 | relation | accounts | RowExclusiveLock | t 6000 | transactionid | 530500 | ExclusiveLock | t 6000 | tuple | accounts:1 | ExclusiveLock | f (3 rows) 

Une vue générale des attentes actuelles peut être vue dans la vue pg_stat_activity, ajoutant des informations sur les processus de blocage:

 => SELECT pid, wait_event_type, wait_event, pg_blocking_pids(pid) FROM pg_stat_activity WHERE backend_type = 'client backend'; 
  pid | wait_event_type | wait_event | pg_blocking_pids ------+-----------------+---------------+------------------ 5892 | | | {} 5928 | Lock | transactionid | {5892} 5964 | Lock | tuple | {5928} 6000 | Lock | tuple | {5928,5964} (4 rows) 

Il s'avère une sorte de «file d'attente», dans laquelle il y a le premier (celui qui détient la version de verrouillage de la chaîne) et tous les autres qui s'alignent derrière le premier.

Pourquoi avons-nous besoin d'un design aussi sophistiqué? Supposons que nous n'aurions pas de verrou de version pour la chaîne. Ensuite, les deuxième et troisième transactions (et ainsi de suite) attendraient le blocage du numéro de la première transaction. Au moment de l'achèvement de la première transaction, la ressource bloquée disparaît ( et que faites-vous ici, hein? La transaction est terminée ) et maintenant tout dépend de lequel des processus en attente sera réveillé en premier par le système d'exploitation et, par conséquent, aura le temps de verrouiller la ligne. Tous les autres processus seront également réveillés, mais ils devront à nouveau faire la queue - maintenant après un autre processus.

Cela se heurte au fait que certaines des transactions peuvent attendre indéfiniment leur tour, si, en raison d'une coïncidence malheureuse des circonstances, elles «contournent» toujours d'autres transactions. En anglais, cette situation est appelée famine de verrouillage.

Dans notre cas, il se passe à peu près la même chose, mais toujours un peu mieux: la transaction qui est arrivée dans le second est garantie qu'elle aura accès à la prochaine ressource. Mais qu'advient-il des éléments suivants (troisième et quatrième)?

Si la première transaction se termine par un rollback, tout ira bien: les transactions entrantes se dérouleront dans l'ordre où elles ont été alignées.

Mais - ce n'est pas de chance - si la première transaction se termine par un commit, alors non seulement le numéro de transaction disparaît, mais aussi la version de la ligne! Autrement dit, la version, bien sûr, reste, mais cesse d'être pertinente, et il sera nécessaire de mettre à jour une toute dernière version complètement différente (de la même ligne). La ressource, qui était au tour, disparaît, et tout le monde organise une course pour la possession d'une nouvelle ressource.

Laissez la première transaction se terminer avec commit.

 => COMMIT; 

La deuxième transaction sera réveillée et exécutera les paragraphes. 3 et 4.

 | UPDATE 1 

 => SELECT * FROM locks_v WHERE pid = 5928; 
  pid | locktype | lockid | mode | granted ------+---------------+----------+------------------+--------- 5928 | relation | accounts | RowExclusiveLock | t 5928 | transactionid | 530498 | ExclusiveLock | t (2 rows) 

Et la troisième transaction? Elle saute l'étape 1 (car la ressource a disparu) et reste bloquée à l'étape 2:

 => SELECT * FROM locks_v WHERE pid = 5964; 
  pid | locktype | lockid | mode | granted ------+---------------+----------+------------------+--------- 5964 | relation | accounts | RowExclusiveLock | t 5964 | transactionid | 530498 | ShareLock | f 5964 | transactionid | 530499 | ExclusiveLock | t (3 rows) 

Et la même chose se produit avec la quatrième transaction:

 => SELECT * FROM locks_v WHERE pid = 6000; 
  pid | locktype | lockid | mode | granted ------+---------------+----------+------------------+--------- 6000 | relation | accounts | RowExclusiveLock | t 6000 | transactionid | 530498 | ShareLock | f 6000 | transactionid | 530500 | ExclusiveLock | t (3 rows) 

Autrement dit, les troisième et quatrième transactions attendent la fin de la deuxième. La ligne s'est transformée en une foule de citrouilles .

Nous finalisons toutes les transactions commencées.

 | => COMMIT; 

 || UPDATE 1 
 || => COMMIT; 

 ||| UPDATE 1 
 ||| => COMMIT; 

Plus de détails sur le blocage des chaînes peuvent être trouvés dans README.tuplock .

Tu n'étais pas là


Ainsi, l'idée d'un schéma de blocage à deux niveaux est de réduire la probabilité d'une attente éternelle pour une transaction "malchanceuse". Néanmoins, comme nous l'avons déjà vu, une telle situation est tout à fait possible. Et si l'application utilise des verrous partagés, tout peut devenir encore plus triste.

Laissez la première transaction verrouiller la ligne en mode partagé.

 => BEGIN; => SELECT txid_current(), pg_backend_pid(); 
  txid_current | pg_backend_pid --------------+---------------- 530501 | 5892 (1 row) 
 => SELECT * FROM accounts WHERE acc_no = 1 FOR SHARE; 
  acc_no | amount --------+-------- 1 | 100.00 (1 row) 

La deuxième transaction essaie de mettre à jour la même ligne, mais ne peut pas - les modes SHARE et NO KEY UPDATE sont incompatibles.

 | => BEGIN; | => SELECT txid_current(), pg_backend_pid(); 
 | txid_current | pg_backend_pid | --------------+---------------- | 530502 | 5928 | (1 row) 
 | => UPDATE accounts SET amount = amount + 100.00 WHERE acc_no = 1; 

La deuxième transaction attend la fin de la première et maintient le verrou de version de ligne - pour l'instant, tout est comme la dernière fois.

 => SELECT * FROM locks_v WHERE pid = 5928; 
  pid | locktype | lockid | mode | granted ------+---------------+-------------+------------------+--------- 5928 | relation | accounts | RowExclusiveLock | t 5928 | tuple | accounts:10 | ExclusiveLock | t 5928 | transactionid | 530501 | ShareLock | f 5928 | transactionid | 530502 | ExclusiveLock | t (4 rows) 

Et puis une troisième transaction apparaît qui veut un verrou partagé. Le problème est qu'il n'essaie pas de capturer le verrou sur la version de la ligne (car il ne va pas changer la ligne), mais qu'il rampe simplement à son tour - il est compatible avec la première transaction.

 || BEGIN || => SELECT txid_current(), pg_backend_pid(); 
 || txid_current | pg_backend_pid || --------------+---------------- || 530503 | 5964 || (1 row) 
 || => SELECT * FROM accounts WHERE acc_no = 1 FOR SHARE; 
 || acc_no | amount || --------+-------- || 1 | 100.00 || (1 row) 

Et maintenant, deux transactions bloquent la ligne:

 => SELECT * FROM pgrowlocks('accounts') \gx 
 -[ RECORD 1 ]--------------- locked_row | (0,10) locker | 62 multi | t xids | {530501,530503} modes | {Share,Share} pids | {5892,5964} 

Que se passe-t-il maintenant lorsque la première transaction est terminée? La deuxième transaction sera réveillée, mais elle verra que le verrou de ligne n'a disparu nulle part et restera à nouveau dans la «file d'attente» - cette fois pour la troisième transaction:

 => COMMIT; => SELECT * FROM locks_v WHERE pid = 5928; 
  pid | locktype | lockid | mode | granted ------+---------------+-------------+------------------+--------- 5928 | relation | accounts | RowExclusiveLock | t 5928 | tuple | accounts:10 | ExclusiveLock | t 5928 | transactionid | 530503 | ShareLock | f 5928 | transactionid | 530502 | ExclusiveLock | t (4 rows) 

Et seulement lorsque la troisième transaction est terminée (et si aucun autre verrou partagé n'apparaît pendant ce temps), le second pourra effectuer la mise à jour.

 || => COMMIT; 

 | UPDATE 1 
 | => ROLLBACK; 

Il est peut-être temps de tirer des conclusions pratiques.

  • Mettre à jour la même ligne dans une table en même temps dans de nombreux processus parallèles n'est pas une bonne idée.
  • Si vous utilisez des verrous partagés de type SHARE dans l'application, alors discrètement.
  • La vérification des clés étrangères ne doit pas interférer, car les champs de clé ne changent généralement pas et les modes KEY SHARE et NO KEY UPDATE sont compatibles.


Demandé de ne pas emprunter


En règle générale, les commandes SQL s'attendent à libérer les ressources dont elles ont besoin. Mais parfois, vous voulez refuser d'exécuter la commande si le verrou n'a pas pu être obtenu immédiatement. Pour ce faire, des commandes telles que SELECT, LOCK, ALTER, vous permettent d'utiliser la phrase NOWAIT.

Par exemple:

 => BEGIN; => UPDATE accounts SET amount = amount + 100.00 WHERE acc_no = 1; 

 | => SELECT * FROM accounts FOR UPDATE NOWAIT; 
 | ERROR: could not obtain lock on row in relation "accounts" 

La commande échoue immédiatement si la ressource est occupée. Dans le code d'application, une telle erreur peut être interceptée et traitée.

Vous ne pouvez pas spécifier la phrase NOWAIT pour les commandes UPDATE et DELETE, mais vous pouvez d'abord exécuter SELECT FOR UPDATE NOWAIT, puis, si possible, mettre à jour ou supprimer la ligne.

Il existe une autre option pour ne pas attendre - utilisez la commande SELECT FOR avec la phrase SKIP LOCKED. Une telle commande sautera les lignes verrouillées, mais traitera les lignes libres.

 | => BEGIN; | => DECLARE c CURSOR FOR | SELECT * FROM accounts ORDER BY acc_no FOR UPDATE SKIP LOCKED; | => FETCH c; 
 | acc_no | amount | --------+-------- | 2 | 200.00 | (1 row) 

Dans cet exemple, la première ligne bloquée a été sautée et nous avons immédiatement reçu (et bloqué) la seconde.

En pratique, cela vous permet d'organiser le traitement multi-thread des files d'attente. Vous ne devriez pas trouver une autre application pour cette commande - si vous voulez l'utiliser, alors vous perdrez probablement de vue une solution plus simple.

 => ROLLBACK; 
 | => ROLLBACK; 

À suivre .

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


All Articles