Blocage lors de la mise à jour de différentes lignes avec un index non clusterisé

13

Je résout un problème de blocage tandis que je remarquais que le comportement du verrouillage était différent lorsque j'utilisais un index cluster et non cluster sur le champ id. Le problème de blocage semble résolu si l'index ou la clé primaire sécurisée est appliqué au champ id.

J'ai différentes transactions effectuant une ou plusieurs mises à jour sur différentes lignes, par exemple la transaction A ne mettra à jour que la ligne avec ID = a, tx B ne touchera que la ligne avec ID = b etc.

Et j'ai compris que sans index, la mise à jour acquerra un verrou de mise à jour pour toutes les lignes et sera convertie en verrou exclusif si nécessaire, ce qui conduira finalement à un blocage. Mais je n'arrive pas à savoir pourquoi avec un index non clusterisé, l'impasse est toujours là (bien que le taux de réussite semble être baissé)

Tableau de données:

CREATE TABLE [dbo].[user](
    [id] [int] IDENTITY(1,1) NOT NULL,
    [userName] [nvarchar](255) NULL,
    [name] [nvarchar](255) NULL,
    [phone] [nvarchar](255) NULL,
    [password] [nvarchar](255) NULL,
    [ip] [nvarchar](30) NULL,
    [email] [nvarchar](255) NULL,
    [pubDate] [datetime] NULL,
    [todoOrder] [text] NULL
)

Trace de blocage

deadlock-list
deadlock victim=process4152ca8
process-list
process id=process4152ca8 taskpriority=0 logused=0 waitresource=RID: 5:1:388:29 waittime=3308 ownerId=252354 transactionname=user_transaction lasttranstarted=2014-04-11T00:15:30.947 XDES=0xb0bf180 lockMode=U schedulerid=3 kpid=11392 status=suspended spid=57 sbid=0 ecid=0 priority=0 trancount=2 lastbatchstarted=2014-04-11T00:15:30.953 lastbatchcompleted=2014-04-11T00:15:30.950 lastattention=1900-01-01T00:00:00.950 clientapp=.Net SqlClient Data Provider hostname=BOOD-PC hostpid=9272 loginname=getodo_sql isolationlevel=read committed (2) xactid=252354 currentdb=5 lockTimeout=4294967295 clientoption1=671088672 clientoption2=128056
executionStack
frame procname=adhoc line=1 stmtstart=62 sqlhandle=0x0200000062f45209ccf17a0e76c2389eb409d7d970b0f89e00000000000000000000000000000000
update [user] WITH (ROWLOCK) set [todoOrder]=@para0 where id=@owner
frame procname=unknown line=1 sqlhandle=0x00000000000000000000000000000000000000000000000000000000000000000000000000000000
unknown
inputbuf
(@para0 nvarchar(2)<c/>@owner int)update [user] WITH (ROWLOCK) set [todoOrder]=@para0 where id=@owner
process id=process4153468 taskpriority=0 logused=4652 waitresource=KEY: 5:72057594042187776 (3fc56173665b) waittime=3303 ownerId=252344 transactionname=user_transaction lasttranstarted=2014-04-11T00:15:30.920 XDES=0x4184b78 lockMode=U schedulerid=3 kpid=7272 status=suspended spid=58 sbid=0 ecid=0 priority=0 trancount=2 lastbatchstarted=2014-04-11T00:15:30.960 lastbatchcompleted=2014-04-11T00:15:30.960 lastattention=1900-01-01T00:00:00.960 clientapp=.Net SqlClient Data Provider hostname=BOOD-PC hostpid=9272 loginname=getodo_sql isolationlevel=read committed (2) xactid=252344 currentdb=5 lockTimeout=4294967295 clientoption1=671088672 clientoption2=128056
executionStack
frame procname=adhoc line=1 stmtstart=60 sqlhandle=0x02000000d4616f250747930a4cd34716b610a8113cb92fbc00000000000000000000000000000000
update [user] WITH (ROWLOCK) set [todoOrder]=@para0 where id=@uid
frame procname=unknown line=1 sqlhandle=0x00000000000000000000000000000000000000000000000000000000000000000000000000000000
unknown
inputbuf
(@para0 nvarchar(61)<c/>@uid int)update [user] WITH (ROWLOCK) set [todoOrder]=@para0 where id=@uid
resource-list
ridlock fileid=1 pageid=388 dbid=5 objectname=SQL2012_707688_webows.dbo.user id=lock3f7af780 mode=X associatedObjectId=72057594042122240
owner-list
owner id=process4153468 mode=X
waiter-list
waiter id=process4152ca8 mode=U requestType=wait
keylock hobtid=72057594042187776 dbid=5 objectname=SQL2012_707688_webows.dbo.user indexname=10 id=lock3f7ad700 mode=U associatedObjectId=72057594042187776
owner-list
owner id=process4152ca8 mode=U
waiter-list
waiter id=process4153468 mode=U requestType=wait

