Bases, Conception logicielle
     

Cohérence, précision et répétabilité éventuelles dans la synchronisation des données

La construction de systèmes distribués, évolutifs est complexe et, encore plus fréquent, grâce au nombre croissant d’appareils IoT et mobiles. Un problème que je vois régulièrement est que les développeurs tentent de traiter les systèmes distribués comme les systèmes monolithiques. Plus précisément, les développeurs qui s’accrochent à des modèles tels que ACID.

ACID, a besoin d’atomicité, de cohérence, d’isolement et de durabilité, représente un pilier central des bases de données relationnelles (SDR). Pour de nombreuses applications, les comportements décrits par ACID, et appliqués par les SDR sont exactement ce qui est souhaité. Personne ne veut des conditions de course telles que les lectures sales. Toutefois, vous lisez ceci parce que vous êtes probablement intéressé par les systèmes distribués – en bref, vous n’êtes pas la construction de l’application typique, et certainement pas les types d’applications envisagées dans les années 1960 lorsque Codd et. al. a jeté les bases des SDR modernes.

picture of Saturn, God of time
Peut-être Saturne, dieu du temps, sait-il quelques choses sur la cohérence éventuelle?

La plupart des éléments d’une conception classique de base de données tournent autour du concept d’une autorité centrale (généralement le RDMS lui-même) gardant toutes les lectures et les écrits. Cela fonctionne bien lorsque tous les clients peuvent maintenir une connexion à l’autorité centrale (par exemple, une connexion de base de données) chaque fois qu’il peut désirer effectuer une écriture. À condition que vous puissiez répondre à cette exigence, et de fournir que vous pouvez mettre à l’échelle ce goulot d’étranglement singulier que le nombre de clients et l’utilisation augmente, alors ce n’est pas une mauvaise solution. En fait, c’est une excellente solution, car la rigidité de l’ACIDE existe pour une raison, et les SDR protègent contre tout type de mauvais scénarios de données.

Il est clair que vous ne construisez pas quelque chose qui est « toujours connecté », et donc stricte ACIDity ne peut pas être atteint. Qu’est-ce que tu fais alors ?

Pour répondre à cette question, prenons du recul et demandons pourquoi ce que nous essayons d’accomplir et, par extension, ce qui nous importe. Pour de nombreux systèmes, cela peut être articulé par l’énoncé simple :

« Je veux m’assurer que toutes les mises à jour sont traitées correctement, et si un conflit survient que l’application fait la bonne chose. »

Ok, assez bien. Ces exigences définissent clairement le point de vue de l’utilisateur, mais laissent évidemment un peu de place à l’interprétation – nous allons courir avec cela et évaluer les ambiguïtés au fur et à mesure qu’elles se présentent.

Exigence 1 : « assurez-vous que toutes les mises à jour sont traitées correctement. »

En supposant que « correctement » signifie « quand une mise à jour arrive, le traiter de la même manière que si le client a toujours été connecté », alors nous pouvons accomplir cela assez facilement. Il suffit que le client hors ligne file d’attente de leurs demandes de mise à jour, et quand ils atteignent un état en ligne, il suffit de les transmettre au serveur, itérer sur eux dans l’ordre chronologique, le traitement de chacun. À condition qu’il n’y ait pas de conflits, c’est assez simple – bien que, comme nous le verrons, il se trouve le frottement.

Exigence 2 : « Si un conflit survient, faites ce qu’il faut »

Comme vous l’avez probablement supposé, c’est là que les choses se compliquent. Les conflits se sous de nombreuses formes, nous définirons chacun à tour de rôle, puis discuterons de solutions générales à tous ces problèmes.

Mises à jour multiples

Si plusieurs clients tentent de mettre à jour le même enregistrement, que faites-vous? Que se passe-t-il s’ils mettent à jour le même champ, ou peut-être des champs différents pour le même enregistrement ?

Champs dérivés/calculés

De nombreuses applications renoncent à une normalisation stricte des schémas de base de données pour les optimisations de performances, et parfois cela prend la forme de champs calculés. La somme des dossiers d’enfants, l’état d’un objet (p. ex., états de machine d’État), etc. Que se passe-t-il lorsque des mises à jour contradictoires (voir ci-dessus) ont également un effet sur ce qui est autrement un champ calculé ?

Suppressions

Idéalement, vous devriez éviter les suppressions dures, et la mise en œuvre d’une certaine forme de suppression logique, mais comment ces modèles tenir avec les conflits?

Comment procédons-nous?

Avec la ventilation précédente des types de conflit à l’écart, passons en revue comment nous pouvons surmonter cela dans un système déconnecté.

Heureusement, une solution facile existe en raison de la similitude de chacun de ces types de conflits – et, d’ailleurs, de tous les conflits. Tous les conflits sont essentiellement des erreurs de timing. Chose A était censé précéder la chose B, mais en raison de la vie, l’univers, et tout ce que nous obtenons B avant A. Tu le sais parce que tu es un humain. Vous pouvez visualiser ces scénarios.

