Punto muerto al actualizar diferentes filas con índice no agrupado

13

Estoy resolviendo un problema de bloqueo mientras noté que el comportamiento de bloqueo es diferente cuando uso el índice agrupado y no agrupado en el campo id. El problema de punto muerto parece resolverse si el índice agrupado o la clave primaria se aplican al campo id.

Tengo diferentes transacciones haciendo una o más actualizaciones a diferentes filas, por ejemplo, la transacción A solo actualizará la fila con ID = a, tx B solo tocará la fila con ID = b, etc.

Y tengo entendido que sin índice, la actualización adquirirá un bloqueo de actualización para todas las filas y se convertirá en un bloqueo exclusivo cuando sea necesario, lo que eventualmente conducirá a un punto muerto. Pero no entiendo por qué con el índice no agrupado, el punto muerto sigue ahí (aunque la tasa de aciertos parece haberse reducido)

Tabla de datos:

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
)

Rastro de punto muerto

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

También un hallazgo interesante y posible relacionado es que el índice agrupado y no agrupado parece tener diferentes comportamientos de bloqueo

Cuando se usa el índice agrupado, hay un bloqueo exclusivo en la clave, así como un bloqueo exclusivo en RID cuando se actualiza, lo que se espera; mientras que hay dos bloqueos exclusivos en dos RID diferentes si se utiliza un índice no agrupado, lo que me confunde.

Sería útil si alguien puede explicar por qué en esto también.

Prueba SQL:

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

Con id como índice agrupado:

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

Con id como índice no agrupado

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

EDITAR1: Detalles del punto muerto sin ningún índice.
Digamos que tengo dos tx A y B, cada uno con dos declaraciones de actualización, una fila diferente, por supuesto,
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} y {4} tendrían una posibilidad de punto muerto, ya que

en {1}, se solicita el bloqueo U para la fila 63502 ya que necesita hacer un escaneo de la tabla, y el bloqueo X podría haberse mantenido en la fila 63501 ya que coincide con la condición

en {4}, se solicita el bloqueo U para la fila 63501 y el bloqueo X ya se mantiene para 63502

entonces tenemos txA contiene 63501 y espera 63502 mientras que txB contiene 63502 esperando 63501, que es un punto muerto

EDIT2: Resulta que un error de mi caso de prueba hace una situación diferente aquí Perdón por la confusión, pero el error hace una situación diferente, y parece causar el punto muerto eventualmente.

Como el análisis de Paul realmente me ayudó en este caso, lo aceptaré como respuesta.

Debido al error de mi caso de prueba, dos transacciones txA y txB pueden actualizar la misma fila, como se muestra a continuación:

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} y {3} tendrían una posibilidad de punto muerto cuando:

txA solicita bloqueo U en la tecla mientras mantiene el bloqueo X en RID (debido a la actualización de {1}) txB solicita bloqueo U en RID mientras mantiene el bloqueo U en la tecla

Bood
fuente
1
No puedo entender por qué una transacción necesita actualizar la misma fila dos veces.
ypercubeᵀᴹ
@ypercube Buen punto, eso es algo que debería mejorar. Pero en este caso solo quiero tener una mejor comprensión de los comportamientos de bloqueo
Bood
@ypercube después de más pensamientos, creo que es posible que una aplicación con lógica compleja necesite actualizar la misma fila dos veces en el mismo tx, podría ser columnas diferentes, por ejemplo
Bood

Respuestas:

16

... por qué con el índice agrupado, el punto muerto sigue ahí (aunque la tasa de aciertos parece haberse reducido)

La pregunta no es clara (por ejemplo, cuántas actualizaciones y qué idvalores hay en cada transacción), pero surge un escenario de punto muerto obvio con múltiples actualizaciones de una sola fila dentro de una sola transacción, donde hay una superposición de [id]valores, y los identificadores son actualizado en un [id]orden diferente :

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

Secuencia de bloqueo: T1 (u2), T2 (u1), T1 (u1) espera , T2 (u2) espera .

Esta secuencia de bloqueo podría evitarse actualizando estrictamente en orden de identificación dentro de cada transacción (adquiriendo bloqueos en el mismo orden en la misma ruta).

Cuando se usa el índice agrupado, hay un bloqueo exclusivo en la clave, así como un bloqueo exclusivo en RID cuando se actualiza, lo que se espera; mientras que hay dos bloqueos exclusivos en dos RID diferentes si se utiliza un índice no agrupado, lo que me confunde.

Con un índice agrupado único activado, se activa idun bloqueo exclusivo en la clave de agrupamiento para proteger las escrituras en los datos de la fila. Se RIDrequiere un bloqueo exclusivo por separado para proteger la escritura en la textcolumna LOB , que se almacena en una página de datos separada de forma predeterminada.

Cuando la tabla es un montón con solo un índice no agrupado activado id, suceden dos cosas. Primero, un RIDbloqueo exclusivo se relaciona con los datos de la fila del montón, y el otro es el bloqueo de los datos LOB como antes. El segundo efecto es que se requiere un plan de ejecución más complejo.

Con un índice agrupado y una actualización simple de predicado de igualdad de valor único, el procesador de consultas puede aplicar una optimización que realiza la actualización (lectura y escritura) en un solo operador, utilizando una única ruta:

Actualización de operador único

La fila se ubica y actualiza en una sola operación de búsqueda, que requiere solo bloqueos exclusivos (no se necesitan bloqueos de actualización). Un ejemplo de secuencia de bloqueo usando su tabla de muestra:

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

Con solo un índice no agrupado, no se puede aplicar la misma optimización porque necesitamos leer de una estructura b-tree y escribir otra. El plan de múltiples rutas tiene fases separadas de lectura y escritura:

Actualización de iteradores múltiples

Esto adquiere bloqueos de actualización al leer, convirtiéndolos en bloqueos exclusivos si la fila califica. Ejemplo de secuencia de bloqueo con el esquema dado:

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 

Tenga en cuenta que los datos de LOB se leen y escriben en el iterador Actualización de tabla. El plan más complejo y las múltiples rutas de lectura y escritura aumentan las posibilidades de un punto muerto.

Finalmente, no puedo evitar notar los tipos de datos utilizados en la definición de la tabla. No debe usar el texttipo de datos en desuso para un nuevo trabajo; la alternativa, si realmente necesita la capacidad de almacenar hasta 2 GB de datos en esta columna, es varchar(max). Una diferencia importante entre texty varchar(max)es que los textdatos se almacenan fuera de la fila de forma predeterminada, mientras que se varchar(max)almacenan en fila de forma predeterminada.

Utilice los tipos Unicode solo si necesita esa flexibilidad (por ejemplo, es difícil ver por qué una dirección IP necesitaría Unicode). Además, elija los límites de longitud apropiados para sus atributos: 255 en todas partes parece poco probable que sea correcto.

Lectura adicional:
Deadlock y livelock patrones comunes
Serie de solución de problemas de deadlock de Bart Duncan

Los bloqueos de rastreo se pueden realizar de varias maneras. SQL Server Express con servicios avanzados (solo 2014 y 2012 SP1 en adelante ) contiene la herramienta Profiler , que es una forma compatible de ver los detalles de la adquisición y liberación de bloqueos.

Paul White 9
fuente
Excelente respuesta ¿Cómo está generando los registros / trazas que tienen los mensajes "adquiriendo ... bloqueo" y "liberando referencia de bloqueo"?
Sanjiv Jivan