2014-12-13 17 views
17

Ho uno script PHP che recupera le righe da un database e quindi esegue il lavoro in base ai contenuti. Il lavoro può essere dispendioso in termini di tempo (ma non necessariamente costoso da un punto di vista computazionale) e quindi è necessario consentire l'esecuzione di più script in parallelo.Implementazione di una coda semplice con PHP e MySQL?

Le righe nel database sembra qualcosa di simile:

+---------------------+---------------+------+-----+---------------------+----------------+ 
| Field    | Type   | Null | Key | Default    | Extra   | 
+---------------------+---------------+------+-----+---------------------+----------------+ 
| id     | bigint(11) | NO | PRI | NULL    | auto_increment | 
..... 
| date_update_started | datetime  | NO |  | 0000-00-00 00:00:00 |    | 
| date_last_updated | datetime  | NO |  | 0000-00-00 00:00:00 |    | 
+---------------------+---------------+------+-----+---------------------+----------------+ 

Il mio script attualmente seleziona le righe con le date più antiche date_last_updated (che viene aggiornato una volta il lavoro è fatto) e non fanno uso di date_update_started.

Se dovessi eseguire più istanze dello script in parallelo adesso, selezionerebbero le stesse righe (almeno una parte del tempo) e il lavoro duplicato sarebbe fatto.

Quello che sto pensando di fare è utilizzare una transazione per selezionare le righe, aggiornare la colonna date_update_started, e quindi aggiungere una condizione WHERE per l'istruzione SQL selezionando il file per selezionare solo le righe con date_update_started maggiore di un certo valore (a assicurati che un altro script non funzioni su di esso). Per esempio.

$sth = $dbh->prepare(' 
    START TRANSACTION; 
    SELECT * FROM table WHERE date_update_started > 1 DAY ORDER BY date_last_updated LIMIT 1000; 
    UPDATE table DAY SET date_update_started = UTC_TIMESTAMP() WHERE id IN (SELECT id FROM table WHERE date_update_started > 1 DAY ORDER BY date_last_updated LIMIT 1000;); 
    COMMIT; 
'); 
$sth->execute(); // in real code some values will be bound 
$rows = $sth->fetchAll(PDO::FETCH_ASSOC); 

Da quello che ho letto, questa è essenzialmente un'implementazione della coda e sembra essere disapprovata in MySQL. Allo stesso tempo, ho bisogno di trovare un modo per consentire l'esecuzione di più script in parallelo, e dopo la ricerca che ho fatto questo è quello che ho trovato.

Questo tipo di approccio funzionerà? C'è un modo migliore?

+0

Come si fa a eseguire gli script in parallelo? – Lupin

+0

@Lupin Attualmente lo script viene eseguito ogni 15 minuti tramite un cron job. Lo script verifica se un'altra istanza è in esecuzione e, in tal caso, termina. Non sono sicuro di come gestirò più script in esecuzione - potrei avere un contatore in un database per vedere quanti sono in esecuzione e limitare il numero di istanze in quel modo, ma un problema alla volta :-) – Nate

+0

OK , alcune domande aggiuntive per me per comprendere appieno: 1. si dispone di uno script che seleziona le righe e lavorare su di essi e quindi aggiornare di nuovo al DB, giusto? 2. Vuoi la possibilità di avere script paralleli in esecuzione e facendo lo stesso, ma su righe diverse, giusto? 3. Ogni volta che lo script viene eseguito, le righe selezionate sono continue, ovvero sono 1-100, 101-200 ecc o sono casuali in termini di ID e selezionate solo da quelle che date_update_started è maggiore di 1? – Lupin

risposta

5

Penso che il tuo approccio potrebbe funzionare, purché tu aggiunga anche una sorta di identificatore alle righe che hai selezionato su cui sono state attualmente lavorate, potrebbe essere come suggerito da @JuniusRendel e vorrei anche pensare all'utilizzo di un'altra stringa chiave (id casuale o di istanza) per i casi in cui lo script ha provocato errori e non è stato completato con garbo, in quanto sarà necessario pulire questi campi una volta aggiornate le righe dopo il lavoro.

Il problema con questo approccio come vedo è l'opzione che ci saranno 2 script eseguiti nello stesso punto e selezioneranno le stesse righe prima che fossero firmati come bloccati. qui come posso vederlo, dipende molto dal tipo di lavoro che fai sulle file, se il risultato finale in questi due script sarà lo stesso, penso che l'unico problema che hai sia per sprecare tempo e memoria del server (che non sono piccoli problemi ma li metterò da parte per ora ...). se il tuo lavoro comporterà diversi aggiornamenti su entrambi gli script, il tuo problema sarà che potresti avere l'aggiornamento sbagliato alla fine nella TB.

@Jean ha menzionato il secondo approccio che è possibile utilizzare per l'utilizzo dei blocchi MySql. non sono un esperto del tema ma sembra un buon approccio e usando la frase 'Select .... FOR UPDATE' potresti darti quello che stai cercando, come si potrebbe fare sulla stessa chiamata selezionare l'aggiornamento & - che sarà più veloce di 2 query separate e potrebbe ridurre il rischio che altre istanze possano selezionare queste righe poiché verranno bloccate.

Il 'SELEZIONA .... FOR UPDATE' consente di eseguire un'istruzione SELECT e bloccare le righe specifiche per l'aggiornamento loro, così la sua dichiarazione potrebbe assomigliare:

START TRANSACTION; 
    SELECT * FROM tb where field='value' LIMIT 1000 FOR UPDATE; 
    UPDATE tb SET lock_field='1' WHERE field='value' LIMIT 1000; 
COMMIT; 

serrature sono potenti, ma fai attenzione a non influenzare la tua applicazione in diverse sezioni. Controlla se quelle file selezionate che sono attualmente bloccate per l'aggiornamento, sono richieste da qualche altra parte nella tua applicazione (forse per l'utente finale) e cosa succederà in quel caso.