Une conclusion connexe intéressante et possible est également que l'index cluster et non cluster semble avoir des comportements de verrouillage différents

Lorsque vous utilisez l'index clusterisé, il existe un verrou exclusif sur la clé ainsi qu'un verrou exclusif sur RID lors de la mise à jour, ce qui est attendu; alors qu'il y a deux verrous exclusifs sur deux RID différents si un index non cluster est utilisé, ce qui m'embrouille.

Serait utile si quelqu'un peut expliquer pourquoi à ce sujet aussi.

Testez SQL:

use SQL2012_707688_webows;
begin transaction;
update [user] with (rowlock) set todoOrder='{1}' where id = 63501
exec sp_lock;
commit;

Avec id comme index clusterisé:

spid    dbid    ObjId   IndId   Type    Resource    Mode    Status
53  5   917578307   1   KEY (b1a92fe5eed4)                      X   GRANT
53  5   917578307   1   PAG 1:879                               IX  GRANT
53  5   917578307   1   PAG 1:1928                              IX  GRANT
53  5   917578307   1   RID 1:879:7                             X   GRANT

Avec id comme index non clusterisé

spid    dbid    ObjId   IndId   Type    Resource    Mode    Status
53  5   917578307   0   PAG 1:879                               IX  GRANT
53  5   917578307   0   PAG 1:1928                              IX  GRANT
53  5   917578307   0   RID 1:879:7                             X   GRANT
53  5   917578307   0   RID 1:1928:18                           X   GRANT

EDIT1: Détails de l'impasse sans aucun index
Disons que j'ai deux tx A et B, chacun avec deux instructions de mise à jour, ligne différente bien sûr
tx A

update [user] with (rowlock) set todoOrder='{1}' where id = 63501
update [user] with (rowlock) set todoOrder='{2}' where id = 63501

tx B

update [user] with (rowlock) set todoOrder='{3}' where id = 63502
update [user] with (rowlock) set todoOrder='{4}' where id = 63502

{1} et {4} auraient une chance de blocage, car

à {1}, le verrou U est demandé pour la ligne 63502 car il doit effectuer une analyse de table, et le verrou X aurait pu être maintenu sur la ligne 63501 car il correspond à la condition

à {4}, le verrou U est demandé pour la ligne 63501 et le verrou X est déjà maintenu pour 63502

nous avons donc txA détient 63501 et attend 63502 tandis que txB détient 63502 en attente de 63501, ce qui est une impasse

EDIT2: Il s'avère qu'un bogue de mon cas de test fait une situation différente ici Désolé pour la confusion mais le bogue fait une situation différente, et semble finalement provoquer l'impasse.

Puisque l'analyse de Paul m'a vraiment aidé dans ce cas, je vais donc accepter cela comme une réponse.

En raison du bogue de mon cas de test, deux transactions txA et txB peuvent mettre à jour la même ligne, comme ci-dessous:

tx A

update [user] with (rowlock) set todoOrder='{1}' where id = 63501
update [user] with (rowlock) set todoOrder='{2}' where id = 63501

tx B

update [user] with (rowlock) set todoOrder='{3}' where id = 63501

{2} et {3} auraient une chance de blocage lorsque:

txA demande le verrouillage U sur la clé tout en maintenant le verrouillage X sur le RID (en raison de la mise à jour de {1}) txB demande le verrouillage U sur le RID tout en maintenant le verrouillage U sur la clé

Bood
la source
1
Je ne peux pas comprendre pourquoi une transaction doit mettre à jour deux fois la même ligne.
ypercubeᵀᴹ
@ypercube Bon point, c'est quelque chose que je devrais améliorer. Mais dans ce cas, je veux juste avoir une meilleure compréhension des comportements de verrouillage
Bood
@ypercube après plus de réflexions, je pense qu'il est possible qu'une application avec une logique complexe ait besoin de mettre à jour deux fois la même ligne dans le même tx, pourrait être des colonnes différentes par exemple
Bood

Réponses:

16

... pourquoi avec un index clusterisé, l'impasse est toujours là (bien que le taux de réussite semble être baissé)

La question n'est pas précisément claire (par exemple, combien de mises à jour et à quelles idvaleurs se trouve dans chaque transaction) mais un scénario de blocage évident se produit avec plusieurs mises à jour sur une seule ligne au sein d'une même transaction, où il y a un chevauchement de [id]valeurs et les identifiants sont mis à jour dans un [id]ordre différent :

[T1]: Update id 2; Update id 1;
[T2]: Update id 1; Update id 2;

Séquence de blocage: T1 (u2), T2 (u1), T1 (u1) d' attente , T2 (u2) d' attente .

Cette séquence de blocage peut être évitée en mettant à jour strictement dans l'ordre id au sein de chaque transaction (acquisition de verrous dans le même ordre sur le même chemin).

Lorsque vous utilisez l'index clusterisé, il existe un verrou exclusif sur la clé ainsi qu'un verrou exclusif sur RID lors de la mise à jour, ce qui est attendu; alors qu'il y a deux verrous exclusifs sur deux RID différents si un index non cluster est utilisé, ce qui m'embrouille.

