Blocco riga InnoDB: come implementare


13

Mi sto guardando intorno, leggendo il sito mysql e ancora non riesco ancora a vedere esattamente come funziona.

Voglio selezionare e bloccare il risultato per la scrittura, scrivere la modifica e rilasciare il blocco. audocommit è attivo.

schema

id (int)
name (varchar50)
status (enum 'pending', 'working', 'complete')
created (datetime)
updated (datetime) 

Seleziona un articolo con uno stato in sospeso e aggiornalo su funzionante. Utilizzare una scrittura esclusiva per assicurarsi che lo stesso articolo non venga ritirato due volte.

così;

"SELECT id FROM `items` WHERE `status`='pending' LIMIT 1 FOR WRITE"

ottenere l'id dal risultato

"UPDATE `items` SET `status`='working', `updated`=NOW() WHERE `id`=<selected id>

Devo fare qualcosa per rilasciare il blocco e funziona come ho fatto sopra?

Risposte:


26

Quello che vuoi è SELEZIONARE ... PER AGGIORNARE dal contesto di una transazione. SELECT FOR UPDATE inserisce un blocco esclusivo nelle righe selezionate, proprio come se si eseguisse UPDATE. Funziona anche implicitamente nel livello di isolamento READ COMMITTED indipendentemente da ciò che il livello di isolamento è esplicitamente impostato su. Basta essere consapevoli del fatto che SELECT ... FOR UPDATE è molto dannoso per la concorrenza e deve essere utilizzato solo quando assolutamente necessario. Ha anche la tendenza a moltiplicarsi in una base di codice mentre le persone tagliano e incollano.

Ecco una sessione di esempio dal database Sakila che illustra alcuni comportamenti delle query FOR UPDATE.

Innanzitutto, solo così siamo cristallini, imposta il livello di isolamento della transazione su REPEATABLE READ. Questo di solito non è necessario, poiché è il livello di isolamento predefinito per InnoDB:

session1> SET TRANSACTION ISOLATION LEVEL REPEATABLE READ;
session1> BEGIN;
session1> SELECT first_name, last_name FROM customer WHERE customer_id = 3;
+------------+-----------+
| first_name | last_name |
+------------+-----------+
| LINDA      | WILLIAMS  |
+------------+-----------+
1 row in set (0.00 sec)    

Nell'altra sessione, aggiorna questa riga. Linda si sposò e cambiò nome:

session2> UPDATE customer SET last_name = 'BROWN' WHERE customer_id = 3;
Query OK, 1 row affected (0.00 sec)
Rows matched: 1  Changed: 1  Warnings: 0

Nella sessione 1, perché eravamo in REPEATABLE READ, Linda è ancora LINDA WILLIAMS:

session1> SELECT first_name, last_name FROM customer WHERE customer_id = 3;
+------------+-----------+
| first_name | last_name |
+------------+-----------+
| LINDA      | WILLIAMS  |
+------------+-----------+
1 row in set (0.00 sec)

Ma ora, vogliamo l'accesso esclusivo a questa riga, quindi chiamiamo FOR UPDATE sulla riga. Si noti che ora otteniamo la versione più recente della riga indietro, che è stata aggiornata nella sessione2 al di fuori di questa transazione. Non è REPEATABLE READ, è READ COMMITTED

session1> SELECT first_name, last_name FROM customer WHERE customer_id = 3 FOR UPDATE;
+------------+-----------+
| first_name | last_name |
+------------+-----------+
| LINDA      | BROWN     |
+------------+-----------+
1 row in set (0.00 sec)

Testiamo il blocco impostato in session1. Si noti che session2 non può aggiornare la riga.

session2> UPDATE customer SET last_name = 'SMITH' WHERE customer_id = 3;
ERROR 1205 (HY000): Lock wait timeout exceeded; try restarting transaction

Ma possiamo ancora selezionare da esso

session2> SELECT c.customer_id, c.first_name, c.last_name, a.address_id, a.address FROM customer c JOIN address a USING (address_id) WHERE c.customer_id = 3;
+-------------+------------+-----------+------------+-------------------+
| customer_id | first_name | last_name | address_id | address           |
+-------------+------------+-----------+------------+-------------------+
|           3 | LINDA      | BROWN     |          7 | 692 Joliet Street |
+-------------+------------+-----------+------------+-------------------+
1 row in set (0.00 sec)

E possiamo ancora aggiornare una tabella figlio con una relazione di chiave esterna

