Transactions dans les globaux InterSystems IRIS

Transactions dans les globaux InterSystems IRISLe SGBD InterSystems IRIS prend en charge des structures intéressantes pour stocker des données - globales. Il s'agit essentiellement de clés à plusieurs niveaux avec divers avantages supplémentaires sous forme de transactions, de fonctions rapides pour parcourir les arborescences de données, de verrous et de son propre langage ObjectScript.

Apprenez-en davantage sur les globals dans la série d’articles « Les globals sont des épées au trésor pour stocker des données » :

Des arbres. Partie 1
Des arbres. Partie 2
Tableaux clairsemés. Partie 3

Je me suis intéressé à la manière dont les transactions sont implémentées dans les globaux et à leurs fonctionnalités. Après tout, il s’agit d’une structure de stockage de données complètement différente de celle des tables habituelles. Niveau bien inférieur.

Comme le montre la théorie des bases de données relationnelles, une bonne implémentation des transactions doit satisfaire aux exigences ACID:

A - Atomique (atomicité). Toutes les modifications apportées à la transaction, voire aucune, sont enregistrées.

C - Cohérence. Une fois la transaction terminée, l'état logique de la base de données doit être cohérent en interne. À bien des égards, cette exigence concerne le programmeur, mais dans le cas des bases de données SQL, elle concerne également les clés étrangères.

I - Isoler. Les transactions exécutées en parallèle ne doivent pas s’influencer mutuellement.

D-Durable. Après la réussite d'une transaction, les problèmes aux niveaux inférieurs (panne de courant, par exemple) ne devraient pas affecter les données modifiées par la transaction.

Les globaux sont des structures de données non relationnelles. Ils ont été conçus pour fonctionner très rapidement sur un matériel très limité. Regardons l'implémentation des transactions dans les globales en utilisant image officielle du menu fixe IRIS.

Pour prendre en charge les transactions dans IRIS, les commandes suivantes sont utilisées : TSTART, TCOMMIT, TROLLBACK.

1. Atomicité

Le moyen le plus simple de vérifier est l’atomicité. Nous vérifions depuis la console de la base de données.

Kill ^a
TSTART
Set ^a(1) = 1
Set ^a(2) = 2
Set ^a(3) = 3
TCOMMIT

Nous concluons alors :

Write ^a(1), “ ”, ^a(2), “ ”, ^a(3)

Nous obtenons:

1 2 3

Tout va bien. L'atomicité est maintenue : tous les changements sont enregistrés.

Compliquons la tâche, introduisons une erreur et voyons comment la transaction est enregistrée, partiellement ou pas du tout.

Vérifions à nouveau l'atomicité :

Kill ^A
TSTART
Set ^a(1) = 1
Set ^a(2) = 2
Set ^a(3) = 3

Ensuite, nous arrêterons de force le conteneur, le lancerons et verrons.

docker kill my-iris

Cette commande équivaut presque à un arrêt forcé, car elle envoie un signal SIGKILL pour arrêter immédiatement le processus.

Peut-être que la transaction a été partiellement enregistrée ?

WRITE ^a(1), ^a(2), ^a(3)
^
<UNDEFINED> ^a(1)

- Non, il n'a pas survécu.

Essayons la commande rollback :

Kill ^A
TSTART
Set ^a(1) = 1
Set ^a(2) = 2
Set ^a(3) = 3
TROLLBACK

WRITE ^a(1), ^a(2), ^a(3)
^
<UNDEFINED> ^a(1)

Rien n’a survécu non plus.

2. Cohérence

Puisque dans les bases de données basées sur des globales, les clés sont également créées sur des globales (rappelons qu'une globale est une structure de niveau inférieur pour stocker des données qu'une table relationnelle), pour répondre à l'exigence de cohérence, une modification de la clé doit être incluse dans la même transaction qu'un changement dans le global.

Par exemple, nous avons un ^person global, dans lequel nous stockons des personnalités et nous utilisons le TIN comme clé.

