Étant donné qu'une table agit en tant que file d'attente, comment puis-je configurer au mieux la table/les requêtes de sorte que plusieurs clients traitent simultanément de la file d'attente?
Par exemple, le tableau ci-dessous indique une commande qu'un opérateur doit traiter. Lorsque le programme de travail est terminé, la valeur traitée est définie sur true.
| ID | COMMAND | PROCESSED |
| 1 | ... | true |
| 2 | ... | false |
| 3 | ... | false |
Les clients peuvent obtenir une commande sur laquelle travailler:
select top 1 COMMAND
from EXAMPLE_TABLE
with (UPDLOCK, ROWLOCK)
where PROCESSED=false;
Cependant, s'il y a plusieurs travailleurs, chacun essaie d'obtenir la ligne avec ID = 2. Seul le premier aura le verrou pessimiste, le reste attendra. Ensuite, l'un d'eux recevra la rangée 3, etc.
Quelle requête/configuration permettrait à chaque client utilisateur d’obtenir une ligne différente et d’y travailler simultanément?
MODIFIER:
Plusieurs réponses suggèrent des variantes sur l'utilisation de la table elle-même pour enregistrer un état en cours de traitement. Je pensais que cela ne serait pas possible en une seule transaction. (c’est-à-dire, à quoi sert-il de mettre à jour l’état si aucun autre travailleur ne le verra jusqu’à ce que le txn soit validé?) La suggestion est peut-être:
# start transaction
update to 'processing'
# end transaction
# start transaction
process the command
update to 'processed'
# end transaction
Est-ce ainsi que les gens abordent généralement ce problème? Il me semble que le problème serait mieux traité par la DB, si possible.
Je vous recommande de passer en revue Utilisation des tables en tant que files d'attente . Des files d'attente correctement implémentées peuvent gérer des milliers d'utilisateurs simultanés et des services pouvant aller jusqu'à 1/2 million d'opérations de mise en file d'attente/de mise en file d'attente par minute. Avant SQL Server 2005, la solution était lourde et impliquait de mélanger une SELECT
et une UPDATE
dans une transaction unique et de donner la bonne combinaison d'indices de verrouillage, comme dans l'article lié par gbn. Heureusement depuis SQL Server 2005 avec l'avènement de la clause OUTPUT, une solution beaucoup plus élégante est disponible, et MSDN recommande désormais d'utiliser la clause OUTPUT :
Vous pouvez utiliser OUTPUT dans les applications Qui utilisent des tableaux comme files d'attente ou pour conserver Jeux de résultats intermédiaires. En d’autres termes, l’application Ajoute constamment ou Supprime des lignes de la table.
Fondamentalement, le puzzle doit comporter trois parties que vous devez résoudre correctement pour que cela fonctionne de manière hautement concurrente:
1) Vous devez retirer la file d'attente atomiquement. Vous devez trouver la ligne, ignorer toutes les lignes verrouillées et la marquer comme 'retirée de la file d'attente' dans une seule opération atomique, et c'est ici que la clause OUTPUT
entre en jeu:
with CTE as (
SELECT TOP(1) COMMAND, PROCESSED
FROM TABLE WITH (READPAST)
WHERE PROCESSED = 0)
UPDATE CTE
SET PROCESSED = 1
OUTPUT INSERTED.*;
2) Vous devez / structurez votre table avec la clé d’index clusterisée la plus à gauche de la colonne PROCESSED
. Si la ID
a été utilisée comme clé primaire, déplacez-la comme deuxième colonne de la clé en cluster. Le débat sur l'opportunité de conserver une clé non clusterisée dans la colonne ID
est ouvert, mais je suis fortement en faveur de pas d'avoir des index secondaires non clusterisés sur les files d'attente:
CREATE CLUSTERED INDEX cdxTable on TABLE(PROCESSED, ID);
3) Vous ne devez pas interroger cette table par un autre moyen que par Dequeue. Essayer d'effectuer des opérations Peek ou d'utiliser la table à la fois comme une file d'attente et, car un magasin va très probablement conduire à des blocages et ralentir considérablement le débit.
La combinaison de la file d'attente atomique, READPAST permet de rechercher des éléments à retirer de la file d'attente et la clé la plus à gauche sur l'index clusterisé en fonction du bit de traitement assure un débit très élevé sous une charge hautement concurrente.
Ma réponse ici montre comment utiliser des tables en tant que files d'attente ... Condition de concurrence de la file d'attente de processus SQL Server
Vous avez essentiellement besoin d’allusions "ROWLOCK, READPAST, UPDLOCK"
Si vous souhaitez sérialiser vos opérations pour plusieurs clients, vous pouvez simplement utiliser des verrous d'application.
BEGIN TRANSACTION
EXEC sp_getapplock @resource = 'app_token', @lockMode = 'Exclusive'
-- perform operation
EXEC sp_releaseapplock @resource = 'app_token'
COMMIT TRANSACTION
Une solution consiste à marquer la ligne avec une seule instruction de mise à jour. Si vous lisez le statut dans la clause where
et le modifiez dans la clause set
, aucun autre processus ne peut s'interposer, car la ligne sera verrouillée. Par exemple:
declare @pickup_id int
set @pickup_id = 1
set rowcount 1
update YourTable
set status = 'picked up'
, @pickup_id = id
where status = 'new'
set rowcount 0
return @pickup_id
Ceci utilise rowcount
pour mettre à jour une ligne au plus. Si aucune ligne n'a été trouvée, @pickup_id
sera -1
.
Plutôt que d'utiliser une valeur booléenne pour Processed, vous pouvez utiliser un int pour définir l'état de la commande:
1 = not processed
2 = in progress
3 = complete
Chaque travailleur obtient alors la ligne suivante avec Processed = 1, met à jour Processed à 2 puis commence à travailler. Lorsque le travail complet est traité et mis à jour à 3. Cette approche permet également l'extension d'autres résultats traités. Par exemple, au lieu de simplement définir qu'un travailleur est complet, vous pouvez ajouter de nouveaux statuts pour "Terminé avec succès" et "Terminé avec des erreurs".
La meilleure option consistera probablement à utiliser une colonne traitée par trisSate avec une colonne version/timestamp. Les trois valeurs de la colonne traitée indiqueront ensuite si la ligne est en cours de traitement, traitée ou non traitée.
Par exemple
CREATE TABLE Queue ID INT NOT NULL PRIMARY KEY,
Command NVARCHAR(100),
Processed INT NOT NULL CHECK (Processed in (0,1,2) ),
Version timestamp)
Vous saisissez la première ligne non traitée, définissez le statut sur sous-traitement et redéfinissez le statut sur traité lorsque les tâches sont terminées. Basez votre statut de mise à jour sur les colonnes Version et Clé primaire. Si la mise à jour échoue, quelqu'un y est déjà allé.
Vous voudrez peut-être également ajouter un identificateur de client. Ainsi, si le client décède pendant son traitement, il peut redémarrer, consulter la dernière ligne, puis recommencer à l’origine.
Je resterais loin de jouer avec les serrures d'une table. Créez simplement deux colonnes supplémentaires, telles que IsProcessing (bit/boolean) et ProcessingStarted (datetime). Lorsqu'un travailleur se bloque ou ne met pas à jour sa ligne après un délai d'attente, vous pouvez faire en sorte qu'un autre travailleur tente de traiter les données.