session2> UPDATE address SET address = '5 Main Street' WHERE address_id = 7;
Query OK, 1 row affected (0.05 sec)
Rows matched: 1  Changed: 1  Warnings: 0

session1> COMMIT;

Un altro effetto collaterale è che aumenti notevolmente la probabilità di provocare un deadlock.

Nel tuo caso specifico, probabilmente vuoi:

BEGIN;
SELECT id FROM `items` WHERE `status`='pending' LIMIT 1 FOR UPDATE;
-- do some other stuff
UPDATE `items` SET `status`='working', `updated`=NOW() WHERE `id`=<selected id>;
COMMIT;

Se il pezzo "fai qualche altra cosa" non è necessario e in realtà non è necessario mantenere le informazioni sulla riga intorno, quindi SELEZIONA PER AGGIORNAMENTO è inutile e dispendioso e puoi invece semplicemente eseguire un aggiornamento:

UPDATE `items` SET `status`='working', `updated`=NOW() WHERE `status`='pending' LIMIT 1;

Spero che ciò abbia un senso.


3
Grazie. Non sembra risolvere il mio problema, quando arrivano due thread con "SELECT id FROM itemsWHERE status= 'pending' LIMIT 1 FOR UPDATE;" ed entrambi vedono la stessa riga, quindi uno bloccherà l'altro. Speravo in qualche modo che fosse in grado di bypassare la fila bloccata e andare al prossimo oggetto che era in sospeso ..
Wizzard

1
La natura dei database è che restituiscono dati coerenti. Se si esegue quella query due volte prima che il valore sia stato aggiornato, si otterrà lo stesso risultato. Non esiste un'estensione SQL "get me il primo valore corrispondente a questa query, a meno che la riga non sia bloccata" di cui sono a conoscenza. Sembra sospetto che tu stia implementando una coda su un database relazionale. È così?
Aaron Brown,

Aaron; sì, è quello che sto cercando di fare. Ho provato a usare qualcosa come Gearman, ma è stato un fallimento. Hai in mente qualcos'altro?
Wizzard,

Penso che dovresti leggere questo: engineyard.com/blog/2011/… - per le code dei messaggi, ce ne sono molti là fuori a seconda della lingua del tuo cliente scelta. ActiveMQ, Resque (Ruby + Redis), ZeroMQ, RabbitMQ, ecc.
Aaron Brown

Come posso fare in modo che la sessione 2 si blocchi sulla lettura fino a quando non viene eseguito l'aggiornamento nella sessione 1?
CMCDragonkai,

2

Se si utilizza il motore di archiviazione InnoDB, utilizza il blocco a livello di riga. In combinazione con il multi-versioning, ciò si traduce in una buona concorrenza tra query perché una determinata tabella può essere letta e modificata da client diversi contemporaneamente. Le proprietà di concorrenza a livello di riga sono le seguenti:

Client diversi possono leggere le stesse righe contemporaneamente.

Clienti diversi possono modificare file diverse contemporaneamente.

Client diversi non possono modificare la stessa riga contemporaneamente. Se una transazione modifica una riga, altre transazioni non possono modificare la stessa riga fino al completamento della prima transazione. Anche le altre transazioni non possono leggere la riga modificata, a meno che non stiano utilizzando il livello di isolamento READ UNCOMMITTED. Cioè, vedranno la riga originale non modificata.

Fondamentalmente, non è necessario specificare il blocco esplicito InnoDB lo gestisce anche se in alcune situazioni potrebbe essere necessario fornire i dettagli del blocco esplicito sul blocco esplicito riportato di seguito:

L'elenco seguente descrive i tipi di blocco disponibili e i loro effetti:

LEGGERE

Blocca un tavolo per la lettura. Un blocco READ blocca una tabella per le query di lettura come SELECT che recupera i dati dalla tabella. Non consente operazioni di scrittura come INSERT, DELETE o UPDATE che modificano la tabella, anche dal client che detiene il blocco. Quando una tabella è bloccata per la lettura, altri client possono leggere dalla tabella contemporaneamente, ma nessun client può scrivervi. Un client che desidera scrivere su una tabella bloccata in lettura deve attendere il completamento e il rilascio dei blocchi da parte di tutti i client attualmente in lettura.

SCRIVI

Blocca un tavolo per la scrittura. Una serratura WRITE è una serratura esclusiva. Può essere acquisito solo quando non viene utilizzata una tabella. Una volta acquisito, solo il client che detiene il blocco di scrittura può leggere o scrivere sulla tabella. Gli altri client non possono né leggere né scrivere su di esso. Nessun altro client può bloccare la tabella per la lettura o la scrittura.

