Intentaba responder la siguiente pregunta de stackoverflow:
Después de publicar una respuesta un tanto ingenua, pensé que pondría mi dinero donde estaba mi boca y realmente probaría el escenario que estaba sugiriendo, para asegurarme de que no estaba enviando el OP en una loca búsqueda. Bueno, resultó ser mucho más difícil de lo que pensaba (no hay sorpresa para nadie, estoy seguro).
Esto es lo que he probado y pensado:
Primero probé una ACTUALIZACIÓN TOP 1 con un ORDER BY dentro de una tabla derivada, usando
ROWLOCK, READPAST
. Esto produjo puntos muertos y también procesó artículos fuera de servicio. Debe estar lo más cerca posible de FIFO, salvo errores que requieran intentar procesar la misma fila más de una vez.Luego trató de selección del próximo QueueID deseada en una variable, usando varias combinaciones de
READPAST
,UPDLOCK
,HOLDLOCK
, yROWLOCK
para preservar la fila exclusivamente para la actualización de esa sesión. Todas las variaciones que probé sufrieron los mismos problemas que antes, así como, para ciertas combinaciones conREADPAST
, quejarse:Solo puede especificar el bloqueo READPAST en los niveles de aislamiento READ COMMITTED o REPEATABLE READ.
Esto fue confuso porque fue LEÍDO COMPROMETIDO. Me he encontrado con esto antes y es frustrante.
Desde que comencé a escribir esta pregunta, Remus Rusani publicó una nueva respuesta a la pregunta. Leí su artículo vinculado y veo que está usando lecturas destructivas, ya que dijo en su respuesta que "no es realmente posible mantener las cerraduras durante la duración de las llamadas web". Después de leer lo que dice su artículo sobre los puntos críticos y las páginas que requieren bloqueo para actualizar o eliminar, me temo que incluso si pudiera encontrar los bloqueos correctos para hacer lo que estoy buscando, no sería escalable y podría No manejar la concurrencia masiva.
En este momento no estoy seguro de dónde ir. ¿Es cierto que no se puede mantener los bloqueos mientras se procesa la fila (incluso si no admitía tps altos o concurrencia masiva)? ¿Qué me estoy perdiendo?
Con la esperanza de que las personas más inteligentes que yo y las personas con más experiencia que yo puedan ayudar, a continuación se muestra el script de prueba que estaba usando. Volvió al método de ACTUALIZACIÓN TOP 1, pero dejé el otro método en, comentado, en caso de que quiera explorar eso también.
Pegue cada uno de estos en una sesión separada, ejecute la sesión 1, luego rápidamente todos los demás. En unos 50 segundos la prueba habrá terminado. Mire los Mensajes de cada sesión para ver qué trabajo hizo (o cómo falló). La primera sesión mostrará un conjunto de filas con una instantánea tomada una vez por segundo que detalla los bloqueos presentes y los elementos de cola procesados. Funciona a veces, y otras veces no funciona en absoluto.
Sesión 1
/* Session 1: Setup and control - Run this session first, then immediately run all other sessions */
IF Object_ID('dbo.Queue', 'U') IS NULL
CREATE TABLE dbo.Queue (
QueueID int identity(1,1) NOT NULL,
StatusID int NOT NULL,
QueuedDate datetime CONSTRAINT DF_Queue_QueuedDate DEFAULT (GetDate()),
CONSTRAINT PK_Queue PRIMARY KEY CLUSTERED (QueuedDate, QueueID)
);
IF Object_ID('dbo.QueueHistory', 'U') IS NULL
CREATE TABLE dbo.QueueHistory (
HistoryDate datetime NOT NULL,
QueueID int NOT NULL
);
IF Object_ID('dbo.LockHistory', 'U') IS NULL
CREATE TABLE dbo.LockHistory (
HistoryDate datetime NOT NULL,
ResourceType varchar(100),
RequestMode varchar(100),
RequestStatus varchar(100),
ResourceDescription varchar(200),
ResourceAssociatedEntityID varchar(200)
);
IF Object_ID('dbo.StartTime', 'U') IS NULL
CREATE TABLE dbo.StartTime (
StartTime datetime NOT NULL
);
SET NOCOUNT ON;
IF (SELECT Count(*) FROM dbo.Queue) < 10000 BEGIN
TRUNCATE TABLE dbo.Queue;
WITH A (N) AS (SELECT 1 UNION ALL SELECT 1 UNION ALL SELECT 1 UNION ALL SELECT 1),
B (N) AS (SELECT 1 FROM A Z, A I, A P),
C (N) AS (SELECT Row_Number() OVER (ORDER BY (SELECT 1)) FROM B O, B W)
INSERT dbo.Queue (StatusID, QueuedDate)
SELECT 1, DateAdd(millisecond, C.N * 3, GetDate() - '00:05:00')
FROM C
WHERE C.N <= 10000;
END;
TRUNCATE TABLE dbo.StartTime;
INSERT dbo.StartTime SELECT GetDate() + '00:00:15'; -- or however long it takes you to go run the other sessions
GO
TRUNCATE TABLE dbo.QueueHistory;
SET NOCOUNT ON;
DECLARE
@Time varchar(8),
@Now datetime;
SELECT @Time = Convert(varchar(8), StartTime, 114)
FROM dbo.StartTime;
WAITFOR TIME @Time;
DECLARE @i int,
@QueueID int;
SET @i = 1;
WHILE @i <= 33 BEGIN
SET @Now = GetDate();
INSERT dbo.QueueHistory
SELECT
@Now,
QueueID
FROM
dbo.Queue Q WITH (NOLOCK)
WHERE
Q.StatusID <> 1;
INSERT dbo.LockHistory
SELECT
@Now,
L.resource_type,
L.request_mode,
L.request_status,
L.resource_description,
L.resource_associated_entity_id
FROM
sys.dm_tran_current_transaction T
INNER JOIN sys.dm_tran_locks L
ON L.request_owner_id = T.transaction_id;
WAITFOR DELAY '00:00:01';
SET @i = @i + 1;
END;
WITH Cols AS (
SELECT *, Row_Number() OVER (PARTITION BY HistoryDate ORDER BY QueueID) Col
FROM dbo.QueueHistory
), P AS (
SELECT *
FROM
Cols
PIVOT (Max(QueueID) FOR Col IN ([1], [2], [3], [4], [5], [6], [7], [8])) P
)
SELECT L.*, P.[1], P.[2], P.[3], P.[4], P.[5], P.[6], P.[7], P.[8]
FROM
dbo.LockHistory L
FULL JOIN P
ON L.HistoryDate = P.HistoryDate
/* Clean up afterward
DROP TABLE dbo.StartTime;
DROP TABLE dbo.LockHistory;
DROP TABLE dbo.QueueHistory;
DROP TABLE dbo.Queue;
*/
Sesión 2
/* Session 2: Simulate an application instance holding a row locked for a long period, and eventually abandoning it. */
SET TRANSACTION ISOLATION LEVEL READ COMMITTED;
SET NOCOUNT ON;
SET XACT_ABORT ON;
DECLARE
@QueueID int,
@Time varchar(8);
SELECT @Time = Convert(varchar(8), StartTime + '0:00:01', 114)
FROM dbo.StartTime;
WAITFOR TIME @Time;
BEGIN TRAN;
--SET @QueueID = (
-- SELECT TOP 1 QueueID
-- FROM dbo.Queue WITH (READPAST, UPDLOCK)
-- WHERE StatusID = 1 -- ready
-- ORDER BY QueuedDate, QueueID
--);
--UPDATE dbo.Queue
--SET StatusID = 2 -- in process
----OUTPUT Inserted.*
--WHERE QueueID = @QueueID;
SET @QueueID = NULL;
UPDATE Q
SET Q.StatusID = 1, @QueueID = Q.QueueID
FROM (
SELECT TOP 1 *
FROM dbo.Queue WITH (ROWLOCK, READPAST)
WHERE StatusID = 1
ORDER BY QueuedDate, QueueID
) Q
PRINT @QueueID;
WAITFOR DELAY '00:00:20'; -- Release it partway through the test
ROLLBACK TRAN; -- Simulate client disconnecting
Sesión 3
/* Session 3: Run a near-continuous series of "failed" queue processing. */
SET TRANSACTION ISOLATION LEVEL READ COMMITTED;
SET XACT_ABORT ON;
SET NOCOUNT ON;
DECLARE
@QueueID int,
@EndDate datetime,
@NextDate datetime,
@Time varchar(8);
SELECT
@EndDate = StartTime + '0:00:33',
@Time = Convert(varchar(8), StartTime, 114)
FROM dbo.StartTime;
WAITFOR TIME @Time;
WHILE GetDate() < @EndDate BEGIN
BEGIN TRAN;
--SET @QueueID = (
-- SELECT TOP 1 QueueID
-- FROM dbo.Queue WITH (READPAST, UPDLOCK)
-- WHERE StatusID = 1 -- ready
-- ORDER BY QueuedDate, QueueID
--);
--UPDATE dbo.Queue
--SET StatusID = 2 -- in process
----OUTPUT Inserted.*
--WHERE QueueID = @QueueID;
SET @QueueID = NULL;
UPDATE Q
SET Q.StatusID = 1, @QueueID = Q.QueueID
FROM (
SELECT TOP 1 *
FROM dbo.Queue WITH (ROWLOCK, READPAST)
WHERE StatusID = 1
ORDER BY QueuedDate, QueueID
) Q
PRINT @QueueID;
SET @NextDate = GetDate() + '00:00:00.015';
WHILE GetDate() < @NextDate SET NOCOUNT ON;
ROLLBACK TRAN;
END
Sesión 4 y superior: tantas como quieras
/* Session 4: "Process" the queue normally, one every second for 30 seconds. */
SET TRANSACTION ISOLATION LEVEL READ COMMITTED;
SET XACT_ABORT ON;
SET NOCOUNT ON;
DECLARE @Time varchar(8);
SELECT @Time = Convert(varchar(8), StartTime, 114)
FROM dbo.StartTime;
WAITFOR TIME @Time;
DECLARE @i int,
@QueueID int;
SET @i = 1;
WHILE @i <= 30 BEGIN
BEGIN TRAN;
--SET @QueueID = (
-- SELECT TOP 1 QueueID
-- FROM dbo.Queue WITH (READPAST, UPDLOCK)
-- WHERE StatusID = 1 -- ready
-- ORDER BY QueuedDate, QueueID
--);
--UPDATE dbo.Queue
--SET StatusID = 2 -- in process
--WHERE QueueID = @QueueID;
SET @QueueID = NULL;
UPDATE Q
SET Q.StatusID = 1, @QueueID = Q.QueueID
FROM (
SELECT TOP 1 *
FROM dbo.Queue WITH (ROWLOCK, READPAST)
WHERE StatusID = 1
ORDER BY QueuedDate, QueueID
) Q
PRINT @QueueID;
WAITFOR DELAY '00:00:01'
SET @i = @i + 1;
DELETE dbo.Queue
WHERE QueueID = @QueueID;
COMMIT TRAN;
END
fuente
READPAST, UPDLOCK, ROWLOCK
mi script para capturar datos en la tabla QueueHistory no está haciendo nada. Me pregunto si eso se debe a que el StatusID no está comprometido. Está usandoWITH (NOLOCK)
tan teóricamente debería funcionar ... ¡y funcionó antes! No estoy seguro de por qué no funciona ahora, pero probablemente sea otra experiencia de aprendizaje.Respuestas:
Necesitas exactamente 3 pistas de bloqueo
Respondí esto anteriormente en SO: /programming/939831/sql-server-process-queue-race-condition/940001#940001
Como dice Remus, usar Service Broker es mejor, pero estas sugerencias funcionan
Su error sobre el nivel de aislamiento generalmente significa replicación o NOLOCK está involucrado.
fuente
UPDATE SET ... FROM (SELECT TOP 1 ... FROM ... ORDER BY ...)
) ¿Significa esto que mi patrón ACTUALIZAR con mantener un bloqueo no puede funcionar? Además, el momento en que se combinaREADPAST
conHOLDLOCK
usted obtiene el error. No hay replicación en este servidor y el nivel de aislamiento es LEÍDO COMPROMETIDO.StatusID
para eliminar un elemento. ¿Es eso correcto? 2. Su orden de retiro debe ser inequívoca. Si está haciendo cola en los artículosGETDATE()
, entonces, a grandes volúmenes, es muy probable que varios artículos sean igualmente elegibles para retirarse al mismo tiempo. Esto conducirá a puntos muertos. Sugiero agregar unIDENTITY
al índice agrupado para garantizar un orden de eliminación inequívoco.El servidor SQL funciona muy bien para almacenar datos relacionales. En cuanto a una cola de trabajo, no es tan genial. Vea este artículo que está escrito para MySQL pero también puede aplicarse aquí. https://blog.engineyard.com/2011/5-subtle-ways-youre-using-mysql-as-a-queue-and-why-itll-bite-you
fuente
emails
tabla y en lanew_emails
cola. El procesamiento sondea lanew_emails
cola y actualiza el estado en laemails
tabla . Esto también evita el problema del estado 'gordo' viajando en colas. Si hablamos sobre el procesamiento distribuido y las colas verdaderas , con la comunicación (por ejemplo, SSB), entonces las cosas se vuelven más complicadas ya que el estado compartido es problemático en los sistemas con problemas.