Avec un index cluster unique activé id, un verrou exclusif est pris sur la clé de clustering pour protéger les écritures dans les données en ligne. Un RIDverrou exclusif distinct est requis pour protéger l'écriture dans la textcolonne LOB , qui est stockée par défaut sur une page de données distincte.

Lorsque la table est un tas avec uniquement un index non clusterisé id, deux choses se produisent. Tout d'abord, un RIDverrou exclusif concerne les données de tas en ligne, et l'autre est le verrou sur les données LOB comme précédemment. Le deuxième effet est qu'un plan d'exécution plus complexe est nécessaire.

Avec un index clusterisé et une simple mise à jour de prédicat d'égalité à valeur unique, le processeur de requêtes peut appliquer une optimisation qui effectue la mise à jour (lecture et écriture) dans un seul opérateur, en utilisant un seul chemin:

Mise à jour mono-opérateur

La ligne est localisée et mise à jour en une seule opération de recherche, ne nécessitant que des verrous exclusifs (aucun verrou de mise à jour n'est nécessaire). Un exemple de séquence de verrouillage à l'aide de votre exemple de table:

acquiring IX lock on OBJECT: 6:992930809:0 -- TABLE
acquiring IX lock on PAGE: 6:1:59104 -- INROW
acquiring X lock on KEY: 6:72057594233618432 (61a06abd401c) -- INROW
acquiring IX lock on PAGE: 6:1:59091 -- LOB
acquiring X lock on RID: 6:1:59091:1 -- LOB

releasing lock reference on PAGE: 6:1:59091 -- LOB
releasing lock reference on RID: 6:1:59091:1 -- LOB
releasing lock reference on KEY: 6:72057594233618432 (61a06abd401c) -- INROW
releasing lock reference on PAGE: 6:1:59104 -- INROW

Avec seulement un index non clusterisé, la même optimisation ne peut pas être appliquée car nous devons lire à partir d'une structure b-tree et en écrire une autre. Le plan à chemins multiples comporte des phases de lecture et d'écriture distinctes:

Mise à jour multi-itérateur

Cela acquiert des verrous de mise à jour lors de la lecture, se convertissant en verrous exclusifs si la ligne se qualifie. Exemple de séquence de verrouillage avec le schéma donné:

acquiring IX lock on OBJECT: 6:992930809:0 -- TABLE
acquiring IU lock on PAGE: 6:1:59105 -- NC INDEX
acquiring U lock on KEY: 6:72057594233749504 (61a06abd401c) -- NC INDEX
acquiring IU lock on PAGE: 6:1:59104 -- HEAP
acquiring U lock on RID: 6:1:59104:1 -- HEAP
acquiring IX lock on PAGE: 6:1:59104 -- HEAP convert to X
acquiring X lock on RID: 6:1:59104:1 -- HEAP convert to X
acquiring IU lock on PAGE: 6:1:59091 -- LOB
acquiring U lock on RID: 6:1:59091:1 -- LOB

releasing lock reference on PAGE: 6:1:59091 
releasing lock reference on RID: 6:1:59091:1
releasing lock reference on RID: 6:1:59104:1
releasing lock reference on PAGE: 6:1:59104 
releasing lock on KEY: 6:72057594233749504 (61a06abd401c)
releasing lock on PAGE: 6:1:59105 

Notez que les données LOB sont lues et écrites dans l'itérateur de mise à jour de table. Le plan plus complexe et les multiples chemins de lecture et d'écriture augmentent les chances d'un blocage.

Enfin, je ne peux m'empêcher de remarquer les types de données utilisés dans la définition de la table. Vous ne devez pas utiliser le texttype de données obsolète pour de nouveaux travaux; l'alternative, si vous avez vraiment besoin de pouvoir stocker jusqu'à 2 Go de données dans cette colonne, est varchar(max). Une différence importante entre textet varchar(max)est que les textdonnées sont stockées hors ligne par défaut, tandis qu'elles sont stockées varchar(max)en ligne par défaut.

N'utilisez les types Unicode que si vous avez besoin de cette flexibilité (par exemple, il est difficile de voir pourquoi une adresse IP aurait besoin d'Unicode). En outre, choisissez des limites de longueur appropriées pour vos attributs - 255 semble peu probable partout être correct.


Lectures supplémentaires: Modèles courants d'interblocage et de blocage des livraisons
Série de dépannage de l'impasse de Bart Duncan

Le traçage des verrous peut être effectué de différentes manières. SQL Server Express avec services avancés ( 2014 et 2012 SP1 uniquement ) contient l' outil Profiler , qui est un moyen pris en charge pour afficher les détails de l'acquisition et de la libération des verrous.

Paul White 9
la source
Excellente réponse. Comment sortez-vous les journaux / traces contenant les messages "acquisition ... verrouillage" et "libération de la référence de verrouillage"?
Sanjiv Jivan