2009-04-28 4 views
1

J'écris un démon pour surveiller la création de nouveaux objets, ce qui ajoute des lignes à une table de base de données quand il détecte de nouvelles choses. Nous appellerons les objets widgets. Le flux de code est à peu près ceci:en utilisant une transaction pour éviter une course

1: every so often: 
2: find newest N widgets (from external source) 
3: foreach widget 
4:  if(widget not yet in database) 
5:  add rows for widget 

Les deux dernières lignes présentent une condition de course, car si deux instances de ce démon sont en cours d'exécution en même temps, ils peuvent à la fois créer une ligne pour un widget X si les lignes de synchronisation en haut La solution la plus évidente consisterait à utiliser une contrainte unique dans la colonne d'identifiant du widget, mais cela n'est pas possible en raison de la disposition de la base de données (il est en effet possible d'avoir plusieurs lignes pour un widget, mais le daemon devrait ne le ferai jamais automatiquement). Ma prochaine pensée serait d'utiliser une transaction, puisque c'est ce à quoi ils sont destinés. Dans le monde ADO.NET, je crois que je voudrais un niveau d'isolement de Serializable, mais je ne suis pas positif. Quelqu'un peut me diriger dans la bonne direction?

Mise à jour: J'ai fait quelques expérimentations, et la transaction sérialisée ne semble pas résoudre le problème, ou du moins pas très bien. Le cas intéressant est décrit ci-dessous et suppose qu'une seule table est impliquée. Notez que je ne suis pas positif sur les détails de blocage, mais je pense que je l'ai droit:

Thread A: Executes line 4, acquiring a read lock on the table 
Thread B: Executes line 4, acquiring a read lock on the table 
Thread A: Tries to execute line 5, which requires upgrading to a write lock 
    (this requires waiting until Thread B unlocks the table) 
Thread B: Tries to execute line 5, again requiring a lock upgrade 
    (this requires waiting until Thread A unlocks) 

Cela nous laisse dans une situation de blocage classique. D'autres chemins de code sont possibles, mais si les threads A et B n'entrelacent pas, il n'y a aucun problème de synchronisation. Le résultat final est qu'une exception SqlException est lancée sur l'un des threads, après que SQL détecte le blocage et termine l'une des instructions. Je peux attraper cette exception et détecter le code d'erreur particulier, mais cela ne me semble pas très propre.

Une autre route que je peux prendre est de créer une deuxième table qui suit les widgets vus par le démon, où je peux utiliser une contrainte unique. Cela nécessite encore d'attraper et de détecter certains codes d'erreur (dans ce cas, les violations de la contrainte d'intégrité), donc je suis toujours intéressé par une meilleure solution, si quelqu'un peut y penser.

Répondre

2

En règle générale, vous devez toujours utiliser les transactions si vous avez plus d'un processus ou thread utilisant la base de données en même temps.

Le niveau d'isolement "sérialisable" devrait fonctionner. Il ne permet pas que les données lues à partir d'une transaction soient modifiées par une autre. Mais il se verrouille beaucoup et ne devrait généralement pas être utilisé, car il ralentit l'application et il y a un risque plus élevé de verrous morts.

Alternatives:

  • vous ne pouvez verrouiller toute la table pour faire en sorte que personne n'écrit en pendant que vous vérifiez si quelque chose est (pas) là-bas. Le problème est que seul un utilisateur peut écrire sur la table en même temps, ce qui ralentit considérablement les choses.(Vous pouvez rechercher les données, si elle est pas là, verrouiller la table et recherche de nouveau avant d'insérer. Ceci est un modèle commun.)
  • Honnêtement, vous devriez penser au fait que deux transactions tentent de insérer la même rangée en même temps. C'est probablement là que vous devriez commencer à le résoudre. Un seul démon devrait être responsable des mêmes données.
    • font tous un démon qui gère ses propres données
    • font tous les démons appeler un service au lieu de la base de données. Là vous pouvez mettre les choses ensemble avant de l'insérer.

Par ailleurs, vous avez besoin d'un identifiant unique pour les données pour identifier clairement de toute façon. Comment pouvez-vous le rechercher dans la base de données si vous n'avez pas d'identifiant unique?

+0

Merci pour les idées. Malheureusement, je pense que cela ne résoudra pas le problème (je vais l'expliquer dans la question initiale). – Charlie

+0

D'un point de vue de la cohérence, cela fonctionne :-) mais bien sûr ce n'est pas bon si cela aboutit à des verrous morts. J'ai peur si je ne peux pas aider beaucoup, mais j'ajoute quelques alternatives à ma réponse. –

+0

Merci encore d'avoir donné tant de réflexion à cela. J'avais aussi considéré l'idée de service, et je pense que ce serait la solution idéale dans mon cas particulier (puisque je peux alors utiliser plus de primitives de verrouillage "standard" comme des mutex ou des moniteurs). Je suis d'accord qu'ayant deux démons comme je l'ai décrit n'est pas une bonne idée. – Charlie

0

Comment vérifiez-vous 'if (widget pas encore dans la base de données)'. Si cela est écrit en sql sous forme de 'Select', vous pouvez utiliser 'Select for update' pour permettre à une seule instance de démon de le faire à la fois. En utilisant la transaction pour les deux dernières lignes, et utilisez 'select for update' pour verrouiller, vous éviterez la course. Je suis sûr qu'il y a un équivalent dans ado.net.

+0

Sélectionnez la mise à jour ne fonctionne pas ici. Parce que vous ne pouvez pas verrouiller une ligne qui n'est pas là. S'il ne trouve pas la ligne, il doit s'assurer que personne ne l'insère en même temps qu'il le fait. –

0

Si vous avez SQL Server 2008 (ou un autre SGBD qui le prend en charge), envisagez d'utiliser une instruction MERGE. Cela peut être écrit pour faire un insert seulement si la ligne n'existe pas.

0

(il est permis effectivement d'avoir plus d'une ligne pour un widget, mais le démon ne devrait jamais faire automatiquement)

Ensuite, il n'est pas la base de données est le problème. c'est le fait que vous avez plusieurs démons qui ne savent pas qu'ils ont détecté le même objet. Il me semble que c'est là que vous devez concentrer votre attention. Du point de vue de la base de données, ils soumettent tous deux des lignes parfaitement légitimes.

Qu'advient-il si vous changez:

1: every so often: 
2: find newest N widgets (from external source) 
3: foreach widget 
4:  if(widget not yet in database) 
5:  add rows for widget 

à

1: every so often: 
2: while there are unhandled widgets 
2:  find first unhandled widget 
4:  if(widget not yet in database) 
5:   add one row for widget 
+0

Bien, je suis d'accord avec vous. Je n'avais pas l'intention de dire que la base de données était en quelque sorte à blâmer, mais cherchait simplement un moyen d'atteindre les objectifs énoncés. Je pense que je serai capable d'utiliser la base de données pour aider, en créant une table spécifiquement pour l'usage par les démons, où je peux * * imposer l'unicité. – Charlie

+0

Je ne suis toujours pas sûr de voir comment vous pouvez détecter une copie légitime d'une copie illégitime. Mais voir modifier à ma réponse. – dkretz

Questions connexes