^person(1234567, ‘firstname’) = ‘Sergey’
^person(1234567, ‘lastname’) = ‘Kamenev’
^person(1234567, ‘phone’) = ‘+74995555555
...

Afin d'avoir une recherche rapide par nom et prénom, nous avons créé la clé ^index.

^index(‘Kamenev’, ‘Sergey’, 1234567) = 1

Pour que la base de données soit cohérente, il faut ajouter le persona comme ceci :

TSTART
^person(1234567, ‘firstname’) = ‘Sergey’
^person(1234567, ‘lastname’) = ‘Kamenev’
^person(1234567, ‘phone’) = ‘+74995555555
^index(‘Kamenev’, ‘Sergey’, 1234567) = 1
TCOMMIT

Par conséquent, lors de la suppression, nous devons également utiliser une transaction :

TSTART
Kill ^person(1234567)
ZKill ^index(‘Kamenev’, ‘Sergey’, 1234567)
TCOMMIT

En d’autres termes, le respect de l’exigence de cohérence repose entièrement sur les épaules du programmeur. Mais lorsqu’il s’agit de variables globales, cela est normal, en raison de leur nature de bas niveau.

3. Isolement

C'est là que commencent les étendues sauvages. De nombreux utilisateurs travaillent simultanément sur la même base de données et modifient les mêmes données.

La situation est comparable à celle où de nombreux utilisateurs travaillent simultanément avec le même référentiel de code et tentent de valider simultanément les modifications apportées à plusieurs fichiers à la fois.

La base de données devrait tout trier en temps réel. Considérant que dans les entreprises sérieuses, il y a même une personne spéciale qui est responsable du contrôle des versions (pour la fusion des branches, la résolution des conflits, etc.), et que la base de données doit faire tout cela en temps réel, la complexité de la tâche et l'exactitude du conception de la base de données et code qui la sert.

La base de données ne peut pas comprendre le sens des actions effectuées par les utilisateurs afin d'éviter les conflits s'ils travaillent sur les mêmes données. Il ne peut annuler qu'une transaction en conflit avec une autre ou les exécuter séquentiellement.

Un autre problème est que lors de l'exécution d'une transaction (avant une validation), l'état de la base de données peut être incohérent, il est donc souhaitable que les autres transactions n'aient pas accès à l'état incohérent de la base de données, ce qui est obtenu dans les bases de données relationnelles. de plusieurs manières : création d'instantanés, lignes multi-versions, etc.

Lors de l'exécution de transactions en parallèle, il est important pour nous qu'elles n'interfèrent pas les unes avec les autres. C'est la propriété de l'isolement.

SQL définit 4 niveaux d'isolement :

  • LIRE NON ENGAGÉ
  • LIRE ENGAGÉ
  • LECTURE RÉPÉTABLE
  • SÉRIALISABLE

Examinons chaque niveau séparément. Les coûts de mise en œuvre de chaque niveau augmentent de manière presque exponentielle.

LIRE NON ENGAGÉ - c'est le niveau d'isolement le plus bas, mais en même temps le plus rapide. Les transactions peuvent lire les modifications apportées les unes par les autres.

LIRE ENGAGÉ est le prochain niveau d’isolement, qui est un compromis. Les transactions ne peuvent pas lire les modifications des autres avant la validation, mais elles peuvent lire toutes les modifications apportées après la validation.

Si nous avons une longue transaction T1, au cours de laquelle des validations ont eu lieu dans les transactions T2, T3 ... Tn, qui travaillaient avec les mêmes données que T1, alors lors de la demande de données dans T1, nous obtiendrons un résultat différent à chaque fois. Ce phénomène est appelé lecture non répétable.

LECTURE RÉPÉTABLE — dans ce niveau d'isolement, nous n'avons pas de phénomène de lecture non répétable, du fait que pour chaque demande de lecture de données, un instantané des données de résultat est créé et lorsqu'il est réutilisé dans la même transaction, les données de l'instantané est utilisé. Cependant, il est possible de lire des données fantômes à ce niveau d'isolement. Cela fait référence à la lecture de nouvelles lignes ajoutées par des transactions validées parallèles.

