Impasse ao atualizar linhas diferentes com índice não clusterizado

13

Estou resolvendo um problema de deadlock enquanto observava que o comportamento do bloqueio é diferente quando uso o índice em cluster e não em cluster no campo id. O problema de deadlock parece ser resolvido se o índice confinado ou a chave primária for aplicada ao campo id.

Tenho transações diferentes fazendo uma ou mais atualizações em linhas diferentes, por exemplo, a transação A atualizará apenas a linha com ID = a, tx B só tocará a linha com ID = b etc.

E compreendi que, sem o índice, a atualização adquirirá o bloqueio de atualização para todas as linhas e ocultará o bloqueio exclusivo quando necessário, o que acabará por levar a um impasse. Mas não consigo descobrir por que, com o índice não agrupado, o impasse ainda está lá (embora a taxa de acertos pareça ter caído)

Tabela de dados:

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
)

Rastreio de impasse

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

Também uma descoberta interessante e possível é que o índice clusterizado e não clusterizado parece ter comportamentos de bloqueio diferentes

Ao usar o índice em cluster, há um bloqueio exclusivo na chave, bem como um bloqueio exclusivo no RID ao atualizar, o que é esperado; enquanto houver dois bloqueios exclusivos em dois RID diferentes se for usado um índice não clusterizado, o que me confunde.

Seria útil se alguém puder explicar o porquê disso também.

Teste de SQL:

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

Com id como índice clusterizado:

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

Com id como índice não clusterizado

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: Detalhes do impasse sem nenhum índice
Digamos que eu tenha dois tx A e B, cada um com duas instruções de atualização, linha diferente do curso
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} e {4} teriam uma chance de impasse, uma vez que

em {1}, o bloqueio U é solicitado para a linha 63502, pois ele precisa fazer uma varredura de tabela, e o bloqueio X poderia ter sido retido na linha 63501, pois corresponde à condição

em {4}, o bloqueio U é solicitado para a linha 63501 e o bloqueio X já é válido para 63502

portanto, txA retém 63501 e aguarda 63502 enquanto txB retém 63502 aguardando 63501, que é um impasse

EDIT2: Acontece que um bug do meu caso de teste faz uma situação diferente aqui Desculpe por confusão, mas o bug faz uma situação diferente e parece causar o impasse eventualmente.

Como a análise de Paul realmente me ajudou nesse caso, eu aceitarei isso como resposta.

Devido ao erro do meu caso de teste, duas transações txA e txB podem atualizar a mesma linha, como abaixo:

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} e {3} teriam uma chance de conflito quando:

txA solicita bloqueio U na chave enquanto mantém o bloqueio X no RID (devido à atualização de {1}) txB solicita bloqueio U no RID enquanto mantém o bloqueio U na chave

Bood
fonte
1
Não consigo entender por que uma transação precisa atualizar a mesma linha duas vezes.
precisa saber é o seguinte
@ypercube Bom ponto, é algo que eu deveria melhorar. Mas neste caso eu só quero ter uma melhor compreensão dos comportamentos de bloqueio
Bood
@ypercube após mais pensamentos Eu acho que é possível que uma aplicação com necessidades lógicas complexas para atualizar a mesma linha duas vezes no mesmo tx, poderia ser colunas diferentes, por exemplo
Bood

Respostas:

16

... por que com o índice clusterizado, o impasse ainda está lá (embora a taxa de acertos pareça ter caído)

A questão não é clara (por exemplo, quantas atualizações e quais idvalores estão em cada transação), mas um cenário óbvio de conflito surge com várias atualizações de linha única em uma única transação, onde há uma sobreposição de [id]valores e os IDs são atualizado em uma [id]ordem diferente :

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

Impasse sequência: T1 (u2), T2 (u1), T1 (u1) de espera , T2 (u2) de espera .

Essa sequência de conflito pode ser evitada atualizando estritamente na ordem de identificação em cada transação (adquirindo bloqueios na mesma ordem no mesmo caminho).

Ao usar o índice em cluster, há um bloqueio exclusivo na chave, bem como um bloqueio exclusivo no RID ao atualizar, o que é esperado; enquanto houver dois bloqueios exclusivos em dois RID diferentes se for usado um índice não clusterizado, o que me confunde.

Com um índice clusterizado exclusivo ativado id, um bloqueio exclusivo é executado na chave de cluster para proteger gravações nos dados em linha. Um RIDbloqueio exclusivo separado é necessário para proteger a gravação na textcoluna LOB , que é armazenada em uma página de dados separada por padrão.

Quando a tabela é um heap com apenas um índice não clusterizado id, duas coisas acontecem. Primeiro, um RIDbloqueio exclusivo refere-se aos dados em heap em linha e o outro é o bloqueio nos dados do LOB como antes. O segundo efeito é que é necessário um plano de execução mais complexo.

Com um índice em cluster e uma atualização simples de predicado de igualdade de valor único, o processador de consultas pode aplicar uma otimização que executa a atualização (leitura e gravação) em um único operador, usando um único caminho:

Atualização de operador único

A linha está localizada e atualizada em uma única operação de busca, exigindo apenas bloqueios exclusivos (nenhum bloqueio de atualização é necessário). Um exemplo de sequência de bloqueio usando sua tabela de amostra:

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

Com apenas um índice não clusterizado, a mesma otimização não pode ser aplicada porque precisamos ler de uma estrutura de árvore b e escrever outra. O plano de caminhos múltiplos possui fases separadas de leitura e gravação:

Atualização multi-iterador

Isso adquire bloqueios de atualização ao ler, convertendo em bloqueios exclusivos, se a linha for qualificada. Exemplo de sequência de bloqueio com o esquema fornecido:

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 

Observe que os dados LOB são lidos e gravados no iterador de atualização de tabela. O plano mais complexo e vários caminhos de leitura e gravação aumentam as chances de um conflito.

Por fim, não posso deixar de notar os tipos de dados usados ​​na definição da tabela. Você não deve usar o texttipo de dados descontinuado para novos trabalhos; a alternativa, se você realmente precisa armazenar até 2 GB de dados nesta coluna, é varchar(max). Uma diferença importante entre texte varchar(max)é que os textdados são armazenados fora da linha por padrão, enquanto os varchar(max)armazenam na linha por padrão.

Use tipos Unicode apenas se você precisar dessa flexibilidade (por exemplo, é difícil ver por que um endereço IP precisaria de Unicode). Além disso, escolha limites de comprimento apropriados para seus atributos - 255 em qualquer lugar parece improvável que esteja correto.

Leitura adicional:
Padrões comuns de impasse e livelock
Série de solução de problemas de impasse de Bart Duncan

O bloqueio de rastreamento pode ser feito de várias maneiras. O SQL Server Express com Serviços Avançados ( somente 2014 e 2012 SP1 em diante ) contém a ferramenta Profiler , que é uma maneira suportada de exibir os detalhes da aquisição e liberação de bloqueios.

Paul White 9
fonte
Excelente resposta. Como você está produzindo os logs / rastreio que possuem as mensagens "adquirindo ... trava" e "liberando referência de trava"?
Sanjiv Jivan