2012-12-23 7 views
5

Sto cercando un modo per selezionare esplicitamente una riga di tabella per un thread. Ho scritto un crawler, che funziona con circa 50 processi paralleli. Ogni processo deve prendere una riga da un tavolo ed elaborarla.Selezionare una sola riga di tabella su connessioni parallele alte

CREATE TABLE `crawler_queue` (
`id` int(10) unsigned NOT NULL AUTO_INCREMENT, 
`url` text NOT NULL, 
`class_id` tinyint(3) unsigned NOT NULL, 
`server_id` tinyint(3) unsigned NOT NULL, 
`proc_id` mediumint(8) unsigned NOT NULL, 
`prio` tinyint(3) unsigned NOT NULL, 
`inserted` int(10) unsigned NOT NULL, 
PRIMARY KEY (`id`), 
KEY `proc_id` (`proc_id`), 
KEY `app_id` (`app_id`), 
KEY `crawler` (`class_id`,`prio`,`proc_id`) 
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8 

Ora i miei processi di effettuare le seguenti operazioni:

  • transazione inizio DB
  • fare un selezionato come SELECT * FROM crawler_queue WHERE class_id=2 AND prio=20 AND proc_id=0 ORDER BY id LIMIT 1 FOR UPDATE
  • quindi aggiornare questa riga con UPDATE crawler_queue SET server_id=1,proc_id=1376 WHERE id=23892
  • commit della transazione

Ciò dovrebbe aiutare che nessun altro processo può afferrare una riga ancora elaborata. Facendo un EXPLAIN sui spettacoli selezionati

id select_type table   type possible_keys key  key_len ref rows Extra 
1 SIMPLE  crawler_queue ref proc_id,crawler proc_id 3  const 617609 Using where 

Ma i processi sembrano causare troppo elevato parallelismo, perché a volte riesco a vedere due tipi di errori/avvisi nel mio log (ogni 5 minuti o giù di lì):

mysqli::query(): (HY000/1205): Lock wait timeout exceeded; try restarting transaction (in /var/www/db.php l 
ine 81) 

mysqli::query(): (40001/1213): Deadlock found when trying to get lock; try restarting transaction (in /var/www/db.php line 81) 

La mia domanda è: qualcuno può indicarmi la giusta direzione per minimizzare questi problemi di blocco? (In stato di produzione, il parallelismo sarebbe 3-4 volte superiore a quello ora, quindi assumere, che ci sarebbero problemi molto più bloccaggio)

EDIT 2012/12/29: Ho modificato SELECT usare indice crawler dal suggerimento USE INDEX(crawler). Il mio problema ora è più timeout di lockwait (deadlock sono scomparsi).

EDIT 2012-12-31: EXPLAIN con USE INDEX() mostra ora (non di righe è più alto, perché la tabella contiene più dati ora.):

id select_type table   type possible_keys key  key_len ref    rows  Extra 
1 SIMPLE  crawler_queue ref proc_id,crawler crawler 5  const,const,const 5472426 Using where 

risposta

0

Una soluzione migliore sarebbe quella di fare l'aggiornamento e saltare la selezione interamente. Quindi è possibile utilizzare last_insert_id() per raccogliere l'elemento aggiornato. Questo dovrebbe consentire di saltare completamente il blocco, mentre si esegue l'aggiornamento allo stesso tempo. Una volta che il record è stato aggiornato, puoi iniziare a processarlo, poiché non sarà mai più selezionato dalla stessa identica query, considerando che non tutte le condizioni iniziali corrispondono più.

Penso che questo dovrebbe aiutarti ad eliminare tutti i problemi relativi al blocco e dovrebbe consentire di eseguire tutti i processi che vuoi in parallelo.

PS: Giusto per chiarire, sto parlando di update ... limit 1 per assicurarsi di aggiornare solo una riga.

EDIT: Solution

è quella corretta, come indicato di seguito.

+1

Bella idea, ma 'LAST_INSERT_ID()' restituirà un valore solo se i dati 'INSERT' o' UPDATE' incrementano la colonna autoincrement: ** EDIT ** Darò http://stackoverflow.com/questions/ 1388025/how-to-get-id-of-the-last-updated-row-in-mysql a prova – rabudde

+0

Per qualche motivo ho ottenuto un valore last_insert_id quando ho provato, ma mi ha ingannato (sembrava quello giusto, ma non lo era). Credo che la soluzione descritta in questa domanda sia la strada da seguire. Aggiornerò anche la mia risposta – Xnoise

0

Da quello che posso dire al problema che si Stai affrontando è che due thread sono vyying per la stessa riga nella tabella e entrambi non possono averlo. Ma non esiste un modo elegante per il database di dire "no non si può avere quello, trovare un'altra riga" e quindi si ottengono errori. Questo è chiamato conflitto di risorse.

Quando si esegue un lavoro altamente parallelo come questo, uno dei modi più semplici per ridurre i problemi basati sui conflitti è eliminare completamente la contesa inventando un modo per tutti i thread per sapere su quali righe devono lavorare in anticipo. Quindi possono bloccare senza dover lottare per le risorse e il database non deve risolvere il conflitto.

Il modo migliore per farlo? Di solito le persone scelgono una sorta di schema id filo e usano l'aritmetica modulo per determinare quali thread ottengono quali file. Se 10 thread, il thread 0 ottiene la riga 0, 10, 20, 30, ecc. Il thread 1 ottiene 1, 11, 21, 31, ecc.

In generale se si dispone di NUM_THREADS, ciascuno dei thread verrebbe selezionato ID che sono THREAD_ID + i * NUM_THREADS dal database e funzionano su quelli.

Abbiamo introdotto un problema in quanto i thread potrebbero bloccarsi o morire e si potrebbero finire con le righe nel database che non vengono mai toccate.Esistono diverse soluzioni a questo problema, una delle quali è eseguire una "pulizia" una volta che la maggior parte/tutti i thread sono finiti dove tutti i thread catturano frammentariamente le righe che possono e li sottopongono a scansione fino a quando non rimangono URL non scansionati. Potresti diventare sempre più sofisticato e avere sempre qualche thread di pulizia in esecuzione, o fare ogni thread in modo occasionale, ecc.

3

Il tuo rapporto EXPLAIN mostra che stai utilizzando solo l'indice a colonna singola proc_id e che la query ha per esaminare oltre 600K righe. Probabilmente sarebbe meglio se l'ottimizzatore avesse scelto l'indice crawler.

InnoDB può bloccare tutte le righe 600K, non solo le righe che corrispondono alla condizione completa nella clausola WHERE. InnoDB blocca tutte le righe esaminate per assicurarsi che le modifiche simultanee non vengano scritte nel binlog nell'ordine sbagliato.

La soluzione è utilizzare un indice per restringere l'intervallo di righe esaminate. Questo probabilmente ti aiuterà non solo a trovare le righe più velocemente, ma anche a evitare di bloccare ampi intervalli di righe. L'indice crawler dovrebbe aiutare qui, ma non è immediatamente chiaro il motivo per cui non sta usando quell'indice.

Potrebbe essere necessario ANALYZE TABLE fare in modo di aggiornare le statistiche di tabella di InnoDB di conoscere l'indice crawler prima che utilizza tale indice nel piano di ottimizzazione. ANALYZE TABLE è un'operazione economica.

L'altra opzione è quella di utilizzare un hint di indice:

SELECT * FROM crawler_queue USE INDEX(crawler) ... 

Questo dice l'ottimizzatore di utilizzare tale indice, e non prendere in considerazione altri indici per la query. Preferisco evitare i suggerimenti sull'indice, perché l'ottimizzatore di solito è in grado di prendere buone decisioni da solo, e l'utilizzo del suggerimento nel codice significa che potrei costringere l'ottimizzatore a non considerare un indice che creo in futuro, che altrimenti sceglierebbe .


Con ulteriori spiegazioni, ora è chiaro che stai utilizzando il tuo RDBMS come FIFO. Questo non è un uso efficiente di un RDBMS. Esistono tecnologie di code di messaggi per questo scopo.

Consulta anche:

+0

Ehi Bill, questo è quello che ho fatto ancora (scusa se non aggiorno la mia domanda, darò +1 anche se). Ma la cosa strana è che, a volte, l'interpretazione ha mostrato l'uso di 'crawler' invece di' proc_id'. Ma per ora impongo l'uso di index 'crawler'. Darò anche il comando della tabella di analisi. Grazie – rabudde

+0

Controlla il campo 'rows' nell'output di EXPLAIN. Con l'indice composto, il numero di righe esaminate dovrebbe essere inferiore, spero. –

+0

No, non lo è (vedi sopra) – rabudde