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:
- Completato (o anche "In sospeso" in questo caso poiché entrambi significano "pronto per essere elaborato")
- In elaborazione (o "Elaborazione")
- 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 OUTPUT
clausola 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 ):