SÉRIALISABLE — le plus haut niveau d'isolation. Elle se caractérise par le fait que les données utilisées de quelque manière que ce soit dans une transaction (lecture ou modification) ne deviennent disponibles pour d'autres transactions qu'après la réalisation de la première transaction.

Tout d’abord, voyons s’il existe une isolation des opérations dans une transaction par rapport au thread principal. Ouvrons 2 fenêtres de terminal.

Kill ^t

Write ^t(1)
2

TSTART
Set ^t(1)=2

Il n'y a pas d'isolement. Un fil voit ce que fait le deuxième qui a ouvert la transaction.

Voyons si les transactions de différents threads voient ce qui se passe à l'intérieur d'eux.

Ouvrons 2 fenêtres de terminal et ouvrons 2 transactions en parallèle.

kill ^t
TSTART
Write ^t(1)
3

TSTART
Set ^t(1)=3

Les transactions parallèles voient les données des autres. Nous avons donc obtenu le niveau d'isolement le plus simple, mais aussi le plus rapide, READ UNCOMMITED.

En principe, on pourrait s'attendre à cela pour les mondiaux, pour lesquels la performance a toujours été une priorité.

Et si nous avions besoin d’un niveau plus élevé d’isolement dans les opérations sur les marchés mondiaux ?

Ici, vous devez réfléchir aux raisons pour lesquelles des niveaux d’isolement sont nécessaires et à la manière dont ils fonctionnent.

Le niveau d'isolement le plus élevé, SERIALIZE, signifie que le résultat des transactions exécutées en parallèle est équivalent à leur exécution séquentielle, ce qui garantit l'absence de collisions.

Nous pouvons le faire en utilisant les verrous intelligents dans ObjectScript, qui ont de nombreuses utilisations différentes : vous pouvez effectuer un verrouillage multiple régulier, incrémentiel avec la commande VERROUILLAGE.

Des niveaux d'isolement inférieurs sont des compromis conçus pour augmenter la vitesse de la base de données.

Voyons comment nous pouvons atteindre différents niveaux d'isolement à l'aide de verrous.

Cet opérateur vous permet de prendre non seulement les verrous exclusifs nécessaires à la modification des données, mais également les verrous dits partagés, qui peuvent prendre plusieurs threads en parallèle lorsqu'ils doivent lire des données qui ne doivent pas être modifiées par d'autres processus pendant le processus de lecture.

Plus d'informations sur la méthode de blocage en deux phases en russe et en anglais :

Blocage biphasé
Verrouillage biphasé

La difficulté est que lors d'une transaction, l'état de la base de données peut être incohérent, mais ces données incohérentes sont visibles par les autres processus. Comment éviter cela ?

A l'aide de verrous, nous allons créer des fenêtres de visibilité dans lesquelles l'état de la base de données sera cohérent. Et tous les accès à ces fenêtres de visibilité de l'État convenu seront contrôlés par des serrures.

Les verrous partagés sur les mêmes données sont réutilisables : plusieurs processus peuvent les prendre. Ces verrous empêchent d'autres processus de modifier les données, c'est-à-dire ils sont utilisés pour former des fenêtres d'état de base de données cohérent.

Des verrous exclusifs sont utilisés pour les modifications de données - un seul processus peut prendre un tel verrou. Un cadenas exclusif peut être pris par :

  1. Tout processus si les données sont gratuites
  2. Seul le processus qui dispose d'un verrou partagé sur ces données et qui a été le premier à demander un verrou exclusif.

Transactions dans les globaux InterSystems IRIS

Plus la fenêtre de visibilité est étroite, plus les autres processus doivent attendre longtemps, mais plus l'état de la base de données qu'elle contient peut être cohérent.

READ_COMMITTED — l'essence de ce niveau est que nous ne voyons que les données validées provenant d'autres threads. Si les données d'une autre transaction n'ont pas encore été validées, nous voyons alors son ancienne version.

