web-dev-qa-db-fra.com

Comment verrouiller une table en lecture en utilisant Entity Framework?

J'ai un SQL Server (2012) auquel j'accède en utilisant Entity Framework (4.1). Dans la base de données, j'ai une table appelée URL dans laquelle un processus indépendant alimente de nouvelles URL. Une entrée dans la table URL peut être dans l'état "Nouveau", "En cours de traitement" ou "Traité".

J'ai besoin d'accéder à la table URL à partir de différents ordinateurs, vérifiez les entrées URL avec le statut "Nouveau", prenez la première et marquez-la comme "En cours".

var newUrl = dbEntity.URLs.FirstOrDefault(url => url.StatusID == (int) URLStatus.New);
if(newUrl != null)
{
    newUrl.StatusID = (int) URLStatus.InProcess;
    dbEntity.SaveChanges();
}
//Process the URL

Étant donné que la requête et la mise à jour ne sont pas atomiques, je peux demander à deux ordinateurs différents de lire et de mettre à jour la même entrée URL dans la base de données.

Existe-t-il un moyen de rendre atomique la séquence de sélection puis de mise à jour pour éviter de tels conflits?

58
Gilad Gat

Je n'ai pu vraiment accomplir cela qu'en émettant manuellement une instruction de verrouillage dans une table. Cela fait un verrou de table complet, alors soyez prudent! Dans mon cas, c'était utile pour créer une file d'attente que je ne voulais pas que plusieurs processus touchent à la fois.

using (Entities entities = new Entities())
using (TransactionScope scope = new TransactionScope())
{
    //Lock the table during this transaction
    entities.Database.ExecuteSqlCommand("SELECT TOP 1 KeyColumn FROM MyTable WITH (TABLOCKX, HOLDLOCK)");

    //Do your work with the locked table here...

    //Complete the scope here to commit, otherwise it will rollback
    //The table lock will be released after we exit the TransactionScope block
    scope.Complete();
}

pdate - Dans Entity Framework 6, en particulier avec le code async/await, vous devez gérer les transactions différemment. Cela plantait pour nous après quelques conversions.

using (Entities entities = new Entities())
using (DbContextTransaction scope = entities.Database.BeginTransaction())
{
    //Lock the table during this transaction
    entities.Database.ExecuteSqlCommand("SELECT TOP 1 KeyColumn FROM MyTable WITH (TABLOCKX, HOLDLOCK)");

    //Do your work with the locked table here...

    //Complete the scope here to commit, otherwise it will rollback
    //The table lock will be released after we exit the TransactionScope block
    scope.Commit();
}
50
jocull

Je ne peux pas ajouter de commentaire à la réponse d'André, mais je suis préoccupé par ce commentaire "Le IsolationLevel.RepeatableRead appliquera un verrou à toutes les lignes qui sont lues de telle manière qu'un Thread 2 ne peut pas lire à partir du tableau A si le tableau A a été lu par Thread 1 et Thread 1 n'a pas terminé la transaction. "

La lecture répétable indique uniquement que vous maintiendrez tous les verrous jusqu'à la fin d'une transaction. Lorsque vous utilisez ce niveau d'isolement dans une transaction et lisez une ligne (par exemple la valeur maximale), un verrou "partagé" est émis et sera conservé jusqu'à la fin de la transaction. Ce verrou partagé empêchera un autre thread de mettre à jour la ligne (la mise à jour essaiera d'appliquer un verrou exclusif sur la ligne et qui serait bloqué par le verrou partagé existant), mais il permettra à un autre thread de lire la valeur (le deuxième thread mettra un autre verrou partagé sur la ligne - ce qui est autorisé (c'est pourquoi ils sont appelés verrous partagés)). Donc, pour que l'instruction ci-dessus soit correcte, il faudrait dire "Le IsolationLevel.RepeatableRead appliquera un verrou à toutes les lignes qui sont lues de manière à ce qu'un Thread 2 ne puisse pas mise à jour Tableau A si Tableau A a été lu par le thread 1 et le thread 1 n'a pas terminé la transaction. "

Pour la question d'origine, vous devez utiliser un niveau d'isolation de lecture reproductible ET faire passer le verrou à un verrou exclusif pour empêcher deux processus de lire et de mettre à jour la même valeur. Toutes les solutions impliqueraient de mapper EF à SQL personnalisé (car l'escalade du type de verrou n'est pas intégrée à EF). Vous pouvez utiliser la réponse jocull ou vous pouvez utiliser une mise à jour avec une clause de sortie pour verrouiller les lignes (les instructions de mise à jour obtiennent toujours des verrous exclusifs et en 2008 ou au-dessus peuvent renvoyer un jeu de résultats).

17
Michael Baer

La réponse fournie par @jocull est excellente. J'offre ce Tweak:

Au lieu de cela:

"SELECT TOP 1 KeyColumn FROM MyTable WITH (TABLOCKX, HOLDLOCK)"

Faites ceci:

"SELECT TOP 0 NULL FROM MyTable WITH (TABLOCKX)"

C'est plus générique. Vous pouvez créer une méthode d'assistance qui prend simplement le nom de la table comme paramètre. Pas besoin de connaître les données (alias les noms de colonnes), et il n'est pas nécessaire de récupérer réellement un enregistrement dans le tuyau (alias TOP 1)

13
Timothy Khouri

Vous pouvez essayer de passer un indice UPDLOCK à la base de données et verrouiller simplement des lignes spécifiques .. Pour que ce qu'il sélectionne pour le mettre à jour acquière également un verrou exclusif afin qu'il puisse enregistrer ses modifications (plutôt que d'acquérir simplement un verrou de lecture au début, qu'il essaie plus tard de mettre à niveau plus tard lors de l'enregistrement). Le Holdlock suggéré par jocull ci-dessus est également une bonne idée.

private static TestEntity GetFirstEntity(Context context) {
return context.TestEntities
          .SqlQuery("SELECT TOP 1 Id, Value FROM TestEntities WITH (UPDLOCK)")
          .Single();
}

Je conseille fortement d'envisager une concurrence optimiste: https://www.entityframeworktutorial.net/EntityFramework5/handle-concurrency-in-entity-framework.aspx

0
andrew pate