2009-03-30 8 views
5

J'ai une table de données héritée dans SQL Server 2005 qui a un PK sans identité/autoincrement et aucun pouvoir pour en implémenter un. Par conséquent, je suis obligé de créer manuellement de nouveaux enregistrements dans ASP.NET via la méthode ole "SELECT MAX (id) + 1 FROM table" -avant-insert.Comment éviter une condition de concurrence de base de données lors de l'incrémentation PK de la nouvelle ligne

De toute évidence, cela crée une condition de concurrence sur l'ID en cas d'insertions simultanées.

Quelle est la meilleure façon de résoudre gracieusement l'événement d'une collision de course? Je suis à la recherche d'idées de code VB.NET ou C# dans le sens de la détection d'une collision, puis réessayer l'insertion échouée en obtenant encore un autre max (id) + 1. Est-ce que cela peut être fait?

Pensées? Commentaires? Sagesse?

Merci!

REMARQUE: que se passe-t-il si je ne peux pas modifier la base de données de quelque manière que ce soit?

Répondre

4

Ne pas pouvoir modifier le schéma de la base de données est difficile.

Si vous insérez un PK existant dans une table, vous obtiendrez SqlException avec un message indiquant une violation de la contrainte PK. Attrapez cette exception et réessayez d'insérer quelques fois jusqu'à ce que vous réussissiez. Si vous trouvez que le taux de collision est trop élevé, vous pouvez essayer max(id) + <small-random-int> au lieu de max(id) + 1. Notez qu'avec cette approche vos identifiants auront des lacunes et l'espace d'identification sera épuisé plus tôt.

Une autre approche possible consiste à émuler l'ID d'auto-incrémentation en dehors de la base de données. Par exemple, créez un entier statique, Interlocked.Increment chaque fois que vous avez besoin de l'ID suivant et utilisez la valeur renvoyée. La partie délicate consiste à initialiser ce compteur statique à bonne valeur. Je le ferais avec Interlocked.CompareExchange:

class Autoincrement { 
    static int id = -1; 
    public static int NextId() { 
    if (id == -1) { 
     // not initialized - initialize 
     int lastId = <select max(id) from db> 
     Interlocked.CompareExchange(id, -1, lastId); 
    } 
    // get next id atomically 
    return Interlocked.Increment(id); 
    } 
} 

Il est évident que ce dernier ne fonctionne que si tous les ids insérés sont obtenus par Autoincrement.NextId de processus unique.

+0

Juste ce dont j'avais besoin merci !!! –

6

Créez une table auxiliaire avec une colonne d'identité. Dans une insertion de transaction dans la table aux, récupérez la valeur et utilisez-la pour l'insérer dans votre table héritée. À ce stade, vous pouvez même supprimer la ligne insérée dans la table aux, le point est juste de l'utiliser comme source de valeurs incrémentées.

+0

C'est beaucoup plus simple à mettre en œuvre, +1 –

+0

Très intelligent, merci! Malheureusement, je n'ai aucun moyen de changer la base de données. C'est gelé. –

+0

Ajoutez-le en tant que seconde base de données liée - ou utilisez un fichier XML local au code pour obtenir la même chose. –

1

La meilleure solution consiste à changer la base de données. Vous ne pouvez pas être en mesure de modifier la colonne pour qu'elle soit une colonne d'identité, mais vous devriez être en mesure de vous assurer qu'il existe une contrainte unique sur la colonne et ajouter une nouvelle colonne d'identité avec vos PK existants. Ensuite, utilisez la nouvelle colonne à la place ou utilisez un déclencheur pour que l'ancienne colonne reflète le nouveau ou les deux.

3

La clé est de le faire dans une déclaration ou une transaction.

Pouvez-vous le faire?

INSERT (PKcol, col2, col3, ...) 
SELECT (SELECT MAX(id) + 1 FROM table WITH (HOLDLOCK, UPDLOCK)), @val2, @val3, ... 

Sans test, cela fonctionnera probablement aussi:

INSERT (PKcol, col2, col3, ...) 
VALUES ((SELECT MAX(id) + 1 FROM table WITH (HOLDLOCK, UPDLOCK)), @val2, @val3, ...) 

Si vous ne pouvez pas, une autre façon est de le faire dans un déclencheur.

  1. Le déclencheur fait partie de la transaction INSERT
  2. utilisation HOLDLOCK, UPDLOCK pour le MAX. Cela est le verrou de ligne jusqu'à engager
  3. La ligne étant mise à jour est verrouillé pour la durée

Un deuxième insert attendra jusqu'à ce que le premier finalise. L'inconvénient est que vous changez la clé primaire.

Une table auxiliaire doit faire partie d'une transaction.

Ou changer le schéma comme l'a suggéré ...

+0

J'ai essayé un insert imbriqué et il m'a dit que je ne peux pas faire d'insertions imbriquées. Je n'ai pas non plus le pouvoir de changer la base de données autrement que par des mises à jour/insertions SQL. –

+0

Les transactions côté client sont-elles autorisées dans votre code client – gbn

2

Qu'en est en cours d'exécution du lot entier (sélectionner pour id et insérer) dans la transaction sérialisable?

Cela devrait vous aider à faire des changements dans la base de données.

3

Remarque: Tout ce dont vous avez besoin est une source d'entiers toujours plus nombreux. Il ne doit pas nécessairement provenir de la même base de données ou même d'une base de données.

Personnellement, je voudrais utiliser SQL Express parce que c'est gratuit et facile.

Si vous avez un seul serveur web: Créer une base de données SQL Express sur le serveur Web avec une seule table [ids] avec un seul champ autoincrementing [new_id]. Insérez un enregistrement dans cette table [ids], récupérez le [new_id] et transmettez-le à votre couche de base de données en tant que PK de la table en question.

Si vous avez plusieurs serveurs Web: C'est une douleur à installer, mais vous pouvez utiliser la même astuce en produisant des graines/incrément approprié (par exemple incrément = 3 et graines = 1/2/3 pour trois web les serveurs).

1

La principale préoccupation concerne-t-elle l'accès simultané? Je veux dire, est-ce que plusieurs instances de votre application (ou, à votre avis, d'autres applications indépendantes de votre volonté) effectueront des insertions simultanément? Si ce n'est pas le cas, vous pouvez probablement gérer les insertions via un module central synchronisé dans votre application, et éviter complètement les conditions de course.

Si oui, eh bien ... comme l'a dit Joel, changez la base de données. Je sais que vous ne pouvez pas, mais le problème est aussi vieux que les collines, et il a été résolu bien - au niveau de la base de données. Si vous voulez le réparer vous-même, vous allez juste devoir boucler (insérer, vérifier les collisions, supprimer) encore et encore et encore. Le problème fondamental est que vous ne pouvez pas effectuer une transaction (je ne veux pas dire cela au sens SQL "TRANSACTION", mais dans le sens plus large de la théorie des données) si vous n'avez pas le support de la base de données. La seule autre pensée que j'ai est que si vous avez au moins le contrôle sur qui a accès à la base de données (par exemple, seules les applications "autorisées", écrites ou approuvées par vous), vous pouvez implémenter un mutex de bande latérale de toutes sortes, où un «bâton parlant» est partagé par toutes les applications et la propriété du mutex est nécessaire pour faire un insert. Il s'agirait de sa propre boule de cire, car il faudrait définir des règles pour les clients décédés, les héberger, les problèmes de configuration, etc. Et bien sûr, un client «voyou» pourrait faire des insertions sans le bâton parlant et Tuyau l'ensemble de l'installation.

Questions connexes