Je ne suis pas un grand fan de la table supplémentaire "lock" ou de l'idée de verrouiller la table entière pour saisir le prochain record. Je comprends pourquoi cela est fait, mais cela nuit également à la concurrence pour les opérations qui se mettent à jour pour libérer un enregistrement verrouillé (deux processus ne peuvent sûrement pas se battre pour cela quand il n'est pas possible pour deux processus d'avoir verrouillé le même enregistrement au en même temps).
Ma préférence serait d'ajouter une colonne ProcessStatusID (généralement TINYINT) à la table avec les données en cours de traitement. Et existe-t-il un champ pour LastModifiedDate? Sinon, alors il devrait être ajouté. Si oui, ces enregistrements sont-ils mis à jour en dehors de ce traitement? Si les enregistrements peuvent être mis à jour en dehors de ce processus particulier, un autre champ doit être ajouté pour suivre StatusModifiedDate (ou quelque chose comme ça). Pour le reste de cette réponse, je vais simplement utiliser "StatusModifiedDate" comme il est clair dans sa signification (et en fait, pourrait être utilisé comme nom de champ même s'il n'y a actuellement aucun champ "LastModifiedDate").
Les valeurs de ProcessStatusID (qui doivent être placées dans une nouvelle table de recherche appelée "ProcessStatus" et à clé étrangère dans cette table) peuvent être:
- Terminé (ou même "En attente" dans ce cas car les deux signifient "prêt à être traité")
- En cours (ou "en cours de traitement")
- Erreur (ou "WTF?")
À ce stade, il semble sûr de supposer qu'à partir de l'application, il veut juste récupérer le prochain enregistrement à traiter et ne transmettra rien pour aider à prendre cette décision. Nous voulons donc récupérer l'enregistrement le plus ancien (au moins en termes de StatusModifiedDate) défini sur "Terminé" / "En attente". Quelque chose dans le sens de:
SELECT TOP 1 pt.RecordID
FROM ProcessTable pt
WHERE pt.StatusID = 1
ORDER BY pt.StatusModifiedDate ASC;
Nous souhaitons également mettre à jour cet enregistrement vers "En cours" en même temps pour empêcher l'autre processus de le saisir. Nous pourrions utiliser la OUTPUT
clause pour nous permettre de faire les UPDATE et SELECT dans la même transaction:
UPDATE TOP (1) pt
SET pt.StatusID = 2,
pt.StatusModifiedDate = GETDATE() -- or GETUTCDATE()
OUTPUT INSERTED.RecordID
FROM ProcessTable pt
WHERE pt.StatusID = 1;
Le principal problème ici est que même si nous pouvons faire TOP (1)
une UPDATE
opération, il n'y a aucun moyen de faire une opération ORDER BY
. Mais, nous pouvons l'envelopper dans un CTE pour combiner ces deux concepts:
;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 question évidente est de savoir si deux processus faisant le SELECT en même temps peuvent récupérer le même enregistrement. Je suis à peu près sûr que la clause UPDATE with OUTPUT, spécialement combinée avec les indications READPAST et UPDLOCK (voir ci-dessous pour plus de détails), conviendra. Cependant, je n'ai pas testé ce scénario exact. Si, pour une raison quelconque, la requête ci-dessus ne prend pas en compte la condition de concurrence, l'ajout de la volonté suivante: verrous d'application.
La requête CTE ci-dessus peut être encapsulée dans sp_getapplock et sp_releaseapplock pour créer un "gardien de porte" pour le processus. Ce faisant, un seul processus à la fois pourra entrer pour exécuter la requête ci-dessus. Les autres processus seront bloqués jusqu'à ce que le processus avec l'applock le libère. Et puisque cette étape du processus global consiste simplement à saisir le RecordID, elle est assez rapide et ne bloquera pas les autres processus très longtemps. Et, tout comme pour la requête CTE, nous ne bloquons pas la totalité de la table, permettant ainsi à d'autres mises à jour d'autres lignes (de définir leur statut sur "Terminé" ou "Erreur"). Essentiellement:
BEGIN TRANSACTION;
EXEC sp_getapplock @Resource = 'GetNextRecordToProcess', @LockMode = 'Exclusive';
{CTE UPDATE query shown above}
EXEC sp_releaseapplock @Resource = 'GetNextRecordToProcess';
COMMIT TRANSACTION;
Les verrous d'application sont très agréables mais doivent être utilisés avec parcimonie.
Enfin, vous avez juste besoin d'une procédure stockée pour gérer la définition de l'état sur "Terminé" ou "Erreur". Et cela peut être simple:
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;
Conseils de table (trouvés dans Hints (Transact-SQL) - Table ):