Question

Nous constatons des conditions de blocage pernicieuses, mais rares, dans la base de données Stack Overflow SQL Server 2005.

J'ai attaché le profileur, configuré un profil de trace en utilisant cet excellent article sur le dépannage des blocages, et capturé un tas d'exemples.Ce qui est bizarre c'est que l'écriture sans blocage est toujours le même:

UPDATE [dbo].[Posts]
SET [AnswerCount] = @p1, [LastActivityDate] = @p2, [LastActivityUserId] = @p3
WHERE [Id] = @p0

L'autre déclaration d'impasse varie, mais il s'agit généralement d'une sorte de déclaration simple et triviale. lire du tableau des messages.Celui-ci est toujours tué dans l'impasse.Voici un exemple

SELECT
[t0].[Id], [t0].[PostTypeId], [t0].[Score], [t0].[Views], [t0].[AnswerCount], 
[t0].[AcceptedAnswerId], [t0].[IsLocked], [t0].[IsLockedEdit], [t0].[ParentId], 
[t0].[CurrentRevisionId], [t0].[FirstRevisionId], [t0].[LockedReason],
[t0].[LastActivityDate], [t0].[LastActivityUserId]
FROM [dbo].[Posts] AS [t0]
WHERE [t0].[ParentId] = @p0

Pour être parfaitement clair, nous ne voyons pas des blocages en écriture/écriture, mais en lecture/écriture.

Nous avons actuellement un mélange de requêtes LINQ et SQL paramétrées.Nous avons ajouté with (nolock) à toutes les requêtes SQL.Cela a peut-être aidé certains.Nous avons également eu une seule requête de badge (très) mal écrite que j'ai corrigée hier, qui prenait plus de 20 secondes à s'exécuter à chaque fois, et qui s'exécutait toutes les minutes en plus.J'espérais que c'était la source de certains problèmes de verrouillage !

Malheureusement, j'ai eu une autre erreur de blocage il y a environ 2 heures.Mêmes symptômes exacts, même coupable exact écrit.

Ce qui est vraiment étrange, c'est que l'instruction SQL d'écriture verrouillable que vous voyez ci-dessus fait partie d'un chemin de code très spécifique.C'est seulement exécuté lorsqu'une nouvelle réponse est ajoutée à une question - il met à jour la question parent avec le nouveau nombre de réponses et la dernière date/utilisateur.Ce n’est évidemment pas si courant par rapport au nombre massif de lectures que nous effectuons !Pour autant que je sache, nous n'effectuons pas un grand nombre d'écritures dans l'application.

Je me rends compte que NOLOCK est une sorte de marteau géant, mais la plupart des requêtes que nous exécutons ici n'ont pas besoin d'être aussi précises.Cela vous inquiétera-t-il si votre profil utilisateur est obsolète de quelques secondes ?

Utiliser NOLOCK avec Linq est un peu plus difficile car Scott Hanselman en parle ici.

Nous flirtons avec l'idée d'utiliser

SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED

