MERGE prevención de bloqueo

9

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 HOLDLOCKestá 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

plan de ejecución de búsqueda de índice

con el siguiente patrón de bloqueo

patrón de bloqueo de búsqueda de índice

es decir, IXbloquear el objeto seguido de bloqueos más granulares.

A veces, sin embargo, el plan de ejecución de consultas es diferente.

plan de ejecución de escaneo de tabla

(esta forma de plan se puede forzar agregando una INDEX(0)pista) y su patrón de bloqueo es

patrón de bloqueo de escaneo de tabla

observe el Xbloqueo colocado en el objeto después de IXque ya esté colocado.

Como dos IXson compatibles, pero dos Xno, lo que sucede bajo concurrencia es

punto muerto

gráfico de punto muerto

punto muerto !

Y aquí surge la primera parte de la pregunta . ¿Colocar el Xcandado en el objeto después de ser IXelegible? ¿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 Xcandado en el objeto después me IXparece 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 TABLOCKpatrón de bloqueo en su lugar se convierte

fusionar patrón de bloqueo de bloqueo de bloqueo

y con el TABLOCKXpatrón de bloqueo es

fusionar patrón de bloqueo tablockx holdlock

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 PAGLOCKy ROWLOCKhacer bloqueos más granulares y reducir la contención. Ambos no tienen ningún efecto ( Xen 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 FORCESEEKpista

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 FORCESEEKse ignorará y se utilizará un patrón de bloqueo incorrecto? (Como mencioné, PAGLOCKy ROWLOCKaparentemente fueron ignorados).


Agregar UPDLOCKno tiene ningún efecto ( Xen 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 @itemKeydeclaración de variable de cambio de varchar (200) a nvarchar (200) , el plan de ejecución se convierte en

plan de ejecución de búsqueda de índice con nvarchar

vea que se utiliza la búsqueda, PERO el patrón de bloqueo en este caso nuevamente muestra el Xbloqueo 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 Xcandado en el objeto después de IXabrir. Y si es elegible, ¿hay algo que uno pueda hacer para evitar el bloqueo de objetos?

i-one
fuente
Solicitud relacionada en feedback.azure.com
i-one

Respuestas:

9

¿La colocación IXseguida Xen el objeto es elegible? ¿Es un error o no?

Parece un poco extraño, pero es válido. En el momento en que IXse toma, la intención puede ser tomar Xcerraduras 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 para ISy Slas 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 Xbloqueo a nivel de objeto. En ese sentido, el motor podría detectar temprano que Xinevitablemente se requerirá un bloqueo si el método de acceso es una exploración de montón, y así evitar tomar el IXbloqueo.

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 IXpuede 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.

¿Podría suceder que FORCESEEKse ignorará y se utilizará un patrón de bloqueo incorrecto?

No. FORCESEEKes 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).

... declaración variable de varchar(200)a nvarchar(200)...

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]....

Entiendo que el bloqueo es situacional en gran medida, y cierta forma de plan de ejecución no implica cierto patrón de bloqueo.

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:

CREATE UNIQUE CLUSTERED INDEX IX_Cache ON [Cache] ([ItemKey]);

Esto puede tener importantes ventajas de reutilización de espacio, así como proporcionar una buena solución para el problema actual de bloqueo.

MERGETambién es un poco inusual ver en un entorno de alta concurrencia. Algo contra-intuitivo, a menudo es más eficiente realizar separaciones INSERTy UPDATEdeclaraciones, por ejemplo:

DECLARE
    @itemKey varchar(200) = 'Item_0F3C43A6A6A14255B2EA977EA730EDF2',
    @fileName nvarchar(255) = 'File_0F3C43A6A6A14255B2EA977EA730EDF2.dat';

BEGIN TRANSACTION;

    DECLARE @expires datetime2(2) = DATEADD(MINUTE, 10, SYSDATETIME());

    UPDATE TOP (1) dbo.Cache WITH (SERIALIZABLE, UPDLOCK)
    SET [FileName] = @fileName,
        Expires = @expires
    OUTPUT Deleted.[FileName]
    WHERE
        ItemKey = @itemKey;

    IF @@ROWCOUNT = 0
        INSERT dbo.Cache
            (ItemKey, [FileName], Expires)
        VALUES
            (@itemKey, @fileName, @expires);

COMMIT TRANSACTION;

Tenga en cuenta que la búsqueda de RID ya no es necesaria:

Plan de ejecución

Si puede garantizar la existencia de un índice único en ItemKey(como en la pregunta), el redundante TOP (1)en el UPDATEpuede eliminarse, dando el plan más simple:

Actualización simplificada

Ambos INSERTy los UPDATEplanes califican para un plan trivial en cualquier caso. MERGEsiempre 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 .

Paul White 9
fuente