Inoltre, le tabelle devono essere InnoDB e si consiglia che i campi in cui si verifica la clausola where abbiano un indice Mysql come se non fosse possibile bloccare l'intera tabella o incontrare lo 'Gap Lock'.

Esiste anche la possibilità che il processo di blocco e in particolare quando si eseguono script paralleli sia pesante sulla memoria della CPU &.

qui è un'altra lettura sul tema: http://www.percona.com/blog/2006/08/06/select-lock-in-share-mode-and-for-update/

Spero che questo aiuti, e vorrebbe sapere come si progredito.

1

Edit: Scusa, ho completamente frainteso la tua domanda

Si dovrebbe solo mettere una colonna "bloccato" sulla vostra tavola mettere il valore su true sulle voci lo script sta lavorando con, e ha messo quando è fatto a falso.

Nel mio caso ho inserito 3 colonne di timestamp (integer): target_ts, start_ts, done_ts. È

UPDATE table SET locked = TRUE WHERE target_ts<=UNIX_TIMESTAMP() AND ISNULL(done_ts) AND ISNULL(start_ts); 

e poi

SELECT * FROM table WHERE target_ts<=UNIX_TIMESTAMP() AND ISNULL(start_ts) AND locked=TRUE; 

fare il vostro lavoro e aggiornare ogni voce uno alla volta (per evitare inconcistencies dati) impostando la proprietà done_ts al timestamp corrente (si può anche sbloccare loro ora). Puoi aggiornare target_ts al prossimo aggiornamento che desideri o puoi ignorare questa colonna e usare solo done_ts per la tua selezione

+0

Non credo che PHP supporti effettivamente il multithreading, ma in ogni caso ottenere più istanze dello script da eseguire non è il problema. La domanda è principalmente come gestire il recupero delle righe dal DB. – Nate

+0

Ho aggiornato, mi dispiace forse ero ubriaco :). Per i thread, non lo so, questo è quanto sostiene l'estensione PECL, ma non l'ho provato, quindi ... – n00dl3

4

Abbiamo qualcosa di simile implementato in produzione.

Per evitare duplicati, facciamo un UPDATE MySQL come questo (ho modificato la query per assomigliare il vostro tavolo):

UPDATE queue SET id = LAST_INSERT_ID(id), date_update_started = ... 
WHERE date_update_started IS NULL AND ... 
LIMIT 1; 

Facciamo questo UPDATE in una singola transazione, e noi sfruttare la funzione di LAST_INSERT_ID. Se usato in questo modo, con un parametro, scrive nella sessione di transazione il parametro che, in questo caso, è l'ID della coda singola (LIMIT 1) che è stata aggiornata (se ce n'è una).

Subito dopo che, lo facciamo:

SELECT LAST_INSERT_ID(); 

Quando utilizzato senza parametri, si recupera il valore precedentemente memorizzato, ottenendo ID dell'elemento coda che deve essere eseguita.

+0

Puoi approfondire cosa intendi per "scrivere serrature"? Forse con un esempio di codice? – Nate

+0

@Nate, modificato ed espanso;) Suggerisco anche di usare RabbitMQ, BTW. Stiamo sognando di usarlo: D – Jean

1

Ogni volta che si esegue lo script, avrei lo script generare un uniqid.

$sctiptInstance = uniqid(); 

Vorrei aggiungere una colonna di istanza di script per mantenere questo valore come varchar e inserire un indice su di esso. Quando lo script viene eseguito, utilizzerò select per l'aggiornamento all'interno di una transazione per selezionare le righe in base a qualsiasi logica, escludendo le righe con un'istanza di script e quindi aggiornarle con l'istanza di script.Qualcosa di simile:

START TRANSACTION; 
SELECT * FROM table WHERE script_instance = '' AND date_update_started > 1 DAY ORDER BY date_last_updated LIMIT 1000 FOR UPDATE; 
UPDATE table SET date_update_started = UTC_TIMESTAMP(), script_instance = '{$scriptInstance}' WHERE script_instance = '' AND date_update_started > 1 DAY ORDER BY date_last_updated LIMIT 1000; 
COMMIT; 

Ora quelle righe saranno esclusi da altre istanze dello script. Lavori, quindi aggiorna le righe per reimpostare l'istanza di script su null o vuoto e aggiornare anche la colonna dell'ultima data aggiornata.

È anche possibile utilizzare l'istanza di script per scrivere in un'altra tabella denominata "istanze correnti" o qualcosa di simile, e fare in modo che lo script controlli tale tabella per ottenere un conteggio degli script in esecuzione per controllare il numero di script simultanei. Aggiungerei anche il PID dello script al tavolo. È quindi possibile utilizzare tali informazioni per creare periodicamente uno script di gestione da eseguire da cron per verificare la presenza di processi lunghi o anomali e ucciderli, ecc.

1

Ho un sistema che funziona esattamente come questo in produzione. Eseguiamo uno script ogni minuto per eseguire alcune elaborazioni, ea volte questa esecuzione può richiedere più di un minuto.

Abbiamo una colonna della tabella per lo stato, che è 0 per NON CORRERE ANCORA, 1 per FINITO, e altro valore per in corso.

La prima cosa che lo script fa è aggiornare la tabella, impostando una linea o più righe con un valore che significa che stiamo lavorando su quella linea. Utilizziamo getmypid() per aggiornare le righe su cui vogliamo lavorare e che non sono ancora state elaborate.

Quando abbiamo finito il trattamento, lo script aggiorna le linee che hanno lo stesso ID di processo, segnando loro come finito (stato 1).

In questo modo evitiamo ciascuno degli script per cercare di elaborare una linea che è già in fase di elaborazione, e funziona come un fascino. Questo non significa che non ci sia un modo migliore, ma questo ha portato a termine il lavoro.

1

ho usato una stored procedure per ragioni molto simili in passato. Abbiamo usato il blocco di lettura FOR UPDATE per bloccare la tabella mentre un flag selezionato è stato aggiornato per rimuovere quella voce da qualsiasi selezione futura. Sembrava qualcosa di simile:

CREATE PROCEDURE `select_and_lock`() 
BEGIN 
    START TRANSACTION; 
    SELECT your_fields FROM a_table WHERE some_stuff=something 
    AND selected = 0 FOR UPDATE; 
    UPDATE a_table SET selected = 1; 
    COMMIT; 
END$$ 

Non c'è ragione che deve essere fatto in una stored procedure anche se ora ci penso.