T-SQL è una query sub per una limitazione di aggiornamento atomica con l'aggiornamento?
-
28-09-2019 - |
Domanda
Ho una semplice implementazione di coda in MS SQL Server 2008 R2. Ecco l'essenza della coda:
CREATE TABLE ToBeProcessed
(
Id BIGINT IDENTITY(1,1) PRIMARY KEY NOT NULL,
[Priority] INT DEFAULT(100) NOT NULL,
IsBeingProcessed BIT default (0) NOT NULL,
SomeData nvarchar(MAX) NOT null
)
voglio selezionare atomicamente all'inizio n righe in ordine di priorità e l'id dove IsBeingProcessed è falsa e aggiornare le righe per dire che sono trattati. Ho pensato di utilizzare una combinazione di aggiornamento, Top, uscita e ORDER BY, ma purtroppo non è possibile utilizzare parte superiore e l'ordine da parte in una dichiarazione di aggiornamento.
Così ho fatto un clausola per limitare l'aggiornamento e che lo fa domanda secondaria dell'ordine da parte (vedi sotto). La mia domanda è, è tutta questa affermazione atomica, o ho bisogno di avvolgerlo in una transazione?
DECLARE @numberToProcess INT = 2
CREATE TABLE #IdsToProcess
(
Id BIGINT NOT null
)
UPDATE
ToBeProcessed
SET
ToBeProcessed.IsBeingProcessed = 1
OUTPUT
INSERTED.Id
INTO
#IdsToProcess
WHERE
ToBeProcessed.Id IN
(
SELECT TOP(@numberToProcess)
ToBeProcessed.Id
FROM
ToBeProcessed
WHERE
ToBeProcessed.IsBeingProcessed = 0
ORDER BY
ToBeProcessed.Id,
ToBeProcessed.Priority DESC)
SELECT
*
FROM
#IdsToProcess
DROP TABLE #IdsToProcess
Ecco qualche SQL per inserire alcune righe fittizi:
INSERT INTO ToBeProcessed (SomeData) VALUES (N'');
INSERT INTO ToBeProcessed (SomeData) VALUES (N'');
INSERT INTO ToBeProcessed (SomeData) VALUES (N'');
INSERT INTO ToBeProcessed (SomeData) VALUES (N'');
INSERT INTO ToBeProcessed (SomeData) VALUES (N'');
Soluzione
Se ho capito la motivazione per la domanda si vuole evitare la possibilità che due transazioni concorrenti potrebbero sia eseguire la query sub per ottenere le righe in alto N per elaborare quindi procedere per aggiornare le stesse righe?
In questo caso userei questo approccio.
;WITH cte As
(
SELECT TOP(@numberToProcess)
*
FROM
ToBeProcessed WITH(UPDLOCK,ROWLOCK,READPAST)
WHERE
ToBeProcessed.IsBeingProcessed = 0
ORDER BY
ToBeProcessed.Id,
ToBeProcessed.Priority DESC
)
UPDATE
cte
SET
IsBeingProcessed = 1
OUTPUT
INSERTED.Id
INTO
#IdsToProcess
ero un po 'prima incerto se SQL Server avrebbe preso serrature U
durante l'elaborazione la versione con la query sub bloccando così due transazioni concorrenti dalla lettura delle stesse righe TOP N
. Questo non sembra essere il caso.
Test Table
CREATE TABLE JobsToProcess
(
priority INT IDENTITY(1,1),
isprocessed BIT ,
number INT
)
INSERT INTO JobsToProcess
SELECT TOP (1000000) 0,0
FROM master..spt_values v1, master..spt_values v2
Test Script (Esegui in 2 sessioni simultanee SSMS)
BEGIN TRY
DECLARE @FinishedMessage VARBINARY (128) = CAST('TestFinished' AS VARBINARY (128))
DECLARE @SynchMessage VARBINARY (128) = CAST('TestSynchronising' AS VARBINARY (128))
SET CONTEXT_INFO @SynchMessage
DECLARE @OtherSpid int
WHILE(@OtherSpid IS NULL)
SELECT @OtherSpid=spid
FROM sys.sysprocesses
WHERE context_info=@SynchMessage and spid<>@@SPID
SELECT @OtherSpid
DECLARE @increment INT = @@spid
DECLARE @number INT = @increment
WHILE (@number = @increment AND NOT EXISTS(SELECT * FROM sys.sysprocesses WHERE context_info=@FinishedMessage))
UPDATE JobsToProcess
SET @number=number +=@increment,isprocessed=1
WHERE priority = (SELECT TOP 1 priority
FROM JobsToProcess
WHERE isprocessed=0
ORDER BY priority DESC)
SELECT *
FROM JobsToProcess
WHERE number not in (0,@OtherSpid,@@spid)
SET CONTEXT_INFO @FinishedMessage
END TRY
BEGIN CATCH
SET CONTEXT_INFO @FinishedMessage
SELECT ERROR_MESSAGE(), ERROR_NUMBER()
END CATCH
Quasi immediatamente esecuzione interrompe sia come transazioni simultanee aggiornare la stessa riga in modo serrature S
adottate pur identificando il TOP 1 priority
devono ottenere rilasciato prima che acquisisce una serratura U
poi le 2 operazioni procedere per ottenere il U
riga e serratura X
in sequenza.
Se un CI viene aggiunto ALTER TABLE JobsToProcess ADD PRIMARY KEY CLUSTERED (priority)
quindi deadlock si verifica quasi immediatamente, invece, come in questo caso il blocco di riga S
non viene rilasciato, una transazione acquisisce un blocco U
sulla riga e attese per convertirlo in un blocco e l'altro X
operazione è ancora in attesa di convertire il suo blocco S
ad un blocco U
.
Se la query di cui sopra è cambiato in uso MIN
piuttosto che TOP
WHERE priority = (SELECT MIN(priority)
FROM JobsToProcess
WHERE isprocessed=0
)
Poi SQL Server riesce ad eliminare completamente la query sub dal piano e prende U
blocca tutta la strada.
Altri suggerimenti
Ogni singola istruzione T-SQL è, secondo tutta la mia esperienza e tutta la documenation che abbia mai letto, dovrebbe essere atomica. Quello che ci hai è una singola istruzione T-SQL, ergo è dovrebbe essere atomica e non richiede dichiarazioni transazione esplicita. Ho usato questo preciso tipo di logica molte volte, e mai avuto un problema con esso. Non vedo l'ora di vedere se qualcuno come un parere alternativo supportabile.
Per inciso, sguardo nelle funzioni ranking, in particolare row_number (), per il recupero di un determinato numero di elementi. La sintassi è forse un po 'imbarazzante, ma nel complesso si tratta di strumenti flessibili e potenti. (Ci sono circa un bazillion domande Pila Overlow e risposte che li trattano.)