Comment concevoir une application Web Ajax multi-utilisateurs pour être simultanément sûre


95

J'ai une page Web qui affiche une grande quantité de données du serveur. La communication se fait via ajax.

Chaque fois que l'utilisateur interagit et modifie ces données (par exemple, l'utilisateur A renomme quelque chose), il indique au serveur de faire l'action et le serveur renvoie les nouvelles données modifiées.

Si l'utilisateur B accède à la page en même temps et crée un nouvel objet de données, il en informera à nouveau le serveur via ajax et le serveur reviendra avec le nouvel objet pour l'utilisateur.

Sur la page de A, nous avons les données avec un objet renommé. Et sur la page de B, nous avons les données avec un nouvel objet. Sur le serveur, les données ont à la fois un objet renommé et un nouvel objet.

Quelles sont mes options pour garder la page synchronisée avec le serveur lorsque plusieurs utilisateurs l'utilisent simultanément?

Des options telles que le verrouillage de la page entière ou le vidage de l'état entier à l'utilisateur à chaque modification sont plutôt évitées.

Si cela peut vous aider, dans cet exemple spécifique, la page Web appelle une méthode Web statique qui exécute une procédure stockée sur la base de données. La procédure stockée renverra toutes les données modifiées et pas plus. La méthode Web statique transmet ensuite le retour de la procédure stockée au client.

Modification de la prime:

Comment concevez-vous une application Web multi-utilisateurs qui utilise Ajax pour communiquer avec le serveur mais évite les problèmes de concurrence?

C'est-à-dire un accès simultané aux fonctionnalités et aux données d'une base de données sans aucun risque de corruption de données ou d'état


pas si sûr, mais vous pouvez avoir une page comme Facebook où le navigateur envoie une requête ajax cherchant constamment des modifications dans la base de données du serveur et les mettant à jour sur le navigateur
Santosh Linkha

Sérialiser l'état du client, puis dire au serveur via ajax, voici mon état que dois-je mettre à jour est une option. Mais oblige le client à savoir comment mettre à jour tout et chaque bit d'information en un seul endroit.
Raynos

1
La meilleure solution à la concurrence d'accès utilisateur n'est-elle pas simplement l'une des variantes push? Websockets, comète, etc.
davin

@davin pourrait très bien l'être. Mais je ne suis pas familier avec les comètes et les Websockets ne sont pas là pour le support multi-navigateurs.
Raynos

2
il existe de bons packages pour le support multi-navigateurs, en particulier je recommande socket.io, bien qu'il y ait aussi jWebSocket, et bien d'autres. Si vous optez pour socket.io, vous pouvez incorporer toutes sortes de goodies node.js, comme des frameworks et des moteurs de modèles (côté client), etc.
davin

Réponses:


157

Aperçu:

  • Intro
  • Architecture serveur
  • Architecture client
  • Mettre à jour le cas
  • Valider le cas
  • Cas de conflit
  • Performance et évolutivité

Salut Raynos,

Je ne parlerai d'aucun produit en particulier ici. Ce que d'autres ont mentionné est un bon ensemble d'outils à examiner déjà (peut-être ajouter node.js à cette liste).

D'un point de vue architectural, vous semblez avoir le même problème que celui des logiciels de contrôle de version. Un utilisateur enregistre une modification sur un objet, un autre utilisateur souhaite modifier le même objet d'une autre manière => conflit. Vous devez intégrer les modifications apportées aux objets par les utilisateurs tout en étant en mesure de fournir des mises à jour en temps opportun et efficacement, en détectant et en résolvant les conflits comme celui ci-dessus.

Si j'étais à votre place, je développerais quelque chose comme ceci:

1. Côté serveur:

  • Déterminez un niveau raisonnable auquel vous définiriez ce que j'appellerais des "artefacts atomiques" (la page? Les objets sur la page? Les valeurs à l'intérieur des objets?). Cela dépendra de vos serveurs Web, de votre base de données et de votre matériel de mise en cache, du nombre d'utilisateurs, du nombre d'objets, etc. Ce n'est pas une décision facile à prendre.

  • Pour chaque artefact atomique ont:

    • un identifiant unique à l'échelle de l'application
    • un identifiant de version incrémenté
    • un mécanisme de verrouillage pour l'accès en écriture (mutex peut-être)
    • un petit historique ou "changelog" dans un ringbuffer (la mémoire partagée fonctionne bien pour ceux-là). Une seule paire clé-valeur peut également convenir, bien que moins extensible. voir http://en.wikipedia.org/wiki/Circular_buffer
  • Un serveur ou un composant pseudo-serveur capable de fournir efficacement des journaux de modifications pertinents à un utilisateur connecté. Observer-Pattern est votre ami pour cela.

2. Côté client:

  • Un client javascript qui est capable d'avoir une connexion HTTP de longue durée avec ledit serveur ci-dessus, ou utilise une interrogation légère.

  • Un composant de mise à jour d'artefacts javascript qui actualise le contenu des sites lorsque le client javascript connecté notifie des modifications dans l'historique des artefacts surveillés. (encore une fois, un modèle d'observateur pourrait être un bon choix)

  • Un composant javascript de validation d'artefact qui peut demander la modification d'un artefact atomique, essayant d'acquérir un verrou mutex. Il détectera si l'état de l'artefact avait été modifié par un autre utilisateur quelques secondes auparavant (latence du client javascript et des facteurs de processus de validation) en comparant l'ID de version de l'artefact du côté client et l'ID de la version de l'artefact du côté du serveur actuel.

  • Un solveur de conflit javascript permettant une décision humaine qui-change-est-la-bonne. Vous ne voudrez peut-être pas simplement dire à l'utilisateur "Quelqu'un était plus rapide que vous. J'ai supprimé votre modification. Allez pleurer.". De nombreuses options de différences plutôt techniques ou de solutions plus conviviales semblent possibles.

Alors, comment ça roulerait ...

Cas 1: diagramme de type de séquence pour la mise à jour:

  • Le navigateur rend la page
  • javascript "voit" les artefacts qui ont chacun au moins un champ de valeur, unique et un identifiant de version
  • le client javascript démarre, demandant de "regarder" l'historique des artefacts trouvés à partir de leurs versions trouvées (les modifications plus anciennes ne sont pas intéressantes)
  • Le processus serveur prend note de la demande et vérifie et / ou envoie l'historique en continu
  • Les entrées d'historique peuvent contenir de simples notifications "l'artefact x a changé, le client demande des données" permettant au client d'interroger indépendamment ou des ensembles de données complets "l'artefact x a changé en valeur foo"
  • javascript artifact-updater fait ce qu'il peut pour récupérer de nouvelles valeurs dès qu'elles sont connues pour avoir été mises à jour. Il exécute de nouvelles requêtes ajax ou est alimenté par le client javascript.
  • Le contenu des pages DOM est mis à jour, l'utilisateur est éventuellement averti. L'observation de l'histoire se poursuit.

Cas 2: Maintenant pour l'engagement:

  • artefact-committer connaît la nouvelle valeur souhaitée à partir de l'entrée utilisateur et envoie une demande de changement au serveur
  • le mutex du serveur est acquis
  • Le serveur reçoit "Hé, je connais l'état de l'artefact x de la version 123, laissez-moi le définir sur la valeur foo pls."
  • Si la version côté serveur de l'artefact x est égale (ne peut pas être inférieure) à 123, la nouvelle valeur est acceptée, un nouvel identifiant de version de 124 est généré.
  • Les nouvelles informations d'état "mises à jour vers la version 124" et éventuellement la nouvelle valeur foo sont placées au début du tampon circulaire de l'artefact x (changelog / history)
  • serveur mutex est libéré
  • demandeur d'artefact committer est heureux de recevoir une confirmation de validation avec le nouvel identifiant.
  • pendant ce temps, le composant serveur côté serveur continue d'interroger / pousser les tampons annulaires vers les clients connectés. Tous les clients qui surveillent le tampon de l'artefact x obtiendront les nouvelles informations d'état et la nouvelle valeur dans leur latence habituelle (voir le cas 1.)

Cas 3: pour les conflits:

  • Le validateur d'artefact connaît la nouvelle valeur souhaitée à partir de l'entrée utilisateur et envoie une demande de changement au serveur
  • dans l'intervalle, un autre utilisateur a mis à jour le même artefact avec succès (voir le cas 2) mais en raison de diverses latences, cela est encore inconnu de notre autre utilisateur.
  • Ainsi, un mutex côté serveur est acquis (ou attendu jusqu'à ce que l'utilisateur "le plus rapide" valide sa modification)
  • Le serveur reçoit "Hey, je connais l'état de l'artefact x de la version 123, laissez-moi le définir sur la valeur foo."
  • Sur le serveur, la version de l'artefact x est déjà 124. Le client demandeur ne peut pas connaître la valeur qu'il remplacerait.
  • De toute évidence, le serveur doit rejeter la demande de changement (sans compter les priorités d'écrasement interventionnistes), libère le mutex et a la gentillesse de renvoyer le nouvel identifiant de version et la nouvelle valeur directement au client.
  • confronté à une demande de validation rejetée et à une valeur que l'utilisateur demandeur de changement ne connaissait pas encore, le validateur d'artefact javascript fait référence au résolveur de conflit qui affiche et explique le problème à l'utilisateur.
  • L'utilisateur, qui se voit présenter certaines options par le résolveur de conflits intelligent JS, est autorisé à une autre tentative pour modifier la valeur.
  • Une fois que l'utilisateur a sélectionné une valeur qu'il juge correcte, le processus recommence à partir du cas 2 (ou du cas 3 si quelqu'un d'autre était plus rapide, encore une fois)

Quelques mots sur les performances et l'évolutivité

Interrogation HTTP vs "push" HTTP

  • L'interrogation crée des requêtes, une par seconde, 5 par seconde, quel que soit votre avis comme une latence acceptable. Cela peut être assez cruel pour votre infrastructure si vous ne configurez pas suffisamment bien votre (Apache?) Et (php?) Pour être des démarreurs "légers". Il est souhaitable d'optimiser la demande d'interrogation côté serveur afin qu'elle s'exécute bien moins longtemps que la longueur de l'intervalle d'interrogation. Diviser ce temps d'exécution en deux pourrait bien signifier réduire la charge de l'ensemble de votre système jusqu'à 50%,
  • Pousser via HTTP (en supposant que les webworkers sont trop éloignés pour les prendre en charge) vous obligera à avoir un processus apache / lighthttpd disponible pour chaque utilisateur à tout moment . La mémoire résidente réservée à chacun de ces processus et la mémoire totale de votre système sera une limite de mise à l'échelle très certaine que vous rencontrerez. Il sera nécessaire de réduire l'empreinte mémoire de la connexion, ainsi que de limiter la quantité de travail continu du processeur et des E / S effectué dans chacun d'entre eux (vous voulez beaucoup de temps de veille / d'inactivité)

mise à l'échelle du backend

  • Oubliez la base de données et le système de fichiers, vous aurez besoin d' une sorte de backend basé sur la mémoire partagée pour l'interrogation fréquente (si le client n'interroge pas directement, chaque processus serveur en cours d'exécution le fera)
  • si vous optez pour Memcache, vous pouvez mieux évoluer, mais c'est toujours cher
  • Le mutex pour les commits doit fonctionner globalement, même si vous souhaitez avoir plusieurs serveurs frontaux pour équilibrer la charge.

mise à l'échelle du frontend

  • peu importe si vous interrogez ou recevez des «push», essayez d'obtenir des informations sur tous les artefacts surveillés en une seule étape.

ajustements "créatifs"

  • Si les clients interrogent et que de nombreux utilisateurs ont tendance à regarder les mêmes artefacts, vous pouvez essayer de publier l'historique de ces artefacts sous forme de fichier statique, permettant à Apache de le mettre en cache, tout en l'actualisant côté serveur lorsque les artefacts changent. Cela prend PHP / memcache hors du jeu pour les demandes. Lighthttpd est très efficace pour servir des fichiers statiques.
  • utilisez un réseau de diffusion de contenu comme cotendo.com pour y envoyer l'historique des artefacts. La latence push sera plus importante mais l'évolutivité est un rêve
  • écrire un vrai serveur (n'utilisant pas HTTP) auquel les utilisateurs se connectent en utilisant java ou flash (?). Vous devez gérer de nombreux utilisateurs dans un seul thread de serveur. Parcourir les prises ouvertes, faire (ou déléguer) le travail requis. Peut évoluer via des processus de fourche ou en démarrant plus de serveurs. Les mutex doivent cependant rester uniques au monde.
  • En fonction des scénarios de charge, regroupez vos serveurs frontaux et principaux par plages d'ID d'artefact. Cela permettra une meilleure utilisation de la mémoire persistante (aucune base de données ne possède toutes les données) et permet de mettre à l'échelle le mutexing. Votre javascript doit cependant maintenir des connexions à plusieurs serveurs en même temps.

Eh bien, j'espère que cela peut être un début pour vos propres idées. Je suis sûr qu'il y a beaucoup plus de possibilités. Je suis plus que favorable à toute critique ou amélioration de cet article, le wiki est activé.

Christoph Strasen


1
@ChristophStrasen Regardez les serveurs événementiels comme node.js qui ne reposent pas sur un thread par utilisateur. Cela permet de gérer la technique de poussée avec une consommation de mémoire inférieure. Je pense qu'un serveur node.js et s'appuyer sur TCP WebSockets aide vraiment à la mise à l'échelle. Cependant, cela ruine complètement la conformité entre les navigateurs.
Raynos

Je suis totalement d'accord et j'espère que ma rédaction n'encourage pas à réinventer la roue! Bien que certaines roues soient un peu nouvelles, elles commencent tout juste à être connues et ne sont pas assez bien expliquées pour que les architectes de logiciels de niveau intermédiaire puissent juger de son application pour une idée spécifique. A MON HUMBLE AVIS. Node.js mérite un peu un livre "pour les nuls";). J'achèterais certainement.
Christoph Strasen

2
+500 Vous en avez un de défi. C'est une excellente réponse.
Raynos

1
@luqmaan cette réponse date de février 2011. Les Websockets étaient encore une nouveauté, et ne sont sortis sans préfixe dans Chrome qu'en août. Comet et socket.io allaient bien, je pense que c'était simplement une suggestion pour une approche plus performante.
Ricardo Tomasi

1
Et si Node.js est un peu trop loin de votre zone de confort (ou de la zone de confort de l'équipe des opérations, mais sûr du contexte commercial de la question), vous pouvez également utiliser Socket.io avec un serveur Java. Tomcat et Jetty prennent en charge les connexions sans thread pour les configurations de type serveur push (voir par exemple: wiki.eclipse.org/Jetty/Feature/Continuations )
Tomas

13

Je sais que c'est une vieille question, mais je pensais que j'interviendrais.

OT (transformations opérationnelles) semble être une bonne solution pour vos besoins d'édition multi-utilisateurs simultanés et cohérents. C'est une technique utilisée dans Google Docs (et a également été utilisée dans Google Wave):

Il existe une bibliothèque basée sur JS pour l'utilisation des transformations opérationnelles - ShareJS ( http://sharejs.org/ ), écrite par un membre de l'équipe Google Wave.

Et si vous le souhaitez, il existe un framework Web MVC complet - DerbyJS ( http://derbyjs.com/ ) basé sur ShareJS qui fait tout pour vous.

Il utilise BrowserChannel pour la communication entre le serveur et les clients (et je pense que la prise en charge de WebSockets devrait être en cours - il y était auparavant via Socket.IO, mais a été retiré en raison des problèmes du développeur avec Socket.io) Les documents pour débutants sont un peu clairsemé pour le moment, cependant.


5

J'envisagerais d'ajouter un tampon modifié basé sur le temps pour chaque ensemble de données. Ainsi, si vous mettez à jour des tables de base de données, vous modifierez l'horodatage modifié en conséquence. À l'aide d'AJAX, vous pouvez comparer l'horodatage modifié du client avec l'horodatage de la source de données - si l'utilisateur est déjà en retard, mettez à jour l'affichage. Similaire à la façon dont ce site vérifie périodiquement une question pour voir si quelqu'un d'autre a répondu pendant que vous tapez une réponse.


C'est un point utile. Cela m'aide également à mieux comprendre les champs "LastEdited" de notre base de données à partir d'un point de conception.
Raynos

Exactement. Ce site utilise un "battement de cœur", c'est-à-dire chaque fois qu'il envoie une requête AJAX au serveur, et il transmet l'ID des données à vérifier, ainsi que l'horodatage modifié dont il dispose pour ces données. Disons que nous sommes sur la question n ° 1029. À chaque requête AJAX, le serveur ne regarde que l'horodatage modifié pour la question # 1029. S'il découvre que le client possède une version plus ancienne des données, il répond au battement de cœur avec une nouvelle copie. Le client peut alors soit recharger la page (actualiser), soit afficher une sorte de message à l'utilisateur pour l'avertir des nouvelles données.
Chris Baker

les tampons modifiés sont beaucoup plus agréables que de hacher nos "données" actuelles et de les comparer avec un hachage de l'autre côté.
Raynos

1
Gardez à l'esprit que le client et le serveur doivent avoir accès à la même heure exacte pour éviter les incohérences.
Prierslayer

3

Vous devez utiliser des techniques push (également connues sous le nom de Comet ou Ajax inverse) pour propager les modifications à l'utilisateur dès qu'elles sont apportées à la base de données. La meilleure technique actuellement disponible pour cela semble être l'interrogation longue Ajax, mais elle n'est pas prise en charge par tous les navigateurs, vous avez donc besoin de solutions de secours. Heureusement, il existe déjà des solutions qui gèrent cela pour vous. Parmi eux, on trouve: orbited.org et le socket.io déjà mentionné.

À l'avenir, il y aura un moyen plus simple de le faire, appelé WebSockets, mais on ne sait pas encore quand ce standard sera prêt pour les heures de grande écoute car il y a des problèmes de sécurité concernant l'état actuel de la norme.

Il ne devrait pas y avoir de problèmes de concurrence dans la base de données avec de nouveaux objets. Mais lorsqu'un utilisateur modifie un objet, le serveur doit avoir une logique qui vérifie si l'objet a été modifié ou supprimé entre-temps. Si l'objet a été supprimé, la solution est, encore une fois, simple: il suffit de supprimer la modification.

Mais le problème le plus difficile apparaît lorsque plusieurs utilisateurs éditent le même objet en même temps. Si les utilisateurs 1 et 2 commencent à modifier un objet en même temps, ils effectueront tous les deux leurs modifications sur les mêmes données. Disons que les modifications apportées par l'utilisateur 1 sont d'abord envoyées au serveur tandis que l'utilisateur 2 est toujours en train de modifier les données. Vous avez alors deux options: vous pouvez essayer de fusionner les modifications de l'utilisateur 1 dans les données de l'utilisateur 2 ou vous pouvez dire à l'utilisateur 2 que ses données sont obsolètes et lui afficher un message d'erreur dès que ses données sont envoyées au serveur. La dernière option n'est pas très conviviale ici, mais la première est très difficile à mettre en œuvre.

L'une des rares implémentations à avoir vraiment réussi pour la première fois était EtherPad , qui a été acquis par Google. Je pense qu'ils ont ensuite utilisé certaines des technologies d'EtherPad dans Google Docs et Google Wave, mais je ne peux pas le dire avec certitude. Google ouvre également EtherPad, donc cela vaut peut-être le coup d'œil, en fonction de ce que vous essayez de faire.

Ce n'est vraiment pas facile de faire cela simultanément en éditant des trucs, car il n'est pas possible de faire des opérations atomiques sur le Web à cause de la latence. Peut - être que cet article vous aidera à en savoir plus sur le sujet.


2

Essayer d'écrire tout cela vous-même est un gros travail, et il est très difficile de bien faire les choses. Une option consiste à utiliser un framework conçu pour garder les clients synchronisés avec la base de données et entre eux en temps réel.

J'ai trouvé que le framework Meteor le faisait bien ( http://docs.meteor.com/#reactivity ).

"Meteor embrasse le concept de programmation réactive. Cela signifie que vous pouvez écrire votre code dans un style impératif simple, et le résultat sera automatiquement recalculé chaque fois que les données changent dont dépend votre code."

"Ce modèle simple (calcul réactif + source de données réactive) a une large applicabilité. Le programmeur est sauvé de l'écriture des appels de désabonnement / réabonnement et de s'assurer qu'ils sont appelés au bon moment, éliminant ainsi des classes entières de code de propagation de données qui autrement obstrueraient votre application avec une logique sujette aux erreurs. "


1

Je ne peux pas croire que personne n'ait mentionné Meteor . C'est un cadre nouveau et immature bien sûr (et ne prend officiellement en charge qu'une seule base de données), mais cela prend tout le travail difficile et la réflexion sur une application multi-utilisateurs comme le décrit l'affiche. En fait, vous ne pouvez PAS créer une application de mise à jour en direct multi-utilisateurs. Voici un bref résumé:

  • Tout est dans node.js (JavaScript ou CoffeeScript), vous pouvez donc partager des éléments comme des validations entre le client et le serveur.
  • Il utilise des websockets, mais peut se replier sur les anciens navigateurs
  • Il se concentre sur les mises à jour immédiates de l'objet local (c'est-à-dire que l'interface utilisateur semble accrocheuse), avec les modifications envoyées au serveur en arrière-plan. Seules les mises à jour atomiques sont autorisées pour simplifier les mises à jour de mixage. Les mises à jour rejetées sur le serveur sont annulées.
  • En prime, il gère les recharges de code en direct pour vous et préserve l'état de l'utilisateur même lorsque l'application change radicalement.

Meteor est assez simple pour que je vous suggère au moins d'y jeter un œil pour des idées à voler.


1
J'aime vraiment l'idée de Derby et Meteor pour certains types d'applications. La propriété et les autorisations des documents / enregistrements ne sont que quelques problèmes du monde réel qui ne sont pas bien résolus à mon humble avis. De plus, venant du monde de longue date de la SEP qui rend ces 80% vraiment faciles et passe trop de temps sur les 20% restants, j'hésite à utiliser de telles solutions PFM (pure f ** king magic).
Tracker1

1

Ces pages Wikipédia peuvent aider à ajouter une perspective à l'apprentissage de la concurrence et de l'informatique simultanée pour la conception d'une application Web ajax qui extrait ou est poussé des messages d' événement d' état ( EDA ) dans un modèle de messagerie . Fondamentalement, les messages sont répliqués vers les abonnés du canal qui répondent aux événements de modification et aux demandes de synchronisation.

Il existe de nombreuses formes de logiciels collaboratifs Web simultanés .

Il existe un certain nombre de bibliothèques clientes d'API HTTP pour etherpad-lite , un éditeur collaboratif en temps réel .

django-realtime-terrain de jeu implémente une application de chat en temps réel dans Django avec diverses technologies en temps réel comme Socket.io .

AppEngine et AppScale implémentent l' API AppEngine Channel ; qui est distincte de l' API Google Realtime , illustrée par googledrive / realtime-terrain de jeu .


0

Les techniques de push côté serveur sont la voie à suivre ici. Comet est (ou était?) Un mot à la mode.

La direction particulière que vous prenez dépend fortement de votre pile de serveurs et de votre flexibilité. Si vous le pouvez, je jetterais un coup d'œil à socket.io , qui fournit une implémentation multi-navigateur de websockets, qui fournit un moyen très simple d'avoir une communication bidirectionnelle avec le serveur, permettant au serveur de pousser les mises à jour vers les clients.

En particulier, regardez cette démonstration de l'auteur de la bibliothèque, qui montre presque exactement la situation que vous décrivez.


C'est une excellente bibliothèque pour réduire les problèmes de comminiation, mais je cherchais davantage des informations de haut niveau sur la façon de concevoir une application
Raynos

1
Juste à noter, que socket.io (et SignalR) sont des frameworks qui utilisent des websockets comme choix de première classe, mais ont des solutions de secours compatibles pour utiliser d'autres techniques telles que la comète, l'interrogation longue, les sockets flash et les foreverframes.
Tracker1
En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.