Par exemple, un scénario impliquant un fournisseur de stockage liquide, qui utilise une application à base de tablettes pour enregistrer les audits quotidiens de tous leurs réservoirs de stockage, y compris les mesures de suivi. Imaginez alors, si:

  1. Fred mesure une prise d’entreposage avant que l’expédition quotidienne soit reçue, et avait enregistré la mesure quotidienne à 47 litres. Fred, cependant, est hors ligne pendant ce temps en raison du manque de connectivité Internet appropriée à l’installation de stockage.
  2. Alice, assise dans le bureau, mettant à jour le niveau de liquide du même réservoir de stockage des jours précédents lecture de 50 litres à 150 litres parce qu’elle vient de recevoir une notification d’un technicien sur le terrain que leur livraison quotidienne vient d’être reçu et le chauffeur-livreur a informé Alice de la lecture correcte après avoir garniture du réservoir.
  3. Plus tard dans la journée, Fred se connectera finalement à Internet, et ses lectures précédemment enregistrées peuvent être transmises.

Dans ce scénario, malgré la transmission de Fred qui se produit après la mise à jour d’Alice, notre intuition humaine est que la mise à jour de Fred doit être ignorée. Peut-être gardé, en cas de vérification, ou un patron trop exigeant se demandant pourquoi Fred ne soumet jamais ses lectures pour ce réservoir, mais certainement nous n’avons pas l’espoir que les lectures de Fred devrait l’empêcher que Alice entrée dans le système.

C’est parce que les mises à jour de Fred se sont produites chronologiquement avant celles enregistrées par Alice, et c’est là que réside notre solution. Notre système doit préserver et respecter l’ordre chronologique que les mises à jour auraient dû se produire, pas nécessairement lorsqu’elles ont été reçues par le serveur central. En fait, le temps qu’une mise à jour est reçue par un serveur est plus d’un artefact technique, bien qu’un curieux, mais certainement pas un d’intérêt pour les utilisateurs moyens du système.

Pour remédier à cela, nous devons ajouter un autre champ à chaque modèle sur qui nous avons l’intention de permettre des mises à jour hors ligne. Ce champ doit suivre l’amorti temps précis que l’utilisateur a indiqué qu’il voulait qu’une action se produise. Appelons cela la « date de soumission ».

En revisitant notre exemple ci-dessus, la date de soumission pour la mise à jour de Fred pourrait être de 9 h le 1er novembre, alors qu’Alice aurait pu être 11 h 45 le même jour. Si nous avions capturé ces timestamps, quand le dossier de Fred arrive, nous pouvons clairement voir qu’il soumet une valeur qui était destiné à arriver beaucoup plus tôt. En fait, nous pouvons même voir que nous avons un nouvel enregistrement, et peut choisir de ne pas tenir compte de sa mise à jour.

À ce stade, vous pouvez voir l’importance de cette lampe de temps supplémentaire. Notre système accumule beaucoup de timestamp, nous avons mis à jour, et créé des dates pour aider à faciliter la synchronisation vers le bas, et maintenant nous avons ajouté une date de soumission pour faciliter la synchronisation. C’est tout simplement parce que, pendant tous les types de synchronisation, le problème le plus important est celui d’une incohérence chronologique – erreurs de synchronisation. Nous pouvons empêcher cela en maintenant une bonne tenue de livres, et puisque le sujet de notre préoccupation est le temps, alors cela signifie que nous gardons simplement beaucoup de timestamps comme comptabilité pour assurer le bon flux de données.

Qu’en est-il des suppressions?

Nous avons déjà défini les bases pour la gestion des suppressions. En utilisant notre timestamp date de soumission nouvellement ajoutée, nous pouvons maintenant facilement ignorer les mises à jour qui se produisent pour supprime les enregistrements … si le timing est correct. Vous voyez, en dépit d’avoir ajouté un nouvel amorti temps, nous avons passé sous silence encore un autre problème de timing. Les suppressions les amènent à l’avant et au centre, parce que les suppressions sont en fait des suppressions logiques (vous n’êtes pas difficile de supprimer des enregistrements, n’est-ce pas?), et en tant que tel impliquent une mise à jour d’une colonne.

En fait, le conflit d’une suppression se produisant avant/après une autre mise à jour est simplement un cas particulier de deux mises à jour se produisant, mais sur différents domaines du même modèle.

Indiqué d’une autre manière, si nous pouvons résoudre le scénario d’Alice mise à jour de la lecture quotidienne des fluides du réservoir, en même temps qu’une mise à jour hors ligne de Fred tente de mettre à jour la lecture de pression du même réservoir, alors nous pouvons également résoudre les suppressions. Examinons ces scénarios de plus près et voyons ce qui nous manque et pourquoi.

  1. Fred visite le site de stockage 4, et l’enregistrement d’une lecture de pression quotidienne pour le réservoir de stockage B – disons 60 PSI. Fred est hors ligne, cependant, de sorte que ses mises à jour ne seront pas encore transmises.
  2. Alice reçoit un appel l’informant de la réception de liquides supplémentaires pour le réservoir B au site 4. Elle est en ligne, et met immédiatement à jour la lecture fluide à 150 litres.
  3. Fred obtient enfin la conductivité d’Internet, et transmet ses lectures de pression de plus tôt dans la journée.

