Prévention de la condition de concurrence de if-exists-update-else-insert dans Entity Framework

J'ai lu d'autres questions sur la façon d'implémenter la sémantique if-exists-insert-else-update dans EF, mais soit je ne comprends pas comment les réponses fonctionnent, soit elles ne résolvent pas le problème. Une solution courante proposée consiste à envelopper le travail dans une portée de transaction (par exemple: implémentant if-not-exists-insert en utilisant Entity Framework sans conditions de course):

using (var scope = new TransactionScope()) // default isolation level is serializable
using(var context = new MyEntities())
{
    var user = context.Users.SingleOrDefault(u => u.Id == userId); // *
    if (user != null)
    {
        // update the user
        user.property = newProperty;
        context.SaveChanges();
    }
    else
    {
        user = new User
        {
             // etc
        };
        context.Users.AddObject(user);
        context.SaveChanges();
    }
}

Mais je ne vois pas comment cela résout quoi que ce soit, comme pour que cela fonctionne, la ligne que j'ai joué ci-dessus devrait bloquer {[7] } Si un second thread tente d'accéder au même ID utilisateur, débloquer uniquement lorsque le premier thread a terminé son travail. L'utilisation d'une transaction ne causera cependant pas , et nous obtiendrons une exception UpdateException levée en raison de la violation de clé qui se produit lorsque le deuxième thread tente de créer le même utilisateur pour une deuxième fois.

Au lieu d'attraper l'exception causée par la condition de course, il serait préférable d'empêcher la condition de course de se produire dans le premier lieu. Une façon de le faire serait que la ligne étoilée prenne un verrou exclusif sur la ligne de base de données qui correspond à sa condition, ce qui signifie que dans le contexte de ce bloc, un seul thread à la fois pourrait fonctionner avec un utilisateur.

Il semble que cela doit être un problème commun pour les utilisateurs de L'EF, donc je suis à la recherche d'une solution propre et générique que je peux utiliser partout.

Je voudrais vraiment éviter d'utiliser une procédure stockée pour créer mon utilisateur, si possible.

Tout des idées?

EDIT : j'ai essayé d'exécuter le code ci-dessus simultanément sur deux threads différents en utilisant le même ID utilisateur, et malgré la sortie de transactions sérialisables, ils ont tous deux pu entrer simultanément dans la section critique ( * ). Cela conduit à une UpdateException levée lorsque le deuxième thread a tenté d'insérer le même ID utilisateur que le premier venait d'insérer. En effet, comme L'a souligné Ladislav ci-dessous, une transaction sérialisable ne prend des verrous exclusifs qu'après il a commencé à modifier les données, pas à lire.

23
demandé sur Community 2011-05-30 05:39:47

4 réponses

Lors de l'utilisation de transactions sérialisables, SQL Server émet des verrous partagés sur les enregistrements / tables de lecture. Les verrous partagés n'autorisent pas les autres transactions à modifier les données verrouillées (les transactions bloqueront), mais ils permettent aux autres transactions de lire les données avant que la transaction qui a émis les verrous ne commence à modifier les données. C'est la raison pour laquelle l'exemple ne fonctionne pas - les lectures simultanées sont autorisées avec des verrous partagés jusqu'à ce que la première transaction commence à modifier les données.

Vous voulez l'isolement où sélectionner commande verrouille la table entière exclusivement pour un seul client. Il doit verrouiller toute la table car sinon il ne résoudra pas la concurrence pour insérer "le même" enregistrement. Un contrôle granulaire pour verrouiller des enregistrements ou des tables par des commandes select est possible lors de l'utilisation d'astuces, mais vous devez écrire des requêtes SQL directes pour les utiliser - EF n'a aucun support pour cela. J'ai décrit l'approche pour verrouiller exclusivement cette table ici mais c'est comme créer un accès séquentiel à la table et cela affecte tous les autres clients accédant à cette table.

Si vous êtes vraiment sûr que cette opération se produit uniquement dans votre méthode unique et qu'il n'y a pas d'autres applications utilisant votre base de données, vous pouvez simplement placer le code dans la section critique (synchronisation. net par exemple avec lock) et vous assurer du côté. net que seul un thread peut accéder à la section critique. Ce n'est pas une solution aussi fiable, mais tout jeu avec les verrous et les niveaux de transaction a un grand impact sur les performances et le débit de la base de données. Vous pouvez combiner cette approche avec une concurrence optimiste (contraintes uniques, horodatages, etc.).

12
répondu Ladislav Mrnka 2017-05-23 12:10:05

Vous pouvez changer le niveau d'isolement des transactions en utilisant TransactionOptions pour TransactionScope à plus strict (je suppose que pour votre cas, il est RepeatableRead ou Serializable), mais rappelez-vous que tous les verrous diminuent l'évolutivité.

Est-ce vraiment important de fournir un tel niveau de contrôle de concurrence? Votre application sera-t-elle utilisée dans les mêmes cas dans un environnement de production? Voici bon post par UDI Dahan sur les conditions de course.

0
répondu xelibrion 2011-05-30 05:23:18

Peut-être qu'il me manque quelque chose, mais quand je simule l'exemple ci-dessus dans SQL Management Studio, cela fonctionne comme prévu.

Les deux transactions sérialisables vérifient si l'ID utilisateur existe et acquièrent des verrous de plage sur la sélection spécifiée.

En supposant que cet ID utilisateur n'existe pas, les deux transactions essaient d'insérer un nouvel enregistrement avec l'ID utilisateur - ce qui n'est pas possible. En raison de leur niveau D'isolement sérialisable, les deux transactions ne peuvent pas insérer un nouvel enregistrement dans les utilisateurs table car cela introduirait des lectures fantômes pour l'autre transaction.

Donc, cette situation entraîne une impasse à cause des verrous de plage. Vous finirez avec une impasse et une transaction sera victimisée, l'autre réussira.

Est-ce que Entity Framework gère cela différemment? Je soupçonne que vous finiriez avec un UpdateException avec un SqlException imbriqué identifiant l'impasse.

0
répondu Jeroen de Bekker 2011-06-07 13:10:23

Juste pour ajouter mon chemin, pas qu'il traite vraiment de l'ennui des exceptions lancées et des transactions ne le coupant pas tout à fait comme une solution évolutive, mais il évite que les conditions de course ne causent des problèmes où les solutions de type verrou ne sont pas possibles (facilement gérées) comme dans les systèmes distribués.

J'utilise très simplement l'exception et j'essaie d'abord l'insertion. J'utilise une modification de votre code d'origine comme exemple:

using(var context = new MyEntities())
{
    EntityEntry entityUser = null;
    try 
    {
        user = new User
        {
             // etc
        };
        entityUser = context.Users.Add(user);
        context.SaveChanges(); // Will throw if the entity already exists
    } 
    catch (DbUpdateException x)
    when (x.InnerException != null && x.InnerException.Message.StartsWith("Cannot insert duplicate key row in object"))
    {
        if (entityUser != null)
        {
            // Detach the entity to stop it hanging around on the context
            entityUser.State = EntityState.Detached;
        }
        var user = context.Users.Find(userId);
        if (user != null) // just in case someone deleted it in the mean time
        {
            // update the user
            user.property = newProperty;
            context.SaveChanges();
        }
    }
}

Ce n'est pas joli, mais cela fonctionne et pourrait être de utilisez à quelqu'un.

0
répondu Baguazhang 2018-01-18 19:36:18