Ci sono diverse domande qui.
1) Perché non possiamo eseguire incrementi nella transazione che non possono essere interrotti da altri comandi?
Tieni presente innanzitutto che le "transazioni" Redis sono completamente diverse da quelle che la maggior parte delle persone pensa che le transazioni siano nei DBMS classici.
# Does not work
redis.multi()
current = redis.get('powerlevel')
redis.set('powerlevel', current + 1)
redis.exec()
Devi capire cosa viene eseguito sul lato server (in Redis) e cosa viene eseguito sul lato client (nel tuo script). Nel codice sopra, i comandi GET e SET verranno eseguiti sul lato Redis, ma l'assegnazione alla corrente e il calcolo della corrente + 1 dovrebbero essere eseguiti sul lato client.
Per garantire l'atomicità, un blocco MULTI/EXEC ritarda l'esecuzione dei comandi Redis fino all'exec. Quindi il client accumulerà solo i comandi GET e SET in memoria e li eseguirà in un colpo solo e alla fine in modo atomico. Naturalmente, il tentativo di assegnare corrente al risultato di GET e di incremento avverrà molto prima. In realtà il metodo redis.get restituirà solo la stringa "QUEUED" per segnalare che il comando è in ritardo e l'incremento non funzionerà.
Nei blocchi MULTI/EXEC è possibile utilizzare solo comandi i cui parametri possono essere completamente conosciuti prima dell'inizio del blocco. Potresti voler leggere la documentazione per ulteriori informazioni.
2) Perché dobbiamo invece eseguire un'iterazione e attendere che nessuno modifichi il valore prima dell'inizio della transazione?
Questo è un esempio di pattern ottimistico simultaneo.
Se non usassimo WATCH/MULTI/EXEC, avremmo una potenziale condizione di gara:
# Initial arbitrary value
powerlevel = 10
session A: GET powerlevel -> 10
session B: GET powerlevel -> 10
session A: current = 10 + 1
session B: current = 10 + 1
session A: SET powerlevel 11
session B: SET powerlevel 11
# In the end we have 11 instead of 12 -> wrong
Ora aggiungiamo un blocco WATCH/MULTI/EXEC. Con una clausola WATCH, i comandi tra MULTI ed EXEC vengono eseguiti solo se il valore non è cambiato.
# Initial arbitrary value
powerlevel = 10
session A: WATCH powerlevel
session B: WATCH powerlevel
session A: GET powerlevel -> 10
session B: GET powerlevel -> 10
session A: current = 10 + 1
session B: current = 10 + 1
session A: MULTI
session B: MULTI
session A: SET powerlevel 11 -> QUEUED
session B: SET powerlevel 11 -> QUEUED
session A: EXEC -> success! powerlevel is now 11
session B: EXEC -> failure, because powerlevel has changed and was watched
# In the end, we have 11, and session B knows it has to attempt the transaction again
# Hopefully, it will work fine this time.
Quindi non è necessario eseguire iterazioni per attendere che nessuno modifichi il valore, ma piuttosto tentare l'operazione ancora e ancora finché Redis non è sicuro che i valori siano coerenti e segnali che ha avuto successo.
Nella maggior parte dei casi, se le "transazioni" sono abbastanza veloci e la probabilità di avere contese è bassa, gli aggiornamenti sono molto efficienti. Ora, in caso di contesa, sarà necessario eseguire alcune operazioni extra per alcune "transazioni" (a causa dell'iterazione e dei tentativi). Ma i dati saranno sempre coerenti e non è richiesto alcun blocco.