Resolver puntos muertos de 2 tablas solo relacionadas a través de la vista indexada


17

Tengo una situación en la que obtengo puntos muertos y creo que he reducido los culpables, pero no estoy muy seguro de qué puedo hacer para solucionarlo.

Esto está en un entorno de producción que ejecuta SQL Server 2008 R2.

Para darle una visión ligeramente simplificada de la situación:


Tengo 3 tablas como se define a continuación:

TABLE activity (
    id, -- PK
    ...
)

TABLE member_activity (
    member_id, -- PK col 1
    activity_id, -- PK col 2
    ...
)

TABLE follow (
    id, -- PK
    follower_id,
    member_id,
    ...
)

La member_activitytabla tiene una clave primaria compuesta definida comomember_id, activity_id , porque solo necesito buscar datos en esa tabla de esa manera.

También tengo un índice no agrupado en follow:

CREATE NONCLUSTERED INDEX [IX_follow_member_id_includes] 
ON follow ( member_id ASC ) INCLUDE ( follower_id )

Además, tengo una vista vinculada al esquema network_activityque se define de la siguiente manera:

CREATE VIEW network_activity
WITH SCHEMABINDING
AS

SELECT
    follow.follower_id as member_id,
    member_activity.activity_id as activity_id,
    COUNT_BIG(*) AS cb
FROM member_activity
INNER JOIN follow ON follow.member_id = member_activity.member_id
INNER JOIN activity ON activity.id = member_activity.activity_id
GROUP BY follow.follower_id, member_activity.activity_id

Que también tiene un índice agrupado único:

CREATE UNIQUE CLUSTERED INDEX [IX_network_activity_unique_member_id_activity_id] 
ON network_activity
(
    member_id ASC,
    activity_id ASC
)

Ahora, tengo dos procedimientos almacenados bloqueados. Pasan por el siguiente proceso:

-- SP1: insert activity
-----------------------
INSERT INTO activity (...)
SELECT ... FROM member_activity WHERE member_id = @a AND activity_id = @b
INSERT INTO member_activity (...)


-- SP2: insert follow
---------------------
SELECT follow WHERE member_id = @x AND follower_id = @y
INSERT INTO follow (...)

Estos 2 procedimientos se ejecutan en aislamiento READ COMMITTED. Me las arreglé para consultar la salida de 1222 eventos extendidos, y he interpretado lo siguiente con respecto a los puntos muertos:

SP1 está esperando un RangeS-Sbloqueo de teclas en elIX_follow_member_id_includes índice mientras que SP2 tiene un conflicto (X)

SP2 espera un Sbloqueo de modo activado PK_member_activity mientras SP1 mantiene un bloqueo conflictivo (X)

El punto muerto parece estar sucediendo en la última línea de cada consulta (las inserciones). Lo que no está claro para mí es por qué SP1 quiere un bloqueo en elIX_follow-member_id_includes índice. El único enlace, para mí, parece ser de esta vista indexada y es por eso que lo he incluido.

¿Cuál sería la mejor manera para mí de evitar que ocurran estos puntos muertos? Cualquier ayuda sería muy apreciada. No tengo mucha experiencia en resolver problemas de punto muerto.

¡Avíseme si hay más información que pueda proporcionar que pueda ayudar!

Gracias por adelantado.


Edición 1: Agregar más información por solicitud.

Aquí está la salida 1222 de este punto muerto:

<deadlock>
    <victim-list>
        <victimProcess id="process4c6672748" />
    </victim-list>
    <process-list>
        <process id="process4c6672748" taskpriority="0" logused="332" waitresource="KEY: 8:72057594104905728 (25014f77eaba)" waittime="581" ownerId="474698706" transactionname="INSERT" lasttranstarted="2014-07-03T17:03:12.287" XDES="0x298487970" lockMode="RangeS-S" schedulerid="1" kpid="972" status="suspended" spid="79" sbid="0" ecid="0" priority="0" trancount="2" lastbatchstarted="2014-07-03T17:03:12.283" lastbatchcompleted="2014-07-03T17:03:12.283" lastattention="2014-07-03T10:25:00.283" clientapp=".Net SqlClient Data Provider" hostname="WIN08CLYDESDALE" hostpid="4596" loginname="TechPro" isolationlevel="read committed (2)" xactid="474698706" currentdb="8" lockTimeout="4294967295" clientoption1="671088672" clientoption2="128056">
            <executionStack>
                <frame procname="" line="7" stmtstart="1194" stmtend="1434" sqlhandle="0x02000000a26bb72a2b220406876cad09c22242e5265c82e6" />
                <frame procname="" line="1" sqlhandle="0x000000000000000000000000000000000000000000000000" />
            </executionStack>
            <inputbuf> <!-- SP 1 --> </inputbuf>
        </process>
        <process id="process6cddc5b88" taskpriority="0" logused="456" waitresource="KEY: 8:72057594098679808 (89013169fc76)" waittime="567" ownerId="474698698" transactionname="INSERT" lasttranstarted="2014-07-03T17:03:12.283" XDES="0x30c459970" lockMode="S" schedulerid="4" kpid="4204" status="suspended" spid="70" sbid="0" ecid="0" priority="0" trancount="2" lastbatchstarted="2014-07-03T17:03:12.283" lastbatchcompleted="2014-07-03T17:03:12.283" lastattention="2014-07-03T15:04:55.870" clientapp=".Net SqlClient Data Provider" hostname="WIN08CLYDESDALE" hostpid="4596" loginname="TechPro" isolationlevel="read committed (2)" xactid="474698698" currentdb="8" lockTimeout="4294967295" clientoption1="673185824" clientoption2="128056">
            <executionStack>
                <frame procname="" line="18" stmtstart="942" stmtend="1250" sqlhandle="0x03000800ca458d315ee9130100a300000100000000000000" />
            </executionStack>
            <inputbuf> <!-- SP 2 --> </inputbuf>
        </process>
    </process-list>
    <resource-list>
        <keylock hobtid="72057594104905728" dbid="8" objectname="" indexname="" id="lock33299fc00" mode="X" associatedObjectId="72057594104905728">
            <owner-list>
                <owner id="process6cddc5b88" mode="X" />
            </owner-list>
            <waiter-list>
                <waiter id="process4c6672748" mode="RangeS-S" requestType="wait" />
            </waiter-list>
        </keylock>
        <keylock hobtid="72057594098679808" dbid="8" objectname="" indexname="" id="lockb7e2ba80" mode="X" associatedObjectId="72057594098679808">
            <owner-list>
                <owner id="process4c6672748" mode="X" />
            </owner-list>
            <waiter-list>
                <waiter id="process6cddc5b88" mode="S" requestType="wait" />
            </waiter-list>
        </keylock>
    </resource-list>
</deadlock>

En este caso,

AssociatedObjectId 72057594098679808 corresponde a member_activity, PK_member_activity

AssociatedObjectId 72057594104905728 corresponde a follow, IX_follow_member_id_includes

Además, aquí hay una imagen más precisa de lo que están haciendo SP1 y SP2

-- SP1: insert activity
-----------------------
DECLARE @activityId INT

INSERT INTO activity (field1, field2)
VALUES (@field1, @field2)

SET @activityId = SCOPE_IDENTITY();

IF NOT EXISTS(
    SELECT TOP 1 member_id 
    FROM member_activity 
    WHERE member_id = @m1 AND activity_id = @activityId
)
    INSERT INTO member_activity (member_id, activity_id, field1)
    VALUES (@m1, @activityId, @field1)

IF NOT EXISTS(
    SELECT TOP 1 member_id 
    FROM member_activity 
    WHERE member_id = @m2 AND activity_id = @activityId
)
    INSERT INTO member_activity (member_id, activity_id, field1)
    VALUES (@m2, @activityId, @field1)

también SP2:

-- SP2: insert follow
---------------------

IF NOT EXISTS(
    SELECT TOP 1 1 
    FROM follow
    WHERE member_id = @memberId AND follower_id = @followerId
)
    INSERT INTO follow (member_id, follower_id)
    VALUES (@memberId, @followerId)

Edición 2: después de releer los comentarios, pensé que también agregaría información sobre qué columnas son claves foráneas ...

  • member_activity.member_id es una clave foránea para un membermesa
  • member_activity.activity_id es una clave foránea para activity mesa
  • follow.member_id es una clave foránea para un membermesa
  • follow.follower_ides una clave foránea para una membermesa

Actualización 1:

Hice un par de cambios que pensé que podrían ayudar a evitar el punto muerto, sin suerte.

Los cambios que hice fueron los siguientes:

-- SP1: insert activity
-----------------------
DECLARE @activityId INT

INSERT INTO activity (field1, field2)
VALUES (@field1, @field2)

SET @activityId = SCOPE_IDENTITY();

MERGE member_activity WITH ( HOLDLOCK ) as target
USING (SELECT @m1 as member_id, @activityId as activity_id, @field1 as field1) as source
    ON target.member_id = source.member_id
    AND target.activity_id = source.activity_id
WHEN NOT MATCHED THEN
    INSERT (member_id, activity_id, field1)
    VALUES (source.member_id, source.activity_id, source.field1)
;

MERGE member_activity WITH ( HOLDLOCK ) as target
USING (SELECT @m2 as member_id, @activityId as activity_id, @field1 as field1) as source
    ON target.member_id = source.member_id
    AND target.activity_id = source.activity_id
WHEN NOT MATCHED THEN
    INSERT (member_id, activity_id, field1)
    VALUES (source.member_id, source.activity_id, source.field1)
;

y con SP2:

-- SP2: insert follow
---------------------

SET TRANSACTION ISOLATION LEVEL SERIALIZABLE;
BEGIN TRANSACTION

IF NOT EXISTS(
    SELECT TOP 1 1 
    FROM follow WITH ( UPDLOCK )
    WHERE member_id = @memberId AND follower_id = @followerId
)
    INSERT INTO follow (member_id, follower_id)
    VALUES (@memberId, @followerId)

COMMIT

Con estos dos cambios, todavía parece que estoy llegando a un punto muerto.

Si hay algo más que pueda proporcionar, hágamelo saber. Gracias.


lectura comprometida no toma bloqueos de rango de clave, solo lo hace serializable. Si el punto muerto muestra de hecho lectura confirmada (2), entonces supongo que está accediendo a cambiar una clave externa que se convertirá en serializable debajo de las cubiertas (aunque todavía diga lectura confirmada). Sinceramente, necesitaríamos todo el ddl y sp para ayudar más.
Sean dice Eliminar Sara Chipps

@SeanGallardy, gracias. He editado para incluir la salida 1222 en caso de que estuviera interpretando mal, y he agregado más detalles sobre lo que están haciendo los SP. ¿Esto ayuda?
Leland Richardson el

2
@SeanGallardy La parte del plan de consulta que mantiene la vista indizada se ejecuta internamente en SERIALIZABLE(hay un poco más que eso, pero este es un comentario, no una respuesta :)
Paul White reinstala a Monica

@PaulWhite Gracias por la información, ¡no lo sabía! Haciendo una prueba rápida, definitivamente puedo obtener los modos de bloqueo serializables con la vista indexada durante la inserción en sus procedimientos almacenados (RangeI-N, RangeS-S, RangeS-U). Parece que el punto muerto está sucediendo debido a que los modos de bloqueo incompatibles golpean el uno contra el otro en el momento correcto durante las inserciones en sus procedimientos almacenados cuando caen dentro de los límites de bloqueo (por ejemplo, en el área mantenida por el bloqueo de rango). Creo que tanto un momento como una colisión de datos de entrada.
Sean dice Eliminar Sara Chipps

Pregunta: Si agrego una sugerencia HOLDLOCK en las instrucciones SELECT, ¿eso evitaría que el bloqueo ocurra en la inserción?
Leland Richardson

Respuestas:


5

El conflicto se reduce a network_activityser una Vista indizada que debe mantenerse (internamente) en todas las declaraciones DML. Es muy probable que SP1 quiera un bloqueo en elIX_follow-member_id_includes índice, ya que probablemente lo usa la Vista (parece ser un índice de cobertura para la Vista).

Dos posibles opciones:

  1. Considere soltar el índice agrupado en la vista para que ya no sea una vista indizada. ¿El beneficio de tenerlo supera el costo de mantenimiento? ¿Seleccionas con suficiente frecuencia o vale la pena el aumento de rendimiento de tenerlo indexado? Si ejecuta estos procs con bastante frecuencia, ¿quizás el costo sea más alto que el beneficio?

  2. Si el beneficio de indexar la Vista supera el costo, considere aislar las operaciones DML contra las tablas base de esa Vista. Esto se puede hacer mediante el uso de bloqueos de aplicación (ver sp_getapplock y sp_releaseapplock ). Los bloqueos de aplicación le permiten crear bloqueos en torno a conceptos arbitrarios. Es decir, puede definir @Resourcecomo "network_activity" en ambos Procs almacenados, lo que los obligará a esperar su turno. Cada proceso seguiría la misma estructura:

    BEGIN TRANSACTION;
    EXEC sp_getapplock @Resource = 'network_activity', @LockMode = 'Exclusive';
    ...current proc code...
    EXEC sp_releaseapplock @Resource = 'network_activity';
    COMMIT TRANSACTION;

    Debe administrar los errores / ROLLBACKusted mismo (como se indica en la documentación vinculada de MSDN), así que ingrese lo habitual TRY...CATCH. Pero, esto le permite manejar la situación.
    Tenga en cuenta: sp_getapplock / sp_releaseapplockdebe usarse con moderación; Los bloqueos de aplicaciones definitivamente pueden ser muy útiles (como en casos como este), pero solo deben usarse cuando sea absolutamente necesario.


Gracias por la ayuda. Voy a leer un poco más sobre la opción # 2 y ver si eso funciona para nosotros. La vista se lee bastante, y el índice agrupado es de gran ayuda ... así que prefiero no eliminarlo todavía. Volveré una actualización una vez que lo pruebe.
Leland Richardson

Creo que usar sp_getapplock funcionará. Todavía no he podido probarlo en nuestro entorno de producción, pero quería asegurarme de que obtuviera la recompensa antes de que expirara. ¡Actualizaré aquí cuando pueda confirmar que funciona!
Leland Richardson

Gracias. Una cosa buena acerca de los bloqueos de aplicaciones es que puede cambiar el nivel de granularidad de concatenación en algo como member_idel @Resourcevalor. Eso no parece aplicarse a esta situación en particular, pero he visto que se usa así y es bastante útil, especialmente en un sistema de múltiples inquilinos donde desea limitar el proceso a un solo hilo por cliente, pero todavía tiene que ser multiproceso en todos los clientes.
Solomon Rutzky

Quería darle una actualización y decir que esto se terminan trabajando en nuestro entorno de producción. :)
Leland Richardson
Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.