sur le contexte de la base de données de base afin que toutes nos requêtes LINQ aient cet ensemble.Sans cela, nous devrions envelopper chaque appel LINQ que nous effectuons (enfin, ceux à lecture simple, qui représentent la grande majorité d'entre eux) dans un bloc de code de transaction de 3 à 4 lignes, ce qui est moche.

Je suppose que je suis un peu frustré que des lectures triviales dans SQL 2005 puissent bloquer les écritures.Je pouvais voir que les blocages d'écriture/écriture étaient un énorme problème, mais lit ? Nous n'exploitons pas un site bancaire ici, nous n'avons pas besoin d'une précision parfaite à chaque fois.

Des idées ?Pensées?


Instanciez-vous un nouvel objet LINQ to SQL DataContext pour chaque opération ou partagez-vous peut-être le même contexte statique pour tous vos appels ?

Jeremy, nous partageons pour la plupart un contexte de données statique dans le contrôleur de base :

private DBContext _db;
/// <summary>
/// Gets the DataContext to be used by a Request's controllers.
/// </summary>
public DBContext DB
{
    get
    {
        if (_db == null)
        {
            _db = new DBContext() { SessionName = GetType().Name };
            //_db.ExecuteCommand("SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED");
        }
        return _db;
    }
}

Recommandez-vous de créer un nouveau contexte pour chaque contrôleur, ou par page, ou ..plus souvent?

Était-ce utile?

La solution

Selon MSDN :

http://msdn.microsoft.com/en-us/library/ms191242.aspx

Lorsque les options de base de données d'isolement d'instantané à lire sont activées, les copies logiques (versions) sont maintenues pour toutes les modifications de données effectuées dans la base de données.Chaque fois qu'une ligne est modifiée par une transaction spécifique, l'instance du moteur de base de données stocke une version de l'image précédemment engagée de la ligne dans TEMPDB.Chaque version est marquée du numéro de séquence de transaction de la transaction qui a effectué la modification.Les versions des lignes modifiées sont enchaînées à l'aide d'une liste de liens.La dernière valeur de ligne est toujours stockée dans la base de données actuelle et enchaînée aux lignes versionnées stockées dans TEMPDB.

Pour les transactions à court terme, une version d'une ligne modifiée peut être mise en cache dans le pool de tampons sans être écrit dans les fichiers de disque de la base de données tempdb.Si le besoin de la ligne versionnée est de courte durée, il sera simplement déposé du pool de tampons et peut ne pas nécessairement encourir les frais généraux d'E / S.

Il semble y avoir une légère pénalité de performances en raison de la surcharge supplémentaire, mais elle peut être négligeable.Nous devrions tester pour nous en assurer.

Essayez de définir cette option et SUPPRIMEZ tous les NOLOCK des requêtes de code, sauf si cela est vraiment nécessaire.Les NOLOCK ou l'utilisation de méthodes globales dans le gestionnaire de contexte de base de données pour lutter contre les niveaux d'isolation des transactions de base de données sont des pansements au problème.NOLOCKS masquera des problèmes fondamentaux avec notre couche de données et conduira éventuellement à la sélection de données peu fiables, où la gestion automatique des versions de ligne de sélection/mise à jour semble être la solution.

ALTER Database [StackOverflow.Beta] SET READ_COMMITTED_SNAPSHOT ON

Autres conseils

AUCUN VERROU et LIRE NON ENGAGÉ sont une pente glissante.Vous ne devriez jamais les utiliser à moins de comprendre d’abord pourquoi le blocage se produit.Cela m'inquiéterait que vous disiez : "Nous avons ajouté with (nolock) à toutes les requêtes SQL".Il faut ajouter AVEC NOLOCK partout est un signe certain que vous avez des problèmes dans votre couche de données.

La déclaration de mise à jour elle-même semble un peu problématique.Déterminez-vous le décompte plus tôt dans la transaction, ou le extrayez-vous simplement d'un objet ? AnswerCount = AnswerCount+1 lorsqu'une question est ajoutée, c'est probablement une meilleure façon de gérer cela.Vous n’avez alors pas besoin d’une transaction pour obtenir le décompte correct et vous n’avez pas à vous soucier du problème de concurrence auquel vous vous exposez potentiellement.

Un moyen simple de contourner ce type de problème de blocage sans trop de travail et sans activer les lectures incorrectes consiste à utiliser "Snapshot Isolation Mode" (nouveau dans SQL 2005) qui vous donnera toujours une lecture claire des dernières données non modifiées.Vous pouvez également intercepter et réessayer assez facilement les instructions bloquées si vous souhaitez les gérer avec élégance.

La question du PO était de demander pourquoi ce problème s'était produit.Cet article espère répondre à cette question tout en laissant les solutions possibles à l'élaboration par d'autres.

Il s'agit probablement d'un problème lié à l'index.Par exemple, disons que la table Posts a un index X non clusterisé qui contient le ParentID et un (ou plusieurs) des champs en cours de mise à jour (AnswerCount, LastActivityDate, LastActivityUserId).

Un blocage se produirait si le cmd SELECT effectue un verrou en lecture partagée sur l'index X pour effectuer une recherche par ParentId, puis doit effectuer un verrou en lecture partagée sur l'index clusterisé pour obtenir les colonnes restantes tandis que le cmd UPDATE effectue une écriture exclusive. verrouiller l'index clusterisé et nécessiter un verrou en écriture exclusive sur l'index X pour le mettre à jour.

Vous vous trouvez maintenant dans une situation où A a verrouillé X et essaie d'obtenir Y tandis que B a verrouillé Y et essaie d'obtenir X.

Bien sûr, nous aurons besoin que l'OP mette à jour sa publication avec plus d'informations sur les index en jeu pour confirmer si c'est réellement la cause.

Je suis assez mal à l'aise face à cette question et aux réponses qui en découlent.Il y a beaucoup de « essayez cette poussière magique !Non, cette poussière magique ! »

Je ne vois nulle part que vous ayez analysé les verrous pris et déterminé quel type exact de verrous est bloqué.

Tout ce que vous avez indiqué, c'est que certains verrous se produisent - et non un blocage.

Dans SQL 2005, vous pouvez obtenir plus d'informations sur les verrous supprimés en utilisant :

DBCC TRACEON (1222, -1)

de sorte que lorsque le blocage se produit, vous aurez de meilleurs diagnostics.

Instanciez-vous un nouvel objet LINQ to SQL DataContext pour chaque opération ou partagez-vous peut-être le même contexte statique pour tous vos appels ?J'ai initialement essayé cette dernière approche et, d'après ce dont je me souviens, cela provoquait un verrouillage indésirable dans la base de données.Je crée maintenant un nouveau contexte pour chaque opération atomique.

Avant de brûler la maison pour attraper une mouche avec NOLOCK partout, vous voudrez peut-être jeter un œil à ce graphique d'impasse que vous auriez dû capturer avec Profiler.

N'oubliez pas qu'un blocage nécessite (au moins) 2 verrous.La connexion 1 a le verrou A, veut le verrou B - et vice-versa pour la connexion 2.C’est une situation insoluble et quelqu’un doit céder.

Ce que vous avez montré jusqu'à présent est résolu par un simple verrouillage, ce que Sql Server se fait un plaisir de faire toute la journée.

Je soupçonne que vous (ou LINQ) démarrez une transaction avec cette instruction UPDATE et que vous sélectionnez au préalable une autre information.Mais vous devez vraiment revenir en arrière dans le graphique des blocages pour trouver les verrous. détenu par chaque thread, puis revenez en arrière dans Profiler pour trouver les instructions qui ont provoqué l'octroi de ces verrous.

Je m'attends à ce qu'il y ait au moins 4 instructions pour compléter ce puzzle (ou une instruction qui prend plusieurs verrous - peut-être y a-t-il un déclencheur sur la table Posts ?).

Cela vous inquiétera-t-il si votre profil utilisateur est obsolète de quelques secondes ?

Non, c'est parfaitement acceptable.Définir le niveau d’isolation de base des transactions est probablement la meilleure façon de procéder.

L’impasse typique en lecture/écriture provient de l’accès à l’ordre d’index.Read (T1) localise la ligne sur l'index A, puis recherche la colonne projetée sur l'index B (généralement regroupée).L'écriture (T2) modifie l'index B (le cluster) doit alors mettre à jour l'index A.T1 a S-Lck sur A, veut S-Lck sur B, T2 a X-Lck sur B, veut U-Lck sur A.Impasse, bouffée.T1 est tué.Ceci est répandu dans les environnements avec un trafic OLTP important et un peu trop d'index :).La solution est de faire en sorte que la lecture n'ait pas à passer de A à B (c.-à-d.colonne incluse dans A, ou supprimer la colonne de la liste projetée) ou T2 n'a pas besoin de passer de B à A (ne pas mettre à jour la colonne indexée).Malheureusement, Linq n'est pas votre ami ici...

