English | 简体中文 | 繁體中文 | Русский язык | Français | Español | Português | Deutsch | 日本語 | 한국어 | Italiano | بالعربية
La lock distribuita generalmente ha tre modi di implementazione: 1. Lock ottimistico del database; 2. Lock distribuito basato su Redis; 3. Lock distribuito basato su ZooKeeper. Questo blog introduce il secondo modo, il lock distribuito implementato su Redis. Anche se ci sono già vari blog che spiegano l'implementazione del lock distribuito su Redis, le loro implementazioni hanno vari problemi. Per evitare di ingannare i lettori, questo blog spiega come implementare correttamente il lock distribuito su Redis.
Riabilità
Prima di tutto, per garantire che il lock distribuito sia disponibile, dobbiamo assicurarci che l'implementazione del lock soddisfi contemporaneamente le seguenti quattro condizioni:
Mutualità. In qualsiasi momento, solo un client può detenere il lock.
Non si verifica il blocco morto. Anche se un client crolla durante la detenzione del lock senza sciogliere attivamente il lock, si garantisce che altri client possano aggiungere il lock.
Ha la capacità di tollerare guasti. Finché la maggior parte dei nodi Redis funziona correttamente, il client può aggiungere e sciogliere i lock.
Per sciogliere il gancio è necessario il cordino con cui è stato legato. La lock e la解锁 devono essere eseguite dallo stesso client, il client non può sciogliere un lock aggiunto da un altro client.
Implementazione del codice
Dipendenza del componente
Prima di tutto, dobbiamo introdurre il componente open source Jedis tramite Maven, aggiungendo il seguente codice nel file pom.xml:
<dependency> <groupId>redis.clients</groupId> <artifactId>jedis</artifactId> <version>2.9.0</version> </dependency>
Codice di lock
Postura corretta
La parola è facile, mostrami il codice. Prima di spiegare come è stato implementato:
public class RedisTool { private static final String LOCK_SUCCESS = "OK"; private static final String SET_IF_NOT_EXIST = "NX"; private static final String SET_WITH_EXPIRE_TIME = "PX"; /** * Tentativo di ottenere un lock distribuito * @param jedis Client Redis * @param lockKey Blocco * @param requestId Identificatore della richiesta * @param expireTime Tempo di scadenza * @return Se ottiene con successo */ public static Boolean tryGetDistributedLock(Jedis jedis, String lockKey, String requestId, int expireTime) { String result = jedis.set(lockKey, requestId, SET_IF_NOT_EXIST, SET_WITH_EXPIRE_TIME, expireTime); if (LOCK_SUCCESS.equals(result)) { return true; } return false; } }
Come possiamo vedere, aggiungere un blocco richiede solo una riga di codice: jedis.set(String key, String value, String nxxx, String expx, int time), questo metodo set() ha cinque parametri formali:
Il primo è key, usiamo key come blocco perché la key è unica.
Il secondo è value, inviamo value che è requestId, molti studenti potrebbero non capire, se la key è sufficiente come blocco, perché usare anche value? La ragione è che quando abbiamo parlato della affidabilità, la lock distribuita deve soddisfare la quarta condizione, il quale deve essere sbloccato chi lo ha bloccato, attraverso l'assegnazione del valore value a requestId, sappiamo quale richiesta ha aggiunto questa lock, e possiamo avere una base durante la sbloccatura. requestId può essere generato utilizzando il metodo UUID.randomUUID().toString().
Il terzo è nxxx, questo parametro inseriamo è NX, il che significa SETIFNOTEXIST, ovvero se la key non esiste, eseguiamo l'operazione set; se la key esiste già, non si esegue alcuna operazione;
Il quarto è expx, questo parametro inviamo è PX, il che significa che dobbiamo aggiungere una impostazione di scadenza alla key, il tempo specifico è determinato dal quinto parametro.
Il quinto è time, che corrisponde al quarto parametro, rappresentando il tempo di scadenza della key.
In sintesi, l'esecuzione del metodo set() sopra menzionato può portare a due risultati: 1. Se non esiste un blocco corrente (la key non esiste), si esegue l'operazione di bloccatura e si imposta un periodo di validità per il blocco, contemporaneamente il valore indica il client che effettua il blocco. 2. Se esiste già un blocco, non si esegue alcuna operazione.
Attenzione ai dettagli, il nostro codice di blocco soddisfa le tre condizioni descritte nella nostra descrizione della affidabilità. Prima di tutto, set() aggiunge il parametro NX, che garantisce che se esiste già una key, la funzione non avrà successo, ovvero solo un client può mantenere il blocco, soddisfacendo l'esclusività. In secondo luogo, poiché abbiamo impostato un tempo di scadenza per il blocco, anche se il detentore del blocco si rompe senza sbloccare, il blocco verrà automaticamente sbloccato (cioè la key verrà eliminata) quando scade il tempo di scadenza, evitando che si verifichi un blocco morto. Infine, poiché assegniamo il valore value a requestId, che rappresenta l'identificativo della richiesta del client che effettua il blocco, il client può verificare se è lo stesso client durante la sbloccatura. Poiché consideriamo solo l'ambiente di distribuzione Redis in un unico host, non dobbiamo preoccuparci della tolleranza ai guasti.
Esempio di errore 1
Un esempio di errore comune è l'uso della combinazione jedis.setnx() e jedis.expire() per implementare il blocco, il codice è come segue:
public static void wrongGetLock1(Jedis jedis, String lockKey, String requestId, int expireTime) { long result = jedis.setnx(lockKey, requestId); if (result == 1) { // Se qui il programma si blocca improvvisamente, non è possibile impostare il tempo di scadenza, il che può causare un blocco morto jedis.expire(lockKey, expireTime); } }
Il metodo setnx() ha l'azione SETIFNOTEXIST, il metodo expire() è aggiungere un tempo di scadenza al blocco. A prima vista sembra che il risultato sia lo stesso del metodo set() precedente, tuttavia, poiché sono due comandi Redis, non sono atomici. Se il programma si blocca improvvisamente dopo l'esecuzione di setnx(), il blocco non viene impostato con un tempo di scadenza, il che può causare un blocco morto. La ragione per cui qualcuno realizza in questo modo è che le versioni precedenti di jedis non supportano il metodo set() con più parametri.
Esempio di errore 2
Questo esempio di errore è piuttosto difficile da trovare e la sua implementazione è anche complessa. Pianificazione dell'implementazione: utilizzare il comando jedis.setnx() per implementare il blocco, tra cui la chiave è il blocco e il valore è il tempo di scadenza del blocco. Processo di esecuzione: 1. Tentare di bloccare attraverso il metodo setnx(), se il blocco non esiste, restituire il successo dell'acquisizione del blocco. 2. Se il blocco esiste, ottenere il tempo di scadenza del blocco e confrontarlo con l'ora corrente, se il blocco è scaduto, impostare un nuovo tempo di scadenza e restituire il successo dell'acquisizione del blocco. Il codice è come segue:
public static Boolean wrongGetLock2(Jedis jedis, String lockKey, int expireTime) { long expires = System.currentTimeMillis() + expireTime; String expiresStr = String.valueOf(expires); // Se l'intero non esiste, restituisce il successo dell'acquisizione del blocco if (jedis.setnx(lockKey, expiresStr) == 1) { return true; } // Se esiste il blocco, ottieni l'ora di scadenza del blocco String currentValueStr = jedis.get(lockKey); if (currentValueStr != null && long.parselong(currentValueStr) < System.currentTimeMillis()) { // Se il blocco è scaduto, ottieni l'ora di scadenza del blocco precedente e impostala ora per il blocco String oldValueStr = jedis.getSet(lockKey, expiresStr); if (oldValueStr != null && oldValueStr.equals(currentValueStr)) { // Considerando la situazione di concorrenza multi-threading, solo se il valore impostato e il valore corrente sono identici per un thread, questo thread ha il diritto di aggiungere il blocco return true; } } // Altre situazioni, restituisce sempre fallimento di aggiunta blocco return false; }
Dove sta il problema di questo pezzo di codice? 1. Poiché l'ora di scadenza è generata dal client, è necessario richiedere che ogni client distribuito abbia l'ora sincronizzata. 2. Quando il blocco scade, se più client eseguono contemporaneamente il metodo jedis.getSet(), anche se alla fine solo un client può aggiungere il blocco, l'ora di scadenza del blocco di questo client potrebbe essere sovrascritta da altri client. 3. Il blocco non ha un identificatore del proprietario, quindi qualsiasi client può sbloccare.
Codice di sblocco
Postura corretta
Prima di tutto, esponiamo il codice e poi spieghiamo gradualmente perché è stato implementato in questo modo:
public class RedisTool { private static final long RELEASE_SUCCESS = 1L; /** * Rilascio del blocco distribuito * @param jedis Client Redis * @param lockKey Blocco * @param requestId Identificatore della richiesta * @return Se è stato rilasciato con successo */ public static Boolean releaseDistributedLock(Jedis jedis, String lockKey, String requestId) { String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end"; Object result = jedis.eval(script, Collections.singletonList(lockKey), Collections.singletonList(requestId)); if (RELEASE_SUCCESS.equals(result)) { return true; } return false; } }
Si può vedere che per sbloccare abbiamo bisogno solo di due righe di codice! La prima riga di codice, abbiamo scritto un semplice codice Lua, l'ultima volta che abbiamo visto questo linguaggio di programmazione è stato nel libro "Hackers & Painters", non ci aspettavamo di usarlo questa volta. La seconda riga di codice, abbiamo trasmesso il codice Lua al metodo jedis.eval() e abbiamo assegnato a KEYS[1] il valore lockKey e a ARGV[1] il valore requestId. Il metodo eval() consegna il codice Lua al server Redis.
Quindi, qual è la funzione di questo codice Lua? In realtà è molto semplice, prima di ottenere il valore associato al blocco, controllare se è uguale a requestId, se è uguale, eliminare il blocco (sbloccare). Allora perché utilizzare il linguaggio Lua? Perché si vuole garantire che l'operazione sopra sia atomica. Su cosa possono portare i problemi della non atomicità, si può leggere nel documento 【Esempio di errore di codifica di sblocco 2】. Perché l'esecuzione del metodo eval() può garantire l'atomicità, grazie alle caratteristiche di Redis, ecco una parte dell'interpretazione del comando eval nella pagina ufficiale:
In parole semplici, quando si esegue il comando eval per eseguire il codice Lua, il codice Lua viene trattato come un comando e Redis esegue altri comandi solo dopo che il comando eval è stato completato.
Esempio di errore 1
Il codice di sblocco più comune è quello di utilizzare direttamente il metodo jedis.del() per eliminare il blocco, questo metodo di sblocco che non giudica il proprietario del blocco prima di procedere può portare a ciò che qualsiasi client può sbloccare in qualsiasi momento, anche se questo blocco non è suo.
public static void wrongReleaseLock1(Jedis jedis, String lockKey) { jedis.del(lockKey); }
Esempio di errore 2
Questa codifica di sblocco, a prima vista, sembra anche avere senso, persino io ho quasi implementato in questo modo, quasi come la postura corretta, l'unica differenza è che è divisa in due comandi per essere eseguiti, il codice è il seguente:
public static void wrongReleaseLock2(Jedis jedis, String lockKey, String requestId) { // Giudicare se il blocco e la sblocco sono effettuati dallo stesso client if (requestId.equals(jedis.get(lockKey))) { // Se in questo momento, questa chiave di lock non appartiene più a questo client, verrà interpretato come un lock jedis.del(lockKey); } }
Come commentato nel codice, il problema risiede nel fatto che se si chiama il metodo jedis.del() quando questa chiave di lock non appartiene più al client corrente, viene rilasciato il lock di qualcun altro. Quindi, esiste davvero questo scenario? La risposta è affermativa, ad esempio, se il client A aggiunge il lock, dopo un certo periodo di tempo il client A rilascia il lock, prima di eseguire jedis.del(), il lock è scaduto, quindi il client B tenta di aggiungere il lock con successo, e poi il client A esegue il metodo del(), liberando il lock del client B.
Conclusione
Questo articolo introduce come implementare correttamente il lock distribuito Redis utilizzando codice Java, fornendo anche due esempi di errore classici per la lock e la unlock. In realtà, non è difficile implementare un lock distribuito Redis, basta garantire che si possano soddisfare le quattro condizioni della affidabilità.
In quali scenari viene principalmente utilizzato il lock distribuito? È necessario sincronizzare i punti, ad esempio, quando si inserisce una riga di dati, è necessario verificare preventivamente se esistono dati simili nel database, quando più richieste inseriscono contemporaneamente, potrebbe verificarsi che tutti i database restituiscono dati simili, quindi possono essere aggiunti. In questo caso, è necessario eseguire la gestione della sincronizzazione, ma bloccare direttamente il database richiede troppo tempo, quindi si utilizza il lock distribuito Redis, che consente solo a un thread di eseguire l'operazione di inserimento dei dati, mentre gli altri thread attendono.
Questo è tutto il contenuto dell'articolo su come implementare correttamente il lock distribuito Redis per il linguaggio Java, spero sia utile a tutti. Gli amici interessati possono continuare a consultare altre sezioni correlate del nostro sito, e sono lieti di ricevere commenti sugli aspetti insufficienti. Grazie per il supporto degli amici al nostro sito!
Dichiarazione: il contenuto di questo articolo è stato raccolto da Internet, il diritto d'autore è dell'autore originale, il contenuto è stato caricato autonomamente dagli utenti di Internet, il sito web non possiede il diritto di proprietà, non è stato elaborato manualmente e non assume responsabilità per le relative responsabilità legali. Se trovi contenuti sospetti di violazione del copyright, ti preghiamo di inviare una e-mail a: notice#oldtoolbag.com (al momento dell'invio dell'e-mail, sostituisci # con @) per segnalare, fornendo prove pertinenti. Una volta verificata, il sito web eliminerà immediatamente il contenuto sospetto di violazione del copyright.