Unit of Work Concurrency, come viene gestito

5

Sto leggendo alcune informazioni sul modello dell'Unità di lavoro. C'è una cosa che non è molto chiara per me: cosa succederà quando richiedi un paio di record su un thread (1) e un altro thread (2) rimuove uno di quei record e commetti il cambiamento?

Il record rimosso sul primo thread ha uno stato "rimosso"? Il problema con questo approccio è che il programma deve controllare ogni volta che vuole usare un record quando ancora esiste. Deve anche bloccare il record quando vuole usarlo.

Non riesco a pensare ad alcun altro approccio, come si risolve questo problema nella pratica?

    
posta Laurence 22.11.2014 - 11:48
fonte

1 risposta

5

In generale un'unità di lavoro corrisponde a una transazione di database. Quindi da quella prospettiva:

BEGIN TRANSACTION

-- do stuff

COMMIT TRANSACTION

I database sono molto avanzati su come gestiscono questo. Tutti i dati che leggi dopo l'istruzione BEGIN TRANSACTION sono "bloccati" su qualsiasi altro thread (nota che puoi sovrascrivere questo comportamento nella tua istruzione SELECT con alcune opzioni di blocco esplicite, ma sto ignorando che qui). Tutti gli altri thread che desiderano accedere agli stessi dati verranno bloccati fino a quando questa transazione non viene confermata o annullata. Ecco perché è importante rendere il lavoro svolto all'interno della transazione il più breve possibile.

In pratica, in un'applicazione che utilizza il modello di repository, esistono due scenari tipici:

  1. Leggi-modifica-scrivi tutto all'interno di una transazione
  2. Leggi in una transazione di sola lettura, modifica-scrivi in una successiva transazione di lettura / scrittura

Per lo scenario 1, immagina di avere un pulsante chiamato "Elimina vecchi record". Il pulsante dovrebbe (approssimativamente) fare questo:

using(var uow = GetUnitOfWork())
{
    var repository = uow.GetRepository<MyRecord>();
    var oldRecords = repository.Entities
        .Where(x => x.Old)
        .ToList();
    foreach(var record in oldRecords)
    {
        repository.Delete(record);
    }
    uow.Commit(); // sometimes this is optional (default behavior could be to commit)
}

Poiché tutta la lettura / scrittura viene eseguita all'interno di un'unità di lavoro, che si trova all'interno di una transazione di database, se due persone hanno fatto clic su questo pulsante contemporaneamente, non c'è alcun problema di concorrenza perché il database serializzerà le richieste .

Lo scenario 2 è molto più comune e più complicato. Immagina di avere una schermata di modifica, come "Modifica dipendente". In genere non si desidera mantenere una transazione aperta per tutto il tempo in cui l'utente ha la schermata di modifica aperta, quindi prima si ottengono le informazioni sul dipendente e lo si visualizza sullo schermo. Quindi l'utente apporta le modifiche e fa clic sul pulsante "Salva". È ragionevole supporre che anche qualcun altro stia modificando quel dipendente allo stesso tempo, quindi entrambi hanno iniziato con la stessa copia del dipendente, hanno apportato 2 diverse modifiche e poi hanno fatto clic su "Salva" (uno di essi farà clic per primo).

Se, all'interno della routine di salvataggio, ricarichi i dati dei dipendenti, modifica solo i dati modificati , quindi esegui il commit, finché i due utenti cambiano qualcosa di diverso (l'utente 1 ha cambiato il nome e l'utente 2 ha cambiato il cognome) quindi è possibile che entrambe le modifiche vengano apportate. Tuttavia, se si scrive la routine di salvataggio in modo tale da copiare tutti i dati dallo schermo di modifica nell'entità dipendente e salvarlo, le modifiche dell'utente 2 sovrascriveranno le modifiche dell'utente 1 (perché l'utente 2 ha ancora il nome originale nel proprio schermo ). Inoltre, se sia l'utente 1 che l'utente 2 hanno cambiato lo stesso campo, la modifica dell'utente 2 sovrascriverà sempre la modifica dell'utente 1. Questo non è il modo giusto per farlo.

In pratica, usando qualcosa come Entity Framework o NHibernate, ci sono due concetti: una sessione e una transazione. Quando viene creata la schermata di modifica, si crea anche una sessione. La sessione tiene traccia di tutti i dati caricati dal database durante quella sessione (compresi i valori originali). La sessione viene eliminata quando lo schermo è chiuso (sto considerando un'applicazione desktop qui, non un'applicazione web). Quando si fa clic su Salva, viene avviata una transazione, ma invece di ricaricare i dati dal database, è sufficiente modificare l'entità dipendente originale caricata dal database e quindi eseguire il commit della transazione. Quindi il framework fa un'istruzione UPDATE come questa (supponiamo che la tabella dei dipendenti abbia solo 3 colonne: "id", "first_name", "last_name"):

BEGIN TRANSACTION

UPDATE Employee
SET first_name = 'new first name'
WHERE id = 123
    AND first_name = 'old first name'
    AND last_name = 'last name'

COMMIT TRANSACTION

Si chiama concorrenza ottimistica. Ciò significa che se qualcun altro ha modificato quel record tra quando hai caricato la schermata di modifica e quando hai fatto clic su Salva, l'aggiornamento avrà esito negativo (e il database indicherà ciò dicendo "0 record aggiornati" invece di "1 record aggiornato") . Il framework rileverà questo errore e in genere genererà un'eccezione (NHibernate genera StaleObjectStateException ). Sta a te gestire questa eccezione. Onestamente ho appena mostrato un messaggio come "qualcun altro ha modificato questo record allo stesso tempo, quindi dovrai chiudere questa schermata e riprovare."

Nota che c'è un'ottimizzazione che puoi fare. Puoi aggiungere una nuova colonna alla tua tabella chiamata VERSION . Quindi puoi configurare il tuo framework per usare quella colonna invece di controllare ogni singola colonna nella tabella per vedere se sono cambiati, quindi l'aggiornamento sarebbe simile a questo:

BEGIN TRANSACTION

UPDATE Employee
SET first_name = 'new first name', 
    version = 2
WHERE id = 123
    AND version = 1

COMMIT TRANSACTION

Quindi, stai facendo affidamento sul database per assicurarti che le modifiche siano atomiche (indivisibili) ma devi fare del lavoro extra per gestire l'elemento umano, dal momento che le persone hanno bisogno di ottenere una copia dei dati, pensaci per un po 'e apporta una modifica, e i dati potrebbero essere cambiati durante quel periodo. Dipende da te come programmatore come vuoi gestire i conflitti in questo modo.

    
risposta data 22.11.2014 - 13:12
fonte

Leggi altre domande sui tag