@Jeff - Je ne suis certainement pas un expert en la matière, mais j'ai obtenu de bons résultats en instanciant un nouveau contexte à presque chaque appel.Je pense que cela revient à créer un nouvel objet Connection à chaque appel avec ADO.La surcharge n'est pas aussi grave qu'on pourrait le penser, puisque le pooling de connexions sera toujours utilisé de toute façon.

J'utilise simplement un assistant statique global comme celui-ci :

public static class AppData
{
    /// <summary>
    /// Gets a new database context
    /// </summary>
    public static CoreDataContext DB
    {
        get
        {
            var dataContext = new CoreDataContext
            {
                DeferredLoadingEnabled = true
            };
            return dataContext;
        }
    }
}

et puis je fais quelque chose comme ça :

var db = AppData.DB;

var results = from p in db.Posts where p.ID = id select p;

Et je ferais la même chose pour les mises à jour.Quoi qu'il en soit, je n'ai pas autant de trafic que vous, mais j'étais définitivement bloqué lorsque j'ai utilisé très tôt un DataContext partagé avec seulement une poignée d'utilisateurs.Aucune garantie, mais cela vaut peut-être la peine d'essayer.

Mise à jour:Là encore, en regardant votre code, vous partagez uniquement le contexte de données pour la durée de vie de cette instance de contrôleur particulière, ce qui semble fondamentalement correct à moins qu'il ne soit utilisé simultanément par plusieurs appels au sein du contrôleur.Dans un fil de discussion sur le sujet, ScottGu a déclaré :