LEGGI LOCALE

Blocca una tabella per la lettura, ma consente inserimenti simultanei. Un inserto simultaneo è un'eccezione al principio "lettori bloccano gli scrittori". Si applica solo alle tabelle MyISAM. Se una tabella MyISAM non presenta buchi nel mezzo risultanti da record eliminati o aggiornati, gli inserimenti avvengono sempre alla fine della tabella. In tal caso, un client che legge da una tabella può bloccarlo con un blocco READ LOCAL per consentire ad altri client di inserirsi nella tabella mentre il client che detiene il blocco di lettura legge da esso. Se una tabella MyISAM presenta buchi, è possibile rimuoverli utilizzando OPTIMIZE TABLE per deframmentare la tabella.


Grazie per la risposta. Dato che ho questa tabella e 100 clienti che stanno verificando la presenza di articoli in sospeso, ho ricevuto molte collisioni - 2-3 clienti hanno ottenuto la stessa riga in sospeso. Il blocco della tabella deve rallentare.
Wizzard,

0

Un'altra alternativa sarebbe quella di aggiungere una colonna che memorizzava il tempo dell'ultimo blocco riuscito e quindi qualsiasi altra cosa che volesse bloccare la riga avrebbe dovuto attendere fino a quando non fosse stata cancellata o fossero trascorsi 5 minuti (o qualsiasi altra cosa).

Qualcosa di simile a...

Schema

id (int)
name (varchar50)
status (enum 'pending', 'working', 'complete')
created (datetime)
updated (datetime)
lastlock (int)

lastlock è un int in quanto memorizza il timestamp unix come più facile (e forse più veloce) da confrontare.

// Scusa la semantica, non ho verificato che funzionino acutamente, ma dovrebbero essere abbastanza vicini se non lo fanno.

UPDATE items 
  SET lastlock = UNIX_TIMESTAMP() 
WHERE 
  lastlock = 0
  OR (UNIX_TIMESTAMP() - lastlock) > 360;

Quindi controlla per vedere quante righe sono state aggiornate, poiché le righe non possono essere aggiornate da due processi contemporaneamente, se hai aggiornato la riga, ottieni il blocco. Supponendo che tu stia usando PHP, useresti mysql_affected_rows (), se il ritorno da quello fosse 1, l'hai bloccato con successo.

Quindi puoi aggiornare lo lastlock a 0 dopo aver fatto ciò che devi fare, oppure essere pigro e attendere 5 minuti quando il prossimo tentativo di blocco avrà comunque successo.

EDIT: potrebbe essere necessario un po 'di lavoro per verificare che funzioni come previsto nei cambiamenti dell'ora legale in quanto gli orologi tornerebbero indietro di un'ora, forse rendendo il controllo vuoto. Dovresti assicurarti che i timestamp di Unix fossero in UTC - che potrebbero essere comunque.


-1

In alternativa, è possibile frammentare i campi del record per consentire la scrittura parallela e bypassare il blocco della riga (stile coppie json frammentate). Quindi, se un campo di un record di lettura composto era un numero intero / reale, si potrebbe avere il frammento 1-8 di quel campo (8 record / righe di scrittura attivi). Quindi sommare i frammenti round-robin dopo ogni scrittura in una ricerca di lettura separata. Ciò consente fino a 8 utenti simultanei in parallelo.

Dato che stai solo lavorando con ogni frammento creando un totale parziale, non ci sono collisioni e veri aggiornamenti paralleli (cioè blocchi in scrittura ogni frammento anziché l'intero record di lettura unificato). Questo funziona ovviamente solo su campi numerici. Qualcosa che si basa su modifiche matematiche per memorizzare un risultato.

Pertanto, più frammenti di scrittura per campo di lettura unificato per record di lettura unificato. Questi frammenti numerici si prestano anche a ECC, crittografia e trasferimento / archiviazione a livello di blocco. Più frammenti di scrittura ci sono, maggiore è la velocità di accesso in scrittura parallela / simultanea su dati saturi.

MMORPG soffre enormemente di questo problema, quando un gran numero di giocatori inizia a picchiarsi a vicenda con abilità Area d'effetto. Tutti questi giocatori multipli devono scrivere / aggiornare tutti gli altri giocatori esattamente nello stesso momento, in parallelo, creando una tempesta di blocco della riga di scrittura sui record dei giocatori unificati.

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.