Estrategias para "retirar" registros para procesamiento

10

No estoy seguro de si hay un patrón con nombre para esto, o si no lo hay porque es una idea terrible. Pero necesito mi servicio para operar en un entorno activo / activo de carga equilibrada. Este es solo el servidor de aplicaciones. La base de datos estará en un servidor separado. Tengo un servicio que necesitará ejecutar un proceso para cada registro en una tabla. Este proceso puede tomar uno o dos minutos y se repetirá cada n minutos (configurable, generalmente 15 minutos).

Con una tabla de 1000 registros que necesita este procesamiento y dos servicios que se ejecutan en este mismo conjunto de datos, me gustaría que cada servicio "revise" un registro para procesar. Necesito asegurarme de que solo un servicio / hilo procese cada registro a la vez.

Tengo colegas que han usado una "mesa de bloqueo" en el pasado. Donde se escribe un registro en esta tabla para bloquear lógicamente el registro en la otra tabla (esa otra tabla es bastante estática por cierto, y con un nuevo registro muy ocasional agregado), y luego se elimina para liberar el bloqueo.

Me pregunto si no sería mejor que la nueva tabla tenga una columna que indique cuándo se bloqueó y que esté bloqueada actualmente, en lugar de insertar una eliminación constantemente.

¿Alguien tiene consejos para este tipo de cosas? ¿Existe un patrón establecido para el bloqueo lógico a largo plazo (ish)? ¿Algún consejo sobre cómo garantizar que solo un servicio agarre la cerradura a la vez? (Mi colega usa TABLOCKX para bloquear toda la tabla).

Decano
fuente

Respuestas:

12

No soy un gran admirador ni de la tabla de "bloqueo" adicional ni de la idea de bloquear toda la tabla para obtener el próximo récord. Entiendo por qué se está haciendo, pero eso también perjudica la concurrencia para las operaciones que se están actualizando para liberar un registro bloqueado (seguramente dos procesos no pueden estar peleando por eso cuando no es posible que dos procesos hayan bloqueado el mismo registro en el Mismo tiempo).

Mi preferencia sería agregar una columna ProcessStatusID (típicamente TINYINT) a la tabla con los datos que se procesan. ¿Y hay un campo para LastModifiedDate? Si no, entonces debe agregarse. En caso afirmativo, ¿se actualizan estos registros fuera de este proceso? Si los registros se pueden actualizar fuera de este proceso en particular, entonces se debe agregar otro campo para rastrear StatusModifiedDate (o algo así). Para el resto de esta respuesta, solo usaré "StatusModifiedDate", ya que tiene un significado claro (y de hecho, podría usarse como el nombre del campo incluso si actualmente no hay un campo "LastModifiedDate").

Los valores para ProcessStatusID (que deben colocarse en una nueva tabla de búsqueda llamada "ProcessStatus" y Foreign Keyed to this table) podrían ser:

  1. Completado (o incluso "Pendiente" en este caso, ya que ambos significan "listo para ser procesado")
  2. En proceso (o "Procesamiento")
  3. Error (o "WTF?")

En este punto, parece seguro asumir que desde la aplicación, solo quiere obtener el siguiente registro para procesar y no pasará nada para ayudar a tomar esa decisión. Por lo tanto, queremos obtener el registro más antiguo (al menos en términos de StatusModifiedDate) que se establece en "Completado" / "Pendiente". Algo en la línea de:

SELECT TOP 1 pt.RecordID
FROM   ProcessTable pt
WHERE  pt.StatusID = 1
ORDER BY pt.StatusModifiedDate ASC;

También queremos actualizar ese registro a "En proceso" al mismo tiempo para evitar que el otro proceso lo tome. Podríamos usar la OUTPUTcláusula para permitirnos hacer la ACTUALIZACIÓN y SELECCIONAR en la misma transacción:

UPDATE TOP (1) pt
SET    pt.StatusID = 2,
       pt.StatusModifiedDate = GETDATE() -- or GETUTCDATE()
OUTPUT INSERTED.RecordID
FROM   ProcessTable pt
WHERE  pt.StatusID = 1;

El principal problema aquí es que mientras que podemos hacer una TOP (1)en una UPDATEoperación, no hay manera de hacer una ORDER BY. Pero, podemos envolverlo en un CTE para combinar esos dos conceptos:

;WITH cte AS
(
   SELECT TOP 1 pt.RecordID
   FROM   ProcessTable pt (READPAST, ROWLOCK, UPDLOCK)
   WHERE  pt.StatusID = 1
   ORDER BY pt.StatusModifiedDate ASC;
)
UPDATE cte
SET    cte.StatusID = 2,
       cte.StatusModifiedDate = GETDATE() -- or GETUTCDATE()
OUTPUT INSERTED.RecordID;

La pregunta obvia es si dos procesos que realizan SELECT al mismo tiempo pueden obtener el mismo registro. Estoy bastante seguro de que la cláusula ACTUALIZAR con SALIDA, especialmente combinada con las sugerencias READPAST y UPDLOCK (ver más abajo para más detalles), estará bien. Sin embargo, no he probado este escenario exacto. Si por alguna razón la consulta anterior no se ocupa de la condición de la carrera, entonces se agregará lo siguiente: bloqueos de la aplicación.

La consulta CTE anterior se puede envolver en sp_getapplock y sp_releaseapplock para crear un "guardián de puerta" para el proceso. Al hacerlo, solo un proceso a la vez podrá ingresar para ejecutar la consulta anterior. Los otros procesos se bloquearán hasta que el proceso con el bloqueo de aplicaciones lo libere. Y dado que este paso del proceso general es solo para tomar el RecordID, es bastante rápido y no bloqueará los otros procesos por mucho tiempo. Y, al igual que con la consulta de CTE, estamos no bloqueando toda la tabla, permitiendo de esta manera otros cambios a otras filas (para ajustar su estatus a cualquiera "Completo" o "Error"). Esencialmente:

BEGIN TRANSACTION;
EXEC sp_getapplock @Resource = 'GetNextRecordToProcess', @LockMode = 'Exclusive';

   {CTE UPDATE query shown above}

EXEC sp_releaseapplock @Resource = 'GetNextRecordToProcess';
COMMIT TRANSACTION;

Los bloqueos de aplicación son muy agradables, pero deben usarse con moderación.

Por último, solo necesita un procedimiento almacenado para manejar la configuración del estado como "Completado" o "Error". Y eso puede ser simple:

CREATE PROCEDURE ProcessTable_SetProcessStatusID
(
   @RecordID INT,
   @ProcessStatusID TINYINT
)
AS
SET NOCOUNT ON;

UPDATE pt
SET    pt.ProcessStatusID = @ProcessStatusID,
       pt.StatusModifiedDate = GETDATE() -- or GETUTCDATE()
FROM   ProcessTable pt
WHERE  pt.RecordID = @RecordID;

Sugerencias de tabla (que se encuentran en Sugerencias (Transact-SQL) - Tabla ):

  • READPAST (parece ajustarse a este escenario exacto)

    Especifica que el Motor de base de datos no lee las filas que están bloqueadas por otras transacciones. Cuando se especifica READPAST, se omiten los bloqueos de nivel de fila. Es decir, el Motor de base de datos omite las filas en lugar de bloquear la transacción actual hasta que se liberan los bloqueos ... READPAST se utiliza principalmente para reducir la contención de bloqueo al implementar una cola de trabajo que utiliza una tabla de SQL Server. Un lector de cola que usa READPAST omite las entradas de cola pasadas bloqueadas por otras transacciones a la siguiente entrada de cola disponible, sin tener que esperar hasta que las otras transacciones liberen sus bloqueos.

  • ROWLOCK (solo para estar seguro)

    Especifica que los bloqueos de fila se toman cuando los bloqueos de página o tabla se toman normalmente.

  • UPDLOCK

    Especifica que los bloqueos de actualización se deben tomar y retener hasta que se complete la transacción. UPDLOCK toma bloqueos de actualización para operaciones de lectura solo a nivel de fila o de página.

Solomon Rutzky
fuente
1

Hizo algo similar (sin aplicaciones, puramente dentro de DB) usando colas de Service Broker. Ligero, totalmente compatible con ACID, se puede escalar casi infinitamente. El bloqueo de fila transparente (o "ocultación", más bien) está incorporado. Disponible a partir de la versión 2005 y posteriores.

En su caso, la arquitectura general podría ser así: algunos procesos envían mensajes a los cuadros de diálogo de Service Broker, de acuerdo con sus programaciones, y los oyentes los recogen de la cola en el lado de destino. Además de crear tipos de mensajes separados, puede incluir casi cualquier cosa en el cuerpo del mensaje: tiempo de espera, por ejemplo, y cualquier parámetro que pueda tener la tarea.

No es lo más fácil de entender, eso es seguro, pero una vez que lo obtenga, sus ventajas serán evidentes.

Roger Wolf
fuente