Come progettare un'applicazione web ajax multiutente per essere contemporaneamente sicura


95

Ho una pagina web che mostra una grande quantità di dati dal server. La comunicazione avviene tramite ajax.

Ogni volta che l'utente interagisce e modifica questi dati (ad esempio l'utente A rinomina qualcosa), dice al server di eseguire l'azione e il server restituisce i nuovi dati modificati.

Se l'utente B accede alla pagina allo stesso tempo e crea un nuovo oggetto dati, lo dirà di nuovo al server tramite ajax e il server tornerà con il nuovo oggetto per l'utente.

Nella pagina di A abbiamo i dati con un oggetto rinominato. E sulla pagina di B abbiamo i dati con un nuovo oggetto. Sul server i dati hanno sia un oggetto rinominato che un nuovo oggetto.

Quali sono le mie opzioni per mantenere la pagina sincronizzata con il server quando più utenti la utilizzano contemporaneamente?

Opzioni come il blocco dell'intera pagina o il dumping dell'intero stato all'utente a ogni modifica sono piuttosto evitate.

Se aiuta, in questo esempio specifico la pagina web chiama un metodo web statico che esegue una procedura memorizzata sul database. La procedura memorizzata restituirà tutti i dati che ha modificato e non di più. Il metodo web statico inoltra quindi la restituzione della stored procedure al client.

Modifica taglia:

Come si progetta un'applicazione web multiutente che utilizza Ajax per comunicare con il server ma evita problemi di concorrenza?

Ovvero accesso simultaneo a funzionalità e dati su un database senza alcun rischio di danneggiamento dei dati o dello stato


non così sicuro ma puoi avere una pagina come Facebook in cui il browser invia una richiesta ajax costantemente alla ricerca di modifiche nel database del server e aggiornandole sul browser
Santosh Linkha

Serializzare lo stato del client e quindi dire al server tramite Ajax ecco il mio stato, cosa devo aggiornare è un'opzione. Ma richiede al cliente di sapere come aggiornare qualsiasi bit di informazione in un unico posto.
Raynos

1
La migliore soluzione per la concorrenza tra utenti non è semplicemente una delle varianti push? Websocket, cometa, ecc.
davin

@davin potrebbe benissimo essere. Ma non ho familiarità con comet e websocket non sono lì per il supporto cross-browser.
Raynos

2
ci sono buoni pacchetti per il supporto cross-browser, in particolare io consiglio socket.io, sebbene ci sia anche jWebSocket e molti altri. Se si utilizza socket.io, è possibile incorporare tutti i tipi di chicche di node.js, come framework e motori di
modelli

Risposte:


157

Panoramica:

  • Intro
  • Architettura del server
  • Architettura client
  • Aggiorna caso
  • Impegnare il caso
  • Caso di conflitto
  • Prestazioni e scalabilità

Ciao Raynos,

Non discuterò qui alcun prodotto particolare. Quello che altri hanno menzionato è un buon set di strumenti a cui dare già un'occhiata (magari aggiungi node.js a quell'elenco).

Da un punto di vista architettonico, sembra che tu abbia lo stesso problema che può essere visto nel software di controllo della versione. Un utente registra una modifica a un oggetto, un altro utente desidera modificare lo stesso oggetto in un altro modo => conflitto. È necessario integrare le modifiche degli utenti agli oggetti e allo stesso tempo essere in grado di fornire aggiornamenti tempestivi ed efficienti, rilevando e risolvendo conflitti come quello sopra.

Se fossi nei tuoi panni svilupperei qualcosa del genere:

1. Lato server:

  • Determina un livello ragionevole al quale definire ciò che chiamerei "artefatti atomici" (la pagina? Oggetti nella pagina? Valori all'interno degli oggetti?). Questo dipenderà dai tuoi server web, database e hardware di memorizzazione nella cache, numero di utenti, numero di oggetti, ecc. Non è una decisione facile da prendere.

  • Per ogni artefatto atomico avere:

    • un ID univoco a livello di applicazione
    • un ID versione incrementale
    • un meccanismo di blocco per l'accesso in scrittura (forse mutex)
    • una piccola storia o "changelog" all'interno di un ringbuffer (la memoria condivisa funziona bene per quelli). Anche una singola coppia chiave-valore potrebbe andare bene sebbene meno estendibile. vedi http://en.wikipedia.org/wiki/Circular_buffer
  • Un server o un componente pseudo-server in grado di fornire in modo efficiente log delle modifiche pertinenti a un utente connesso. Observer-Pattern è tuo amico per questo.

2. lato client:

  • Un client javascript che è in grado di avere una connessione HTTP di lunga durata a detto server sopra, o utilizza polling leggero.

  • Un componente javascript artifact-updater che aggiorna il contenuto dei siti quando il client javascript connesso notifica le modifiche nella cronologia degli artefatti guardati. (ancora una volta uno schema di osservazione potrebbe essere una buona scelta)

  • Un componente javascript artefatto-committer che potrebbe richiedere di modificare un artefatto atomico, cercando di acquisire il blocco mutex. Rileverà se lo stato dell'artefatto è stato modificato da un altro utente pochi secondi prima (latenza del client JavaScript e fattori di processo di commit in) confrontando l'ID versione artefatto lato client noto e l'ID versione artefatto lato server corrente.

  • Un risolutore di conflitti javascript che consente una decisione umana che il cambiamento è la giusta. Potresti non voler dire semplicemente all'utente "Qualcuno è stato più veloce di te. Ho cancellato la tua modifica. Vai a piangere.". Sembrano possibili molte opzioni da differenze piuttosto tecniche o soluzioni più user-friendly.

Quindi come andrebbe ...

Caso 1: tipo di diagramma di sequenza per l'aggiornamento:

  • Il browser esegue il rendering della pagina
  • javascript "vede" artefatti ognuno dei quali ha almeno un campo valore, univoco e un id di versione
  • il client javascript viene avviato, richiedendo di "guardare" la cronologia degli artefatti trovati a partire dalle versioni trovate (le modifiche precedenti non sono interessanti)
  • Il processo del server annota la richiesta e controlla e / o invia continuamente la cronologia
  • Le voci della cronologia possono contenere semplici notifiche "l'artefatto x è cambiato, il cliente richiede dati" che consente al client di eseguire il polling in modo indipendente o set di dati completi "l'artefatto x è cambiato nel valore pippo"
  • javascript artifact-updater fa il possibile per recuperare nuovi valori non appena si sa che sono stati aggiornati. Esegue nuove richieste ajax o viene alimentato dal client javascript.
  • Le pagine del contenuto DOM vengono aggiornate, l'utente viene facoltativamente informato. L'osservazione della storia continua.

Caso 2: ora per il commit:

  • artefatto-committer conosce il nuovo valore desiderato dall'input dell'utente e invia una richiesta di modifica al server
  • viene acquisito il mutex lato server
  • Il server riceve "Ehi, conosco lo stato di artifact x dalla versione 123, fammi impostare il valore foo pls."
  • Se la versione Serverside di artifact x è uguale (non può essere inferiore) a 123 il nuovo valore viene accettato, viene generato un nuovo id versione di 124.
  • Le nuove informazioni di stato "aggiornate alla versione 124" e opzionalmente il nuovo valore foo vengono messi all'inizio del ringbuffer del manufatto x (changelog / history)
  • viene rilasciato il server mutex
  • Il committer artefatto richiedente è felice di ricevere una conferma di commit insieme al nuovo ID.
  • nel frattempo, il componente server lato server continua a eseguire il polling / push dei ringbuffer ai client connessi. Tutti i client che guardano il buffer di artifact x riceveranno le nuove informazioni sullo stato e il valore entro la loro latenza abituale (vedi caso 1.)

Caso 3: per i conflitti:

  • Il committer artefatto conosce il nuovo valore desiderato dall'input dell'utente e invia una richiesta di modifica al server
  • nel frattempo un altro utente ha aggiornato con successo lo stesso artefatto (vedi caso 2.) ma a causa di varie latenze questo è ancora sconosciuto al nostro altro utente.
  • Quindi viene acquisito un mutex lato server (o atteso fino a quando l'utente "più veloce" non ha commesso la sua modifica)
  • Il server riceve "Ehi, conosco lo stato di artifact x dalla versione 123, fammi impostare il valore foo."
  • Sul lato server la versione di artifact x ora è già 124. Il cliente richiedente non può conoscere il valore che sovrascriverebbe.
  • Ovviamente il Server deve rifiutare la richiesta di modifica (senza contare le priorità di sovrascrittura intervenute da Dio), rilascia il mutex ed è così gentile da inviare indietro il nuovo ID di versione e il nuovo valore direttamente al client.
  • di fronte a una richiesta di commit rifiutata e un valore che l'utente che richiede la modifica non conosceva ancora, il committer di artefatti javascript si riferisce al risolutore di conflitti che mostra e spiega il problema all'utente.
  • All'utente, che viene presentato con alcune opzioni dal JS intelligente di risoluzione dei conflitti, è consentito un altro tentativo di modificare il valore.
  • Una volta che l'utente ha selezionato un valore che ritiene giusto, il processo ricomincia dal caso 2 (o dal caso 3 se qualcun altro è stato più veloce, di nuovo)

Qualche parola su prestazioni e scalabilità

Polling HTTP e "push" HTTP

  • Il polling crea richieste, una al secondo, 5 al secondo, qualunque cosa tu consideri una latenza accettabile. Questo può essere piuttosto crudele per la tua infrastruttura se non configuri (Apache?) E (php?) Abbastanza bene da essere dei principianti "leggeri". È preferibile ottimizzare la richiesta di polling sul lato server in modo che venga eseguita per un tempo molto inferiore rispetto alla lunghezza dell'intervallo di polling. Dividere il tempo di esecuzione a metà potrebbe significare ridurre il carico dell'intero sistema fino al 50%,
  • Spingendo via HTTP (webworkers assumendo sono troppo lontano per sostenerli) si richiede all'utente di avere uno apache / LightHTTPD processo disponibili per ogni utente per tutto il tempo . La memoria residente riservata a ciascuno di questi processi e la memoria totale del sistema rappresenterà un limite di scala molto certo che incontrerai. Sarà necessario ridurre il footprint di memoria della connessione, oltre a limitare la quantità di lavoro continuo di CPU e I / O svolto in ciascuno di questi (si desidera molto tempo di sospensione / inattività)

ridimensionamento del backend

  • Dimentica database e filesystem, avrai bisogno di una sorta di backend basato sulla memoria condivisa per il polling frequente (se il client non esegue il polling direttamente, lo farà ogni processo del server in esecuzione)
  • se scegli memcache puoi ridimensionare meglio, ma è comunque costoso
  • Il mutex per i commit deve funzionare a livello globale anche se si desidera avere più server frontend per bilanciare il carico.

ridimensionamento del frontend

  • indipendentemente dal fatto che si stia effettuando un polling o ricevendo "push", cercare di ottenere informazioni per tutti gli artefatti osservati in un unico passaggio.

modifiche "creative"

  • Se i client eseguono il polling e molti utenti tendono a guardare gli stessi artefatti, si potrebbe provare a pubblicare la cronologia di tali artefatti come file statico, consentendo ad apache di memorizzarlo nella cache, aggiornandolo comunque sul lato server quando gli artefatti cambiano. Questo toglie PHP / memcache dal gioco per alcune richieste. Lighthttpd è molto efficiente nel servire file statici.
  • utilizzare una rete di distribuzione di contenuti come cotendo.com per inserire la cronologia degli artefatti lì. La latenza push sarà maggiore ma la scalabilità è un sogno
  • scrivere un server reale (non utilizzando HTTP) a cui gli utenti si connettono utilizzando java o flash (?). Devi occuparti di servire molti utenti in un thread del server. Andando in bicicletta attraverso prese aperte, facendo (o delegando) il lavoro richiesto. Può scalare tramite processi di fork o avviando più server. Tuttavia, i mutex devono rimanere unici al mondo.
  • A seconda degli scenari di carico, raggruppa i tuoi server frontend e backend per intervalli di ID artefatto. Ciò consentirà un migliore utilizzo della memoria persistente (nessun database ha tutti i dati) e renderà possibile scalare il mutexing. Il tuo javascript deve mantenere le connessioni a più server contemporaneamente.

Beh, spero che questo possa essere un inizio per le tue idee. Sono sicuro che ci sono molte più possibilità. Accolgo con favore qualsiasi critica o miglioramento a questo post, wiki è abilitato.

Christoph Strasen


1
@ChristophStrasen Guarda i server con eventi come node.js che non si basano su un thread per utente. Ciò consente di gestire la tecnica di spinta con un minor consumo di memoria. Penso che un server node.js e fare affidamento su TCP WebSocket aiuti davvero con il ridimensionamento. Tuttavia, rovina completamente la conformità del browser.
Raynos

Sono totalmente d'accordo e spero che il mio articolo non incoraggi a reinventare la ruota! Sebbene alcune ruote siano un po 'nuove, stanno appena iniziando a diventare note e non sono sufficientemente spiegate in modo che gli architetti di software di livello intermedio possano giudicare la sua applicazione per un'idea specifica. A PARER MIO. Node.js merita un libro "for dummies";). Lo comprerei sicuramente.
Christoph Strasen

2
+500 Ne hai uno con aria di sfida questo. È un'ottima risposta.
Raynos

1
@luqmaan questa risposta è del febbraio 2011. I websocket erano ancora una novità e sono stati rilasciati senza prefisso in Chrome solo intorno ad agosto. Comet e socket.io andavano bene, penso che fosse solo un suggerimento per un approccio più performante.
Ricardo Tomasi

1
E se Node.js è un po 'troppo fuori dalla tua zona di comfort (o dalla zona di comfort del team operativo, ma sicuro del contesto aziendale della domanda) potresti anche utilizzare Socket.io con un server basato su Java. Sia Tomcat che Jetty supportano connessioni senza thread per configurazioni server push (vedi ad esempio: wiki.eclipse.org/Jetty/Feature/Continuations )
Tomas

13

So che questa è una vecchia domanda, ma ho pensato di intervenire.

OT (trasformazioni operative) sembra una buona soluzione per le tue esigenze di editing multiutente simultaneo e coerente. È una tecnica utilizzata in Google Docs (ed è stata utilizzata anche in Google Wave):

Esiste una libreria basata su JS per l'utilizzo di trasformazioni operative - ShareJS ( http://sharejs.org/ ), scritta da un membro del team di Google Wave.

E se vuoi, c'è un framework web MVC completo - DerbyJS ( http://derbyjs.com/ ) costruito su ShareJS che fa tutto per te.

Utilizza BrowserChannel per la comunicazione tra il server e i client (e credo che il supporto di WebSockets dovrebbe essere in lavorazione - era lì in precedenza tramite Socket.IO, ma è stato rimosso a causa dei problemi dello sviluppatore con Socket.io) I documenti per principianti sono un un po 'scarso al momento, tuttavia.


5

Considererei l'aggiunta di un timbro modificato basato sul tempo per ogni set di dati. Quindi, se stai aggiornando le tabelle db, cambierai di conseguenza il timestamp modificato. Utilizzando AJAX, puoi confrontare il timestamp modificato del client con il timestamp dell'origine dati: se l'utente è in ritardo, aggiorna la visualizzazione. Simile a come questo sito controlla periodicamente una domanda per vedere se qualcun altro ha risposto mentre si digita una risposta.


Questo è un punto utile. Mi aiuta anche a capire i campi "LastEdited" nel nostro database più da un punto di vista della progettazione.
Raynos

Esattamente. Questo sito utilizza un "heartbeat", ovvero ogni x quantità di tempo invia una richiesta AJAX al server e passa l'ID dei dati da controllare, nonché il timestamp modificato che ha per quei dati. Quindi diciamo che siamo sulla domanda # 1029. Ogni richiesta AJAX, il server guarda solo il timestamp modificato per la domanda n. 1029. Se scopre che il client ha una versione precedente dei dati, risponde al hearbeat con una nuova copia. Il client può quindi ricaricare la pagina (aggiornamento) o visualizzare un qualche tipo di messaggio all'utente che lo avverte della presenza di nuovi dati.
Chris Baker,

i timbri modificati sono molto più carini rispetto all'hashing dei nostri attuali "dati" e al confronto con un hash sull'altro lato.
Raynos

1
Tieni presente che client e server devono avere accesso esattamente allo stesso tempo per evitare incongruenze.
Prayerslayer

3

È necessario utilizzare tecniche push (note anche come Comet o reverse Ajax) per propagare le modifiche all'utente non appena vengono apportate al database. La migliore tecnica attualmente disponibile per questo sembra essere il polling lungo Ajax, ma non è supportata da tutti i browser, quindi sono necessari dei fallback. Fortunatamente ci sono già soluzioni che gestiscono questo per te. Tra questi ci sono: orbited.org e il già citato socket.io.

In futuro ci sarà un modo più semplice per farlo che si chiama WebSocket, ma non è ancora sicuro quando lo standard sarà pronto per la prima serata in quanto vi sono problemi di sicurezza sullo stato attuale dello standard.

Non dovrebbero esserci problemi di concorrenza nel database con i nuovi oggetti. Ma quando un utente modifica un oggetto, il server deve disporre di una logica che controlli se l'oggetto è stato modificato o eliminato nel frattempo. Se l'oggetto è stato cancellato, la soluzione è, ancora una volta, semplice: basta eliminare la modifica.

Ma il problema più difficile appare quando più utenti modificano lo stesso oggetto contemporaneamente. Se l'utente 1 e 2 iniziano a modificare un oggetto contemporaneamente, apporteranno le modifiche agli stessi dati. Supponiamo che le modifiche apportate dall'utente 1 vengano inviate prima al server mentre l'utente 2 sta ancora modificando i dati. Hai quindi due opzioni: potresti provare a unire le modifiche dell'utente 1 ai dati dell'utente 2 oppure puoi dire all'utente 2 che i suoi dati non sono aggiornati e visualizzargli un messaggio di errore non appena i suoi dati vengono inviati al server. Quest'ultima non è un'opzione molto facile da usare qui, ma la prima è molto difficile da implementare.

Una delle poche implementazioni che ha davvero ottenuto questo risultato per la prima volta è stata EtherPad , che è stata acquisita da Google. Credo che abbiano poi utilizzato alcune delle tecnologie di EtherPad in Google Docs e Google Wave, ma non posso dirlo con certezza. Google apre anche EtherPad, quindi forse vale la pena dare un'occhiata, a seconda di cosa stai cercando di fare.

Non è davvero facile fare questo editing simultaneo di cose, perché non è possibile eseguire operazioni atomiche sul web a causa della latenza. Forse questo articolo ti aiuterà a saperne di più sull'argomento.


2

Cercare di scrivere tutto questo da soli è un grande lavoro ed è molto difficile farlo bene. Un'opzione consiste nell'usare un framework creato per mantenere i client sincronizzati con il database e tra loro in tempo reale.

Ho scoperto che il framework Meteor lo fa bene ( http://docs.meteor.com/#reactivity ).

"Meteor abbraccia il concetto di programmazione reattiva. Ciò significa che puoi scrivere il tuo codice in un semplice stile imperativo, e il risultato verrà ricalcolato automaticamente ogni volta che i dati cambiano da cui dipende il tuo codice."

"Questo semplice schema (calcolo reattivo + sorgente dati reattiva) ha un'ampia applicabilità. Il programmatore viene salvato dalla scrittura di chiamate di annullamento / riscrittura e assicurandosi che siano chiamate al momento giusto, eliminando intere classi di codice di propagazione dei dati che altrimenti intaserebbe il tuo applicazione con logica soggetta a errori. "


1

Non posso credere che nessuno abbia menzionato Meteor . È sicuramente un framework nuovo e immaturo (e supporta ufficialmente solo un DB), ma richiede tutto il lavoro e il pensiero di un'app multi-utente come descrive il poster. In effetti, NON puoi creare un'app di aggiornamento live per più utenti. Ecco un breve riepilogo:

  • Tutto è in node.js (JavaScript o CoffeeScript), quindi puoi condividere cose come le convalide tra il client e il server.
  • Utilizza websocket, ma può ripiegare su browser meno recenti
  • Si concentra sugli aggiornamenti immediati dell'oggetto locale (cioè l'interfaccia utente sembra scattante), con le modifiche inviate al server in background. Solo gli aggiornamenti atomici possono semplificare la miscelazione degli aggiornamenti. Gli aggiornamenti rifiutati sul server vengono annullati.
  • Come bonus, gestisce le ricariche del codice live per te e preserva lo stato dell'utente anche quando l'app cambia radicalmente.

Meteor è abbastanza semplice che ti suggerirei almeno di guardarlo per trovare idee da rubare.


1
Mi piace molto l'idea di Derby e Meteor per alcuni tipi di app .. la proprietà e le autorizzazioni di documenti / record sono solo un paio di problemi del mondo reale che non sono ben risolti. Inoltre, provenendo dal mondo MS di lunga data che rendeva quell'80% davvero facile e trascorreva troppo tempo sull'altro 20%, sono riluttante a usare tali soluzioni PFM (pura magia del cazzo).
Tracker1

1

Queste pagine di Wikipedia possono aiutare ad aggiungere una prospettiva all'apprendimento della concorrenza e dell'elaborazione simultanea per la progettazione di un'applicazione web ajax che tira o è spinto evento di stato ( EDA ) i messaggi in un modello di messaggistica . Fondamentalmente, i messaggi vengono replicati agli abbonati di canale che rispondono a eventi di modifica e richieste di sincronizzazione.

Esistono molte forme di software collaborativo basato sul Web simultaneo .

Esistono numerose librerie client API HTTP per etherpad-lite , un editor collaborativo in tempo reale .

django-realtime-playground implementa un'app di chat in tempo reale in Django con varie tecnologie in tempo reale come Socket.io .

Sia AppEngine che AppScale implementano l' API del canale AppEngine ; che è distinto dall'API di Google Realtime , dimostrata da googledrive / realtime-playground .


0

Le tecniche push lato server sono la strada da percorrere qui. Cometa è (o era?) Una parola in voga.

La particolare direzione che prendi dipende in gran parte dallo stack del tuo server e da quanto sei flessibile. Se puoi, darei un'occhiata a socket.io , che fornisce un'implementazione cross-browser di websocket, che fornisce un modo molto semplificato per avere una comunicazione bidirezionale con il server, consentendo al server di inviare aggiornamenti ai client.

In particolare, guarda questa dimostrazione dell'autore della biblioteca, che mostra quasi esattamente la situazione che descrivi.


Questa è un'ottima libreria per ridurre i problemi con la comminazione, ma ero più alla ricerca di informazioni di alto livello su come progettare l'applicazione
Raynos

1
Solo per notare che socket.io (e SignalR) sono framework che utilizzano websocket come scelta di prima classe, ma hanno fallback compatibili per utilizzare altre tecniche come comet, long polling, flash socket e foreverframe.
Tracker1
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.