8.1 Identifiants des transactions
Il est désigné comme XID ou TxID (s'il y a une différence, dites-le moi). Les horodatages peuvent être utilisés comme TxID, ce qui peut jouer dans les mains si nous voulons restaurer toutes les actions à un moment donné. Le problème peut survenir si l'horodatage n'est pas suffisamment précis - les transactions peuvent alors obtenir le même ID.
Par conséquent, l'option la plus fiable consiste à générer des ID de produit UUID uniques. En Python c'est très simple :
>>> import uuid
>>> str(uuid.uuid4())
'f50ec0b7-f960-400d-91f0-c42a6d44e3d0'
>>> str(uuid.uuid4())
'd15bed89-c0a5-4a72-98d9-5507ea7bc0ba'
Il existe également une option pour hacher un ensemble de données définissant la transaction et utiliser ce hachage comme TxID.
8.2 Nouvelles tentatives
Si nous savons qu'une certaine fonction ou un programme est idempotent, cela signifie que nous pouvons et devons essayer de répéter son appel en cas d'erreur. Et nous devons juste être préparés au fait que certaines opérations donneront une erreur - étant donné que les applications modernes sont distribuées sur le réseau et le matériel, l'erreur ne doit pas être considérée comme une exception, mais comme la norme. L'erreur peut se produire en raison d'une panne de serveur, d'une erreur de réseau, d'une congestion d'application distante. Comment notre application doit-elle se comporter ? C'est vrai, essayez de répéter l'opération.
Puisqu'un morceau de code peut en dire plus qu'une page entière de mots, utilisons un exemple pour comprendre comment le mécanisme de nouvelle tentative naïve devrait idéalement fonctionner. Je vais le démontrer en utilisant la bibliothèque Tenacity (elle est si bien conçue que même si vous ne prévoyez pas de l'utiliser, l'exemple devrait vous montrer comment vous pouvez concevoir le mécanisme de récurrence) :
import logging
import random
import sys
from tenacity import retry, stop_after_attempt, stop_after_delay, wait_exponential, retry_if_exception_type, before_log
logging.basicConfig(stream=sys.stderr, level=logging.DEBUG)
logger = logging.getLogger(__name__)
@retry(
stop=(stop_after_delay(10) | stop_after_attempt(5)),
wait=wait_exponential(multiplier=1, min=4, max=10),
retry=retry_if_exception_type(IOError),
before=before_log(logger, logging.DEBUG)
)
def do_something_unreliable():
if random.randint(0, 10) > 1:
raise IOError("Broken sauce, everything is hosed!!!111one")
else:
return "Awesome sauce!"
print(do_something_unreliable.retry.statistics)
> Juste au cas où, je dirai : \@retry(...) est une syntaxe Python spéciale appelée "décorateur". C'est juste une fonction retry(...) qui encapsule une autre fonction et fait quelque chose avant ou après son exécution.
Comme nous pouvons le voir, les tentatives peuvent être conçues de manière créative :
- Vous pouvez limiter les tentatives par durée (10 secondes) ou nombre de tentatives (5).
- Peut être exponentiel (c'est-à-dire 2 ** un nombre croissant n ). ou d'une autre manière (par exemple, fixe) pour augmenter le temps entre les tentatives séparées. La variante exponentielle est appelée "effondrement de la congestion".
- Vous ne pouvez réessayer que pour certains types d'erreurs (IOError).
- Les nouvelles tentatives peuvent être précédées ou complétées par des entrées spéciales dans le journal.
Maintenant que nous avons terminé le cours de jeune combattant et que nous connaissons les éléments de base dont nous avons besoin pour travailler avec les transactions côté application, familiarisons-nous avec deux méthodes qui nous permettent de mettre en œuvre des transactions dans des systèmes distribués.
8.3 Outils avancés pour les amoureux des transactions
Je ne donnerai que des définitions assez générales, car ce sujet mérite un gros article séparé.
Validation en deux phases (2pc) . 2pc comporte deux phases : une phase de préparation et une phase de validation. Au cours de la phase de préparation, tous les microservices seront invités à se préparer à certaines modifications de données pouvant être effectuées de manière atomique. Une fois qu'ils sont tous prêts, la phase de validation apportera les modifications réelles. Pour coordonner le processus, un coordinateur global est nécessaire, qui verrouille les objets nécessaires - c'est-à-dire qu'ils deviennent inaccessibles pour les modifications jusqu'à ce que le coordinateur les déverrouille. Si un microservice particulier n'est pas prêt pour les modifications (par exemple, ne répond pas), le coordinateur interrompra la transaction et commencera le processus de restauration.
Pourquoi ce protocole est-il bon ? Il fournit l'atomicité. De plus, il garantit l'isolement lors de l'écriture et de la lecture. Cela signifie que les modifications apportées à une transaction ne sont pas visibles pour les autres tant que le coordinateur n'a pas validé les modifications. Mais ces propriétés ont aussi un inconvénient : ce protocole étant synchrone (bloquant), il ralentit le système (malgré le fait que l'appel RPC lui-même est assez lent). Et là encore, il y a un risque de blocage mutuel.
Saga . Dans ce modèle, une transaction distribuée est exécutée par des transactions locales asynchrones sur tous les microservices associés. Les microservices communiquent entre eux via un bus d'événements. Si un microservice ne parvient pas à terminer sa transaction locale, d'autres microservices effectueront des transactions de compensation pour annuler les modifications.
L'avantage de Saga est qu'aucun objet n'est bloqué. Mais il y a bien sûr des inconvénients.
Saga est difficile à déboguer, surtout lorsque de nombreux microservices sont impliqués. Un autre inconvénient du modèle Saga est qu'il manque d'isolement en lecture. Autrement dit, si les propriétés indiquées dans ACID sont importantes pour nous, alors Saga ne nous convient pas très bien.
Que voit-on de la description de ces deux techniques ? Le fait que dans les systèmes distribués, la responsabilité de l'atomicité et de l'isolation incombe à l'application. La même chose se produit lors de l'utilisation de bases de données qui ne fournissent pas de garanties ACID. C'est-à-dire que des choses comme la résolution de conflits, les retours en arrière, les commits et la libération d'espace incombent au développeur.
8.4 Comment savoir quand j'ai besoin de garanties ACID ?
Lorsqu'il existe une forte probabilité qu'un certain ensemble d'utilisateurs ou de processus travaillent simultanément sur les mêmes données .
Désolé pour la banalité, mais un exemple typique est celui des transactions financières.
Lorsque l'ordre dans lequel les transactions sont exécutées est important.
Imaginez que votre entreprise est sur le point de passer de la messagerie FunnyYellowChat à la messagerie FunnyRedChat, car FunnyRedChat vous permet d'envoyer des gifs, mais FunnyYellowChat ne le peut pas. Mais vous ne faites pas que changer de messager - vous migrez la correspondance de votre entreprise d'un messager à un autre. Vous faites cela parce que vos programmeurs étaient trop paresseux pour documenter les programmes et les processus quelque part de manière centralisée, et à la place, ils ont tout publié dans différents canaux du messager. Oui, et vos vendeurs ont publié les détails des négociations et des accords au même endroit. Bref, toute la vie de votre entreprise est là, et comme personne n'a le temps de transférer le tout à un service de documentation, et que la recherche de messageries instantanées marche bien, vous avez décidé au lieu de déblayer les décombres de simplement recopier tous les messages vers un nouvel emplacement. L'ordre des messages est important
Soit dit en passant, pour la correspondance dans un messager, l'ordre est généralement important, mais lorsque deux personnes écrivent quelque chose dans le même chat en même temps, alors en général, il n'est pas si important de savoir quel message apparaîtra en premier. Ainsi, pour ce scénario particulier, ACID ne serait pas nécessaire.
Un autre exemple possible est la bioinformatique. Je ne comprends pas du tout cela, mais je suppose que l'ordre est important lors du déchiffrement du génome humain. Cependant, j'ai entendu dire que les bioinformaticiens utilisent généralement certains de leurs outils pour tout - ils ont peut-être leurs propres bases de données.
Lorsque vous ne pouvez pas donner à un utilisateur ou traiter des données obsolètes.
Et encore - les transactions financières. Pour être honnête, je ne pouvais penser à aucun autre exemple.
Lorsque les transactions en cours sont associées à des coûts importants. Imaginez les problèmes qui peuvent survenir lorsqu'un médecin et une infirmière mettent à jour le dossier d'un patient et effacent les modifications de l'autre en même temps, car la base de données ne peut pas isoler les transactions. Le système de santé est un autre domaine, outre la finance, où les garanties ACID ont tendance à être essentielles.
8.5 Quand n'ai-je pas besoin d'ACID ?
Lorsque les utilisateurs ne mettent à jour qu'une partie de leurs données privées.
Par exemple, un utilisateur laisse des commentaires ou des notes autocollantes sur une page Web. Ou modifie les données personnelles dans un compte personnel avec un fournisseur de services.
Lorsque les utilisateurs ne mettent pas du tout à jour les données, mais les complètent uniquement avec de nouvelles (ajout).
Par exemple, une application en cours d'exécution qui enregistre des données sur vos courses : combien vous avez couru, pendant quelle heure, itinéraire, etc. Chaque nouvelle exécution est une nouvelle donnée, et les anciennes ne sont pas modifiées du tout. Peut-être que, sur la base des données, vous obtenez des analyses - et seules les bases de données NoSQL sont bonnes pour ce scénario.
Lorsque la logique métier ne détermine pas la nécessité d'un certain ordre dans lequel les transactions sont effectuées.
Probablement, pour un blogueur Youtube qui collecte des dons pour la production de nouveau matériel lors de la prochaine diffusion en direct, peu importe qui, quand et dans quel ordre, lui a jeté de l'argent.
Lorsque les utilisateurs resteront sur la même page Web ou la même fenêtre d'application pendant plusieurs secondes, voire plusieurs minutes, ils verront donc d'une manière ou d'une autre des données obsolètes.
Théoriquement, il s'agit de n'importe quel média d'information en ligne, ou du même Youtube. Ou "Habr". Lorsque cela ne vous importe pas que des transactions incomplètes puissent être temporairement stockées dans le système, vous pouvez les ignorer sans aucun dommage.
Si vous agrégez des données provenant de nombreuses sources et des données mises à jour à une fréquence élevée - par exemple, des données sur l'occupation des places de stationnement dans une ville qui changent au moins toutes les 5 minutes, alors en théorie, ce ne sera pas un gros problème pour vous si à un moment donné la transaction pour l'un des parkings n'aboutit pas. Bien sûr, cela dépend de ce que vous voulez faire exactement avec ces données.