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?
Respuestas:
Parece un poco extraño, pero es válido. En el momento en que
IX
se toma, la intención puede ser tomarX
cerraduras en un nivel inferior. No hay nada que decir que tales cerraduras deben ser tomadas. Después de todo, puede que no haya nada que bloquear en el nivel inferior; el motor no puede saber eso antes de tiempo. Además, puede haber optimizaciones tal que inferior se asegure de nivel se pueden saltar (un ejemplo paraIS
yS
las cerraduras se pueden ver aquí ).Más específicamente para el escenario actual, es cierto que los bloqueos de rango de clave serializables no están disponibles para un montón, por lo que la única alternativa es un
X
bloqueo a nivel de objeto. En ese sentido, el motor podría detectar temprano queX
inevitablemente se requerirá un bloqueo si el método de acceso es una exploración de montón, y así evitar tomar elIX
bloqueo.Por otro lado, el bloqueo es complejo, y los bloqueos de intención a veces se pueden tomar por razones internas no necesariamente relacionadas con la intención de tomar bloqueos de nivel inferior. Tomar
IX
puede ser la forma menos invasiva de proporcionar una protección requerida para algunos casos de borde oscuro. Para un tipo de consideración similar, vea Bloqueo compartido emitido en IsolationLevel.ReadUncommitted .Entonces, la situación actual es desafortunada para su escenario de punto muerto, y puede ser evitable en principio, pero eso no es necesariamente lo mismo que ser un 'error'. Puede informar el problema a través de su canal de soporte normal, o en Microsoft Connect, si necesita una respuesta definitiva al respecto.
No.
FORCESEEK
es menos una pista y más una directiva. Si el optimizador no puede encontrar un plan que respete la 'pista', producirá un error.Forzar el índice es una forma de garantizar que se puedan tomar bloqueos de rango de claves. Junto con los bloqueos de actualización que se toman naturalmente al procesar un método de acceso para que cambien las filas, esto proporciona una garantía suficiente para evitar problemas de concurrencia en su escenario.
Si el esquema de la tabla no cambia (por ejemplo, agregar un nuevo índice), la sugerencia también es suficiente para evitar que esta consulta se interrumpa. Todavía existe la posibilidad de un punto muerto cíclico con otras consultas que puedan acceder al montón antes del índice no agrupado (como una actualización de la clave del índice no agrupado).
Esto rompe la garantía de que una sola fila se verá afectada, por lo que se introduce un Eager Table Spool para la protección de Halloween. Como solución alternativa para esto, haga explícita la garantía con
MERGE TOP (1) INTO [Cache]...
.Ciertamente, hay mucho más en juego que es visible en un plan de ejecución. Puede forzar una determinada forma de plan con, por ejemplo, una guía de plan, pero el motor aún puede decidir tomar diferentes bloqueos en tiempo de ejecución. Las posibilidades son bastante bajas si incorpora el
TOP (1)
elemento anterior.Observaciones generales
Es algo inusual ver una tabla de montón utilizada de esta manera. Debería considerar los méritos de convertirlo en una tabla agrupada, tal vez utilizando el índice que Dan Guzman sugirió en un comentario:
Esto puede tener importantes ventajas de reutilización de espacio, así como proporcionar una buena solución para el problema actual de bloqueo.
MERGE
También es un poco inusual ver en un entorno de alta concurrencia. Algo contra-intuitivo, a menudo es más eficiente realizar separacionesINSERT
yUPDATE
declaraciones, por ejemplo:Tenga en cuenta que la búsqueda de RID ya no es necesaria:
Si puede garantizar la existencia de un índice único en
ItemKey
(como en la pregunta), el redundanteTOP (1)
en elUPDATE
puede eliminarse, dando el plan más simple:Ambos
INSERT
y losUPDATE
planes califican para un plan trivial en cualquier caso.MERGE
siempre requiere una optimización completa basada en el costo.Consulte el problema relacionado de entrada simultánea de preguntas y respuestas de SQL Server 2014 para conocer el patrón correcto que debe usar y obtener más información al respecto
MERGE
.Los puntos muertos no siempre se pueden evitar. Se pueden reducir al mínimo con una codificación y diseño cuidadosos, pero la aplicación siempre debe estar preparada para manejar el punto muerto impar con gracia (por ejemplo, vuelva a verificar las condiciones y vuelva a intentarlo).
Si tiene control completo sobre los procesos que acceden al objeto en cuestión, también puede considerar el uso de bloqueos de aplicaciones para serializar el acceso a elementos individuales, como se describe en Inserciones y eliminaciones concurrentes de SQL Server .
fuente