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:
- Completado (o incluso "Pendiente" en este caso, ya que ambos significan "listo para ser procesado")
- En proceso (o "Procesamiento")
- 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 OUTPUT
clá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 UPDATE
operació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 ):