Cela nous permet de paralléliser le travail au lieu d'attendre que le verrou soit libéré.

Sans astuces particulières, nous ne pourrons pas voir l'ancienne version des données dans IRIS, nous devrons donc nous contenter de verrous.

En conséquence, nous devrons utiliser des verrous partagés pour permettre la lecture des données uniquement à des moments de cohérence.

Disons que nous avons une base d'utilisateurs ^personnes qui se transfèrent de l'argent.

Moment du transfert de la personne 123 à la personne 242 :

LOCK +^person(123), +^person(242)
Set ^person(123, amount) = ^person(123, amount) - amount
Set ^person(242, amount) = ^person(242, amount) + amount
LOCK -^person(123), -^person(242)

Le moment de demander le montant d'argent à la personne 123 avant le débit doit être accompagné d'un blocage exclusif (par défaut) :

LOCK +^person(123)
Write ^person(123)

Et si vous devez afficher l'état du compte dans votre compte personnel, vous pouvez alors utiliser un verrou partagé ou ne pas l'utiliser du tout :

LOCK +^person(123)#”S”
Write ^person(123)

Cependant, si nous supposons que les opérations sur la base de données sont effectuées presque instantanément (permettez-moi de vous rappeler que les globales sont une structure de niveau bien inférieur à celle d'une table relationnelle), alors la nécessité de ce niveau diminue.

LECTURE RÉPÉTABLE - Ce niveau d'isolement permet plusieurs lectures de données qui peuvent être modifiées par des transactions simultanées.

En conséquence, nous devrons mettre un verrou partagé sur la lecture des données que nous modifions et des verrous exclusifs sur les données que nous modifions.

Heureusement, l'opérateur LOCK vous permet de lister en détail tous les verrous nécessaires, qui peuvent être nombreux, dans une seule instruction.

LOCK +^person(123, amount)#”S”
чтение ^person(123, amount)

d'autres opérations (à ce moment, les threads parallèles essaient de changer ^person(123, montant), mais ne le peuvent pas)

LOCK +^person(123, amount)
изменение ^person(123, amount)
LOCK -^person(123, amount)

чтение ^person(123, amount)
LOCK -^person(123, amount)#”S”

Lorsque vous répertoriez les verrous séparés par des virgules, ils sont pris séquentiellement, mais si vous faites ceci :

LOCK +(^person(123),^person(242))

puis ils sont pris atomiquement en une seule fois.

SÉRIALISER — nous devrons définir des verrous pour qu'à terme, toutes les transactions ayant des données communes soient exécutées séquentiellement. Pour cette approche, la plupart des verrous doivent être exclusifs et appliqués aux plus petites zones du monde pour des raisons de performance.

Si nous parlons de débiter des fonds chez la personne globale, alors seul le niveau d'isolement SERIALIZE est acceptable, car l'argent doit être dépensé de manière strictement séquentielle, sinon il est possible de dépenser le même montant plusieurs fois.

4. Durabilité

J'ai effectué des tests avec découpe dure du récipient en utilisant

docker kill my-iris

La base les a bien tolérés. Aucun problème n'a été identifié.

Conclusion

Pour les globaux, InterSystems IRIS prend en charge les transactions. Ils sont vraiment atomiques et fiables. Pour assurer la cohérence d'une base de données basée sur des valeurs globales, des efforts de programmation et l'utilisation de transactions sont nécessaires, car elle n'a pas de constructions intégrées complexes telles que des clés étrangères.

Le niveau d'isolement des globals sans utiliser de verrous est READ UNCOMMITED, et lors de l'utilisation de verrous, il peut être assuré jusqu'au niveau SERIALIZE.

L'exactitude et la rapidité des transactions sur les transactions globales dépendent beaucoup des compétences du programmeur : plus les verrous partagés sont utilisés lors de la lecture, plus le niveau d'isolement est élevé, et plus les verrous étroitement exclusifs sont pris, plus les performances sont rapides.

Source: habr.com

Ajouter un commentaire