Strategie per il "check out" dei record per l'elaborazione


10

Non sono sicuro se esiste un modello chiamato per questo, o se non lo è perché è un'idea terribile. Ma ho bisogno del mio servizio per operare in un ambiente bilanciato carico attivo / attivo. Questo è solo il server delle applicazioni. Il database sarà su un server separato. Ho un servizio che dovrà eseguire un processo per ogni record in una tabella. Questo processo può richiedere un minuto o due e si ripeterà ogni n minuti (configurabile, in genere 15 minuti).

Con una tabella di 1000 record che richiedono questa elaborazione e due servizi in esecuzione su questo stesso set di dati, vorrei che ogni servizio "esegua il checkout" di un record da elaborare. Devo assicurarmi che un solo servizio / thread stia elaborando ogni record alla volta.

Ho colleghi che hanno usato una "tabella di blocco" in passato. Dove un record viene scritto in questa tabella per bloccare logicamente il record nell'altra tabella (quell'altra tabella è piuttosto statica tra l'altro e con un nuovo record molto occasionale aggiunto), e quindi cancellata per rilasciare il blocco.

Mi chiedo se non sarebbe meglio per la nuova tabella avere una colonna che indica quando è stata bloccata e che è attualmente bloccata, invece di inserire una cancellazione costantemente.

