En una de nuestras bases de datos tenemos una tabla a la que se accede de manera simultánea de manera simultánea por múltiples hilos. Los hilos actualizan o insertan filas a través de MERGE
. También hay hilos que eliminan filas ocasionalmente, por lo que los datos de la tabla son muy volátiles. Los subprocesos que hacen upserts a veces sufren de interbloqueo. El problema es similar al descrito en esta pregunta. La diferencia, sin embargo, es que en nuestro caso cada hilo actualiza o inserta exactamente una fila .
La configuración simplificada está siguiendo. La tabla es un montón con dos índices únicos no agrupados sobre
CREATE TABLE [Cache]
(
[UID] uniqueidentifier NOT NULL CONSTRAINT DF_Cache_UID DEFAULT (newid()),
[ItemKey] varchar(200) NOT NULL,
[FileName] nvarchar(255) NOT NULL,
[Expires] datetime2(2) NOT NULL,
CONSTRAINT [PK_Cache] PRIMARY KEY NONCLUSTERED ([UID])
)
GO
CREATE UNIQUE INDEX IX_Cache ON [Cache] ([ItemKey]);
GO
y la consulta típica es
DECLARE
@itemKey varchar(200) = 'Item_0F3C43A6A6A14255B2EA977EA730EDF2',
@fileName nvarchar(255) = 'File_0F3C43A6A6A14255B2EA977EA730EDF2.dat';
MERGE INTO [Cache] WITH (HOLDLOCK) T
USING (
VALUES (@itemKey, @fileName, dateadd(minute, 10, sysdatetime()))
) S(ItemKey, FileName, Expires)
ON T.ItemKey = S.ItemKey
WHEN MATCHED THEN
UPDATE
SET
T.FileName = S.FileName,
T.Expires = S.Expires
WHEN NOT MATCHED THEN
INSERT (ItemKey, FileName, Expires)
VALUES (S.ItemKey, S.FileName, S.Expires)
OUTPUT deleted.FileName;
es decir, la coincidencia ocurre por clave de índice única. La sugerencia HOLDLOCK
está aquí, debido a la concurrencia (como se recomienda aquí ).
Hice una pequeña investigación y lo siguiente es lo que encontré.
En la mayoría de los casos, el plan de ejecución de consultas es
con el siguiente patrón de bloqueo
es decir, IX
bloquear el objeto seguido de bloqueos más granulares.
A veces, sin embargo, el plan de ejecución de consultas es diferente.
(esta forma de plan se puede forzar agregando una INDEX(0)
pista) y su patrón de bloqueo es
observe el X
bloqueo colocado en el objeto después de IX
que ya esté colocado.
Como dos IX
son compatibles, pero dos X
no, lo que sucede bajo concurrencia es
punto muerto !
Y aquí surge la primera parte de la pregunta . ¿Colocar el X
candado en el objeto después de ser IX
elegible? ¿No es un error?
Documentación declara:
Los bloqueos de intención se denominan bloqueos de intención porque se adquieren antes de un bloqueo en el nivel inferior y, por lo tanto, indican la intención de colocar bloqueos en un nivel inferior .
y tambien
IX significa la intención de actualizar solo algunas de las filas en lugar de todas
Por lo tanto, colocar el X
candado en el objeto después me IX
parece MUY sospechoso.
Primero intenté evitar el bloqueo al intentar agregar sugerencias de bloqueo de tabla
MERGE INTO [Cache] WITH (HOLDLOCK, TABLOCK) T
y
MERGE INTO [Cache] WITH (HOLDLOCK, TABLOCKX) T
con el TABLOCK
patrón de bloqueo en su lugar se convierte
y con el TABLOCKX
patrón de bloqueo es
Dado que dos SIX
(así como dos X
) no son compatibles, esto evita un punto muerto efectivo, pero, desafortunadamente, también evita la concurrencia (lo cual no es deseable).
Mis próximos intentos fueron agregar PAGLOCK
y ROWLOCK
hacer bloqueos más granulares y reducir la contención. Ambos no tienen ningún efecto ( X
en el objeto todavía se observó inmediatamente después IX
).
Mi intento final fue forzar una "buena" forma de plan de ejecución con un buen bloqueo granular al agregar una FORCESEEK
pista
MERGE INTO [Cache] WITH (HOLDLOCK, FORCESEEK(IX_Cache(ItemKey))) T
Y funcionó.
Y aquí surge la segunda parte de la pregunta . ¿Podría suceder que FORCESEEK
se ignorará y se utilizará un patrón de bloqueo incorrecto? (Como mencioné, PAGLOCK
y ROWLOCK
aparentemente fueron ignorados).
Agregar UPDLOCK
no tiene ningún efecto ( X
en el objeto aún observable después IX
).
Hacer el IX_Cache
índice agrupado, como se anticipó, funcionó. Condujo al plan con Clustered Index Seek y bloqueo granular. Además, intenté forzar el Análisis de índice agrupado que también mostraba bloqueo granular.
Sin embargo. Observación adicional En la configuración original incluso con el FORCESEEK(IX_Cache(ItemKey)))
lugar, si una @itemKey
declaración de variable de cambio de varchar (200) a nvarchar (200) , el plan de ejecución se convierte en
vea que se utiliza la búsqueda, PERO el patrón de bloqueo en este caso nuevamente muestra el X
bloqueo colocado en el objeto después IX
.
Entonces, parece que forzar la búsqueda no necesariamente garantiza bloqueos granulares (y la ausencia de puntos muertos por lo tanto). No estoy seguro de que tener un índice agrupado garantice el bloqueo granular. O lo hace?
Mi comprensión (corríjame si me equivoco) es que el bloqueo es situacional en gran medida, y cierta forma de plan de ejecución no implica cierto patrón de bloqueo.
La pregunta sobre la elegibilidad de colocar un X
candado en el objeto después de IX
abrir. Y si es elegible, ¿hay algo que uno pueda hacer para evitar el bloqueo de objetos?