Une grande partie de cela se résume à la façon dont nous structurer nos données, nous allons rendre cela encore plus concret. En supposant que notre schéma ressemble à ceci:

Au moment où nous arrivons à l’étape 3, nous avions des dossiers qui ressemblent à ceci:

Si notre système, notant qu’il est plus tôt timestamp, tout simplement ignoré la soumission de Fred, alors nous venons de jeter des données que seul le dossier de Fred contient la lecture de pression. Qu’est-ce qu’il faut faire?

Valeur d’attribut d’entité au sauvetage

Heureusement pour nous, un modèle existe pour résoudre ce problème exact, et c’est ce qu’on appelle le modèle de valeur d’attribut d’entité. Le modèle EAV est en fait assez simple, et une fois intégré peut grandement simplifier les complexités autour de up-syncs.

À la base, EAV dit que les entités (de sorte que la représentation d’une lecture pour un réservoir fluide) qui contiennent de multiples attributs (le niveau de fluide et la lecture de pression de notre réservoir) devraient être représentées par des valeurs distinctes lorsqu’elles sont enregistrées. En bref, nous devrions réviser notre schéma à ce qu’un enregistrement dit « niveau de fluide est de 150 litres » et un autre dit « la pression est de 60 psi », mais ni dit les deux. Confus? Regardons comment le schéma ci-dessus pourrait ressembler sous EAV et aller à partir de là:

Dans ce schéma révisé, nous utilisons le champ « lecture » pour servir à double objectif. Parfois, une lecture est une pression, d’autres fois c’est un volume. Comment différencier les deux? C’est à cela que s’adresse le champ de type lecture. Dans cet exemple, le type 1 représente la lecture de pression, et le type 2 représente le niveau de liquide. Ces types sont entièrement spécifiques à l’application, alors ne vous accrochez pas à cela, mais plutôt observer comment dans ce schéma révisé, nous avons essentiellement pivoté notre dossier, et permettre la préservation de la mise à jour d’Alice et Fred sans aucun conflit ce que-si-jamais.

En fait, si nous utilisons ce schéma et boucle de retour dans notre exemple précédent de Fred également l’enregistrement du niveau de fluide, nous obtenons quelque chose de très intéressant:

 

Ici, nous avons non seulement utilisé notre nouveau schéma, mais nous permettons également que la lecture de pression antérieure de Fred soit stockée dans la base de données. Puisque nous utilisons à la fois le modèle EAV, et ont une bonne lampe de temps submission_date, lorsque nous interrogeons pour la lecture correcte / fluide actuelle, nous pouvons clairement voir que la lecture fluide de Fred s’est produite plus tôt et l’ignorer. Toutefois, nous l’avons quand même enregistré à des fins de vérification. Aussi, et peut-être plus important encore, c’est plus facile. Chaque fois que nous mettons à jour sélectivement les données, nous courons le risque d’erreurs. Toutefois, si nous écrivons tout dans ce qui est essentiellement un journal d’audit géant, nous pouvons (au moment de la requête) résoudre les conflits en temps réel – chaque enregistrement nouvellement enregistré résout rétroactivement tous les conflits antérieurs.

Ce dernier morceau vaut la peine d’être discuté un peu plus.

Alors que nos exemples jusqu’à présent ont été assez simpliste, mises à jour de synchronisation peut devenir assez compliqué. Lancez les dés assez, et vous pouvez avoir plusieurs mises à jour de la même personne, certains en ligne, certains hors ligne, d’autres mises à jour de différents utilisateurs, etc, tous se produisant en parallèle. Vous pouvez, sur chaque enregistrement nouvellement reçu, évaluer tous les enregistrements existants dans le système, et déterminer si vous devez insérer ou mettre à jour cette ligne, mais que se passe-t-il si, comme vous l’avez évalué si la mise à jour d’Alice doit être enregistrée, une autre mise à jour d’un autre utilisateur (par exemple Bob) arrive. Si vous exécutant plusieurs serveurs Web, comme presque n’importe quelle application Web moderne, alors la logique qui évalue si l’enregistrement d’Alice doit être enregistré est en cours d’exécution en même temps que Bob, et les deux de l’ignorance de l’autre – plus de conflit!

Au lieu de cela, le modèle ci-dessus permet à la fois alice et bob mise à jour d’être écrit à la base de données, et quand quelqu’un d’autre lit les données, seul l’enregistrement avec la plus récente timestamp (tel que déterminé par la date de soumission) est utilisé.

 

About Jason

Jason est un entrepreneur expérimenté et développeur de logiciels qualifié dans le leadership, le développement mobile, la synchronisation des données et l’architecture SaaS. Il a obtenu son baccalauréat ès sciences (B.S.) en informatique de l’Université d’État de l’Arkansas.
View all posts by Jason →

Laisser un commentaire

Votre adresse e-mail ne sera pas publiée. Les champs obligatoires sont indiqués avec *