Qualcuno ha un suggerimento per questo tipo di cose? Esiste un modello stabilito per il blocco logico a lungo termine (ish)? Qualche consiglio su come garantire che un solo servizio afferri il lucchetto alla volta? (Il mio collega utilizza TABLOCKX per bloccare l'intero tavolo.)

Risposte:


12

Non sono un grande fan né del tavolo extra "lock" né dell'idea di bloccare l'intero tavolo per ottenere il record successivo. Capisco perché è stato fatto, ma questo danneggia anche la concorrenza per le operazioni che si stanno aggiornando per rilasciare un record bloccato (sicuramente due processi non possono essere in conflitto con questo quando non è possibile che due processi abbiano bloccato lo stesso record sul contemporaneamente).

La mia preferenza sarebbe quella di aggiungere una colonna ProcessStatusID (in genere TINYINT) alla tabella con i dati in elaborazione. E c'è un campo per LastModifiedDate? In caso contrario, dovrebbe essere aggiunto. Se sì, questi record vengono aggiornati al di fuori di questa elaborazione? Se i record possono essere aggiornati al di fuori di questo particolare processo, è necessario aggiungere un altro campo per tenere traccia di StatusModifiedDate (o qualcosa del genere). Per il resto di questa risposta userò semplicemente "StatusModifiedDate" in quanto è chiaro nel suo significato (e in effetti, potrebbe essere usato come nome del campo anche se attualmente non esiste un campo "LastModifiedDate").

I valori per ProcessStatusID (che dovrebbe essere inserito in una nuova tabella di ricerca chiamata "ProcessStatus" e con chiave esterna in questa tabella) potrebbero essere:

  1. Completato (o anche "In sospeso" in questo caso poiché entrambi significano "pronto per essere elaborato")
  2. In elaborazione (o "Elaborazione")
  3. Errore (o "WTF?")

A questo punto sembra sicuro supporre che dall'applicazione, vuole solo prendere il prossimo record da elaborare e non passerà nulla per aiutare a prendere quella decisione. Quindi vogliamo prendere il record più vecchio (almeno in termini di StatusModifiedDate) impostato su "Completato" / "In sospeso". Qualcosa sulla falsariga di:

SELECT TOP 1 pt.RecordID
FROM   ProcessTable pt
WHERE  pt.StatusID = 1
ORDER BY pt.StatusModifiedDate ASC;

Vogliamo anche aggiornare quel record su "In elaborazione" allo stesso tempo per evitare che l'altro processo lo afferri. Potremmo utilizzare la OUTPUTclausola per consentirci di eseguire AGGIORNAMENTO e SELEZIONARE nella stessa transazione:

UPDATE TOP (1) pt
SET    pt.StatusID = 2,
       pt.StatusModifiedDate = GETDATE() -- or GETUTCDATE()
OUTPUT INSERTED.RecordID
FROM   ProcessTable pt
WHERE  pt.StatusID = 1;

Il problema principale qui è che mentre possiamo fare un'operazione TOP (1)in UPDATE, non c'è modo di farlo ORDER BY. Tuttavia, possiamo inserirlo in un CTE per combinare questi due concetti:

;WITH cte AS
(
   SELECT TOP 1 pt.RecordID
   FROM   ProcessTable pt (READPAST, ROWLOCK, UPDLOCK)
   WHERE  pt.StatusID = 1
   ORDER BY pt.StatusModifiedDate ASC;
)
UPDATE cte
SET    cte.StatusID = 2,
       cte.StatusModifiedDate = GETDATE() -- or GETUTCDATE()
OUTPUT INSERTED.RecordID;

La domanda ovvia è se due processi che eseguono SELECT contemporaneamente o meno possono ottenere lo stesso record. Sono abbastanza sicuro che la clausola UPDATE con OUTPUT, specialmente combinata con i suggerimenti READPAST e UPDLOCK (vedi sotto per maggiori dettagli), andrà bene. Tuttavia, non ho testato questo scenario esatto. Se per qualche motivo la query di cui sopra non si occupa delle condizioni di gara, l'aggiunta della seguente volontà: blocchi dell'applicazione.

La query CTE sopra può essere racchiusa in sp_getapplock e sp_releaseapplock per creare un "gate keeper" per il processo. In tal modo, solo un processo alla volta sarà in grado di entrare per eseguire la query sopra. Gli altri processi verranno bloccati fino a quando il processo con l'applock non lo rilascia. E poiché questo passaggio dell'intero processo è solo quello di afferrare RecordID, è abbastanza veloce e non bloccherà gli altri processi per molto tempo. E, proprio come con la query CTE, stiamo non bloccando l'intera tabella, consentendo in tal modo altri aggiornamenti di altre righe (per impostare il loro stato a uno "Completato" o "errore"). Essenzialmente:

BEGIN TRANSACTION;
EXEC sp_getapplock @Resource = 'GetNextRecordToProcess', @LockMode = 'Exclusive';

   {CTE UPDATE query shown above}

EXEC sp_releaseapplock @Resource = 'GetNextRecordToProcess';
COMMIT TRANSACTION;

I blocchi dell'applicazione sono molto belli ma dovrebbero essere usati con parsimonia.

Infine, è sufficiente una procedura memorizzata per gestire l'impostazione dello stato su "Completato" o "Errore". E quello può essere un semplice:

CREATE PROCEDURE ProcessTable_SetProcessStatusID
(
   @RecordID INT,
   @ProcessStatusID TINYINT
)
AS
SET NOCOUNT ON;

UPDATE pt
SET    pt.ProcessStatusID = @ProcessStatusID,
       pt.StatusModifiedDate = GETDATE() -- or GETUTCDATE()
FROM   ProcessTable pt
WHERE  pt.RecordID = @RecordID;

Suggerimenti per la tabella (disponibili in Suggerimenti (Transact-SQL) - Tabella ):

  • READPAST (sembra adattarsi a questo scenario esatto)

    Specifica che Motore di database non legge le righe bloccate da altre transazioni. Quando viene specificato READPAST, i blocchi a livello di riga vengono ignorati. Cioè, Motore di database salta le righe invece di bloccare la transazione corrente fino al rilascio dei blocchi ... READPAST viene utilizzato principalmente per ridurre la contesa di blocco durante l'implementazione di una coda di lavoro che utilizza una tabella di SQL Server. Un lettore di code che utilizza READPAST salta le voci della coda passate bloccate da altre transazioni alla successiva voce di coda disponibile, senza dover attendere che le altre transazioni rilascino i loro blocchi.

  • ROWLOCK (solo per sicurezza)

    Specifica che i blocchi di riga vengono eseguiti quando i blocchi di pagine o tabelle vengono normalmente eseguiti.

  • UPDLOCK

    Specifica che i blocchi di aggiornamento devono essere presi e mantenuti fino al completamento della transazione. UPDLOCK accetta i blocchi di aggiornamento per le operazioni di lettura solo a livello di riga o di pagina.


1

Ha fatto cose simili (senza applicazioni, puramente all'interno di DB) utilizzando le code di Service Broker. Leggero, completamente compatibile ACID, può essere ridimensionato quasi all'infinito. Il blocco delle righe trasparente (o "nascosto", piuttosto) è incorporato. Disponibile dalla versione 2005 in poi.

Nel tuo caso, l'architettura complessiva potrebbe essere così: alcuni processi inviano messaggi nelle finestre di dialogo di Service Broker, in base alle loro pianificazioni, e gli ascoltatori li raccolgono dalla coda sul lato target. Oltre a creare tipi di messaggi separati, è possibile includere praticamente qualsiasi cosa nel corpo del messaggio, ad esempio il timeout e qualsiasi parametro che l'attività potrebbe avere.

Non è la cosa più facile da capire, questo è certo, ma una volta che lo avrai, i suoi vantaggi diventeranno evidenti.

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.