Les contrôleurs ne vivent que pour une seule requête - donc à la fin du traitement d'une requête, ils sont récupérés (ce qui signifie que le DataContext est collecté)...

Quoi qu'il en soit, ce n'est peut-être pas le cas, mais encore une fois, cela vaut probablement la peine d'essayer, peut-être en conjonction avec des tests de charge.

Q.Pourquoi stockez-vous le AnswerCount dans le Posts la table en premier lieu ?

Une autre approche consiste à éliminer la « réécriture » ​​dans le Posts table en ne stockant pas le AnswerCount dans le tableau mais pour calculer dynamiquement le nombre de réponses au message selon les besoins.

Oui, cela signifie que vous exécutez une requête supplémentaire :

SELECT COUNT(*) FROM Answers WHERE post_id = @id

ou plus généralement (si vous affichez ceci pour la page d'accueil) :

SELECT p.post_id, 
     p.<additional post fields>,
     a.AnswerCount
FROM Posts p
    INNER JOIN AnswersCount_view a
    ON <join criteria>
WHERE <home page criteria>

mais cela se traduit généralement par un INDEX SCAN et peut être plus efficace dans l'utilisation des ressources que dans l'utilisation READ ISOLATION.

Il existe plusieurs façons d’écorcher un chat.La dénormalisation prématurée d'un schéma de base de données peut introduire des problèmes d'évolutivité.

Vous voulez absolument que READ_COMMITTED_SNAPSHOT soit activé, ce qui n'est pas le cas par défaut.Cela vous donne la sémantique MVCC.C'est la même chose qu'Oracle utilise par défaut.Avoir une base de données MVCC est incroyablement utile, NE PAS en utiliser une est insensé.Cela vous permet d'exécuter les opérations suivantes dans une transaction :

Mettre à jour les UTILISATEURS Définir FirstName = 'foobar';// décide de dormir pendant un an.

en attendant, sans commettre ce qui précède, tout le monde peut très bien continuer à sélectionner dans ce tableau.Si vous n'êtes pas familier avec MVCC, vous serez choqué d'avoir pu vivre sans lui.Sérieusement.

Définir votre valeur par défaut sur lecture non validée n'est pas une bonne idée.Vous introduirez sans aucun doute des incohérences et vous retrouverez avec un problème pire que celui que vous rencontrez actuellement.L'isolation des instantanés peut bien fonctionner, mais il s'agit d'un changement radical dans la façon dont Sql Server fonctionne et met un énorme charger sur tempdb.

Voici ce que vous devriez faire:utilisez try-catch (en T-SQL) pour détecter la condition de blocage.Lorsque cela se produit, réexécutez simplement la requête.Il s’agit d’une pratique standard de programmation de base de données.

Il existe de bons exemples de cette technique dans l'ouvrage de Paul Nielson Bible SQL Server 2005.

Voici un modèle rapide que j'utilise :

-- Deadlock retry template

declare @lastError int;
declare @numErrors int;

set @numErrors = 0;

LockTimeoutRetry:

begin try;

-- The query goes here

return; -- this is the normal end of the procedure

end try begin catch
    set @lastError=@@error
    if @lastError = 1222 or @lastError = 1205 -- Lock timeout or deadlock
    begin;
        if @numErrors >= 3 -- We hit the retry limit
        begin;
            raiserror('Could not get a lock after 3 attempts', 16, 1);
            return -100;
        end;

        -- Wait and then try the transaction again
        waitfor delay '00:00:00.25';
        set @numErrors = @numErrors + 1;
        goto LockTimeoutRetry;

    end;

    -- Some other error occurred
    declare @errorMessage nvarchar(4000), @errorSeverity int
    select    @errorMessage = error_message(),
            @errorSeverity = error_severity()

    raiserror(@errorMessage, @errorSeverity, 1)

    return -100
end catch;    

Une chose qui a fonctionné pour moi dans le passé est de m'assurer que toutes mes requêtes et mises à jour accèdent aux ressources (tables) dans le même ordre.

Autrement dit, si une requête est mise à jour dans l'ordre Table1, Table2 et qu'une requête différente la met à jour dans l'ordre Table2, Table1, vous pourriez voir des blocages.

Je ne sais pas s'il vous est possible de modifier l'ordre des mises à jour puisque vous utilisez LINQ.Mais c'est quelque chose à regarder.

Cela vous inquiétera-t-il si votre profil utilisateur est obsolète de quelques secondes ?

Quelques secondes seraient certainement acceptables.De toute façon, cela ne semble pas si long, à moins qu'un grand nombre de personnes ne soumettent des réponses en même temps.

Je suis d'accord avec Jeremy sur ce point.Vous demandez si vous devez créer un nouveau contexte de données pour chaque contrôleur ou par page - j'ai tendance à en créer un nouveau pour chaque requête indépendante.

Je construis actuellement une solution qui implémentait le contexte statique comme vous le faites, et lorsque j'ai lancé des tonnes de requêtes sur la bête d'un serveur (plus d'un million) lors de tests de stress, j'obtenais également des verrous de lecture/écriture de manière aléatoire.

Dès que j'ai changé ma stratégie pour utiliser un contexte de données différent au niveau LINQ par requête et que j'ai eu confiance que le serveur SQL pouvait exploiter sa magie de regroupement de connexions, les verrous ont semblé disparaître.

Bien sûr, j'étais sous une certaine pression de temps, donc j'essayais un certain nombre de choses en même temps, donc je ne peux pas être sûr à 100% que c'est ce qui a résolu le problème, mais j'ai un haut niveau de confiance - disons les choses de cette façon .

Vous devez implémenter des lectures sales.

SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED

Si vous n'avez pas absolument besoin d'une intégrité transactionnelle parfaite avec vos requêtes, vous devez utiliser des lectures sales lorsque vous accédez à des tables à forte concurrence.Je suppose que votre table Posts en ferait partie.

Cela peut vous donner ce qu'on appelle des « lectures fantômes », c'est-à-dire lorsque votre requête agit sur les données d'une transaction qui n'a pas été validée.

Nous n'exploitons pas un site bancaire ici, nous n'avons pas besoin d'une précision parfaite à chaque fois

Utilisez des lectures sales.Vous avez raison, ils ne vous donneront pas une précision parfaite, mais ils devraient résoudre vos problèmes de verrouillage mort.

Sans cela, nous devrions envelopper chaque appel LINQ que nous effectuons (enfin, ceux à lecture simple, qui représentent la grande majorité d'entre eux) dans un bloc de code de transaction de 3 à 4 lignes, ce qui est moche

Si vous implémentez des lectures sales sur "le contexte de base de données de base", vous pouvez toujours envelopper vos appels individuels en utilisant un niveau d'isolation plus élevé si vous avez besoin de l'intégrité transactionnelle.

Alors, quel est le problème avec la mise en œuvre d’un mécanisme de nouvelle tentative ?Il y aura toujours la possibilité qu'une impasse se produise, alors pourquoi ne pas avoir une certaine logique pour l'identifier et réessayer ?

Est-ce qu'au moins certaines des autres options n'introduiront pas des pénalités de performances qui seront appliquées à tout moment alors qu'un système de nouvelle tentative se déclenchera rarement ?

N'oubliez pas non plus une sorte de journalisation lorsqu'une nouvelle tentative se produit afin de ne pas vous retrouver dans cette situation rare qui devient souvent.

Maintenant que je vois la réponse de Jeremy, je pense que je me souviens avoir entendu dire que la meilleure pratique consiste à utiliser un nouveau DataContext pour chaque opération sur les données.Rob Conery a écrit plusieurs articles sur DataContext, et il les publie toujours plutôt que d'utiliser un singleton.

Voici le modèle que nous avons utilisé pour Video.Show (lien vers la vue source dans CodePlex):

using System.Configuration;
namespace VideoShow.Data
{
  public class DataContextFactory
  {
    public static VideoShowDataContext DataContext()
    {
        return new VideoShowDataContext(ConfigurationManager.ConnectionStrings["VideoShowConnectionString"].ConnectionString);
    }
    public static VideoShowDataContext DataContext(string connectionString)
    {
        return new VideoShowDataContext(connectionString);
    }
  }
}

Puis au niveau service (ou encore plus granulaire, pour les mises à jour) :

private VideoShowDataContext dataContext = DataContextFactory.DataContext();

public VideoSearchResult GetVideos(int pageSize, int pageNumber, string sortType)
{
  var videos =
  from video in DataContext.Videos
  where video.StatusId == (int)VideoServices.VideoStatus.Complete
  orderby video.DatePublished descending
  select video;
  return GetSearchResult(videos, pageSize, pageNumber);
}

Je devrais être d'accord avec Greg tant que définir le niveau d'isolement sur lecture non validée n'a pas d'effets néfastes sur les autres requêtes.

Je serais intéressé de savoir, Jeff, comment le définir au niveau de la base de données affecterait une requête telle que la suivante :

Begin Tran
Insert into Table (Columns) Values (Values)
Select Max(ID) From Table
Commit Tran

Cela ne me dérange pas si mon profil est obsolète ne serait-ce que depuis plusieurs minutes.

Réessayez-vous la lecture après un échec ?Il est certainement possible, lors du lancement d'une tonne de lectures aléatoires, que quelques-unes frappent alors qu'elles ne peuvent pas lire.La plupart des applications avec lesquelles je travaille comportent très peu d'écritures par rapport au nombre de lectures et je suis sûr que les lectures sont loin d'être proches du nombre que vous obtenez.

Si l'implémentation de "READ UNCOMMITTED" ne résout pas votre problème, il est alors difficile de vous aider sans en savoir beaucoup plus sur le traitement.Il peut exister une autre option de réglage qui pourrait améliorer ce comportement.À moins qu'un gourou MSSQL ne vienne à la rescousse, je recommande de soumettre le problème au fournisseur.

Je continuerais à tout régler ;comment fonctionne le sous-système de disque ?Quelle est la longueur moyenne de la file d’attente du disque ?Si les E/S effectuent une sauvegarde, le véritable problème ne vient peut-être pas de ces deux requêtes qui bloquent, mais plutôt d'une autre requête qui engorge le système ;vous avez évoqué une requête prenant 20 secondes qui a été réglée, y en a-t-il d'autres ?

Concentrez-vous sur le raccourcissement des requêtes de longue durée, je parie que les problèmes de blocage disparaîtront.

J'ai eu le même problème et je ne peux pas utiliser "IsolationLevel = IsolationLevel.ReadUncommit" sur TransactionScope car le serveur n'a pas activé DTS (!).

C'est ce que j'ai fait avec une méthode d'extension :

public static void SetNoLock(this MyDataContext myDS)
{
    myDS.ExecuteCommand("SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED");
}

Ainsi, pour les sélections qui utilisent des tables de concurrence critiques, nous activons le « nolock » comme ceci :

using (MyDataContext myDS = new MyDataContext())
{
   myDS.SetNoLock();

   //  var query = from ...my dirty querys here...
}

Les suggestions sont les bienvenues !

Licencié sous: CC-BY-SA avec attribution
Non affilié à StackOverflow
scroll top