Stratégies de «vérification» des enregistrements pour le traitement

10

Je ne sais pas s'il y a un modèle nommé pour cela, ou s'il n'y en a pas parce que c'est une idée terrible. Mais j'ai besoin de mon service pour fonctionner dans un environnement à charge équilibrée actif / actif. Il s'agit uniquement du serveur d'application. La base de données sera sur un serveur distinct. J'ai un service qui devra exécuter un processus pour chaque enregistrement d'une table. Ce processus peut prendre une minute ou deux et se répétera toutes les n minutes (configurable, généralement 15 minutes).

Avec une table de 1 000 enregistrements nécessitant ce traitement et deux services fonctionnant avec ce même ensemble de données, j'aimerais que chaque service "extrait" un enregistrement à traiter. Je dois m'assurer qu'un seul service / thread traite chaque enregistrement à la fois.

J'ai des collègues qui ont utilisé une «table de verrouillage» dans le passé. Où un enregistrement est écrit dans cette table pour verrouiller logiquement l'enregistrement dans l'autre table (cette autre table est assez statique et avec un nouvel enregistrement très occasionnel ajouté), puis supprimé pour libérer le verrou.

Je me demande si ce ne serait pas mieux pour la nouvelle table d'avoir une colonne qui indique quand elle a été verrouillée, et qu'elle est actuellement verrouillée, au lieu d'insérer une suppression en permanence.

Quelqu'un at-il des conseils pour ce genre de chose? Existe-t-il un modèle établi pour le verrouillage logique à long terme (ish)? Des conseils sur la façon de garantir qu'un seul service attrape le verrou à la fois? (Mon collègue utilise TABLOCKX pour verrouiller la table entière.)

doyen
la source

Réponses:

12

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:

  1. Terminé (ou même "En attente" dans ce cas car les deux signifient "prêt à être traité")
  2. En cours (ou "en cours de traitement")
  3. 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 OUTPUTclause 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 UPDATEopé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 ):

  • READPAST (semble correspondre à ce scénario exact)

    Spécifie que le moteur de base de données ne lit pas les lignes verrouillées par d'autres transactions. Lorsque READPAST est spécifié, les verrous au niveau des lignes sont ignorés. Autrement dit, le moteur de base de données ignore les lignes au lieu de bloquer la transaction en cours jusqu'à ce que les verrous soient libérés ... READPAST est principalement utilisé pour réduire les conflits de verrouillage lors de l'implémentation d'une file d'attente de travail qui utilise une table SQL Server. Un lecteur de file d'attente qui utilise READPAST ignore les entrées de file d'attente verrouillées par d'autres transactions à l'entrée de file d'attente disponible suivante, sans avoir à attendre que les autres transactions libèrent leurs verrous.

  • ROWLOCK (juste pour être sûr)

    Spécifie que les verrous de ligne sont pris lorsque les verrous de page ou de table sont généralement pris.

  • UPDLOCK

    Spécifie que les verrous de mise à jour doivent être pris et maintenus jusqu'à la fin de la transaction. UPDLOCK prend les verrous de mise à jour pour les opérations de lecture uniquement au niveau de la ligne ou de la page.

Solomon Rutzky
la source
1

A fait la même chose (sans applications, uniquement dans la base de données) en utilisant les files d'attente Service Broker. Léger, entièrement conforme à ACID, peut être étendu presque à l'infini. Le verrouillage transparent des lignes (ou "masquage", plutôt) est intégré. Disponible à partir de la version 2005 et plus.

Dans votre cas, l'architecture globale pourrait être la suivante: certains processus envoient des messages dans les boîtes de dialogue Service Broker, selon leurs planifications, et des écouteurs les récupèrent dans la file d'attente du côté cible. Outre la création de types de messages distincts, vous pouvez inclure à peu près n'importe quoi dans le corps du message - le délai d'expiration, par exemple, et tous les paramètres que la tâche peut avoir.

Ce n'est pas la chose la plus facile à saisir, c'est sûr, mais une fois que vous l'aurez, ses avantages deviendront apparents.

Roger Wolf
la source