it-swarm-eu.dev

FIFO tabulka front pro více pracovníků na serveru SQL)

Pokoušel jsem se odpovědět na následující otázku stackoverflow:

Po zveřejnění poněkud naivní odpovědi jsem usoudil, že peníze vložím tam, kde jsou moje ústa, a ve skutečnosti test scénář, který navrhuji, abych se ujistil, že neposílám OP na divoké husí honičce. No, ukázalo se, že je mnohem těžší, než jsem si myslel (jistě není nikoho překvapení).

Zde je to, o co jsem se snažil a přemýšlel:

  • Nejprve jsem zkusil TOP 1 UPDATE s OBJEDNÁVKEM uvnitř odvozené tabulky, pomocí ROWLOCK, READPAST. To vedlo k zablokování a také ke zpracování položek mimo provoz. Musí být co nejblíže k FIFO, jak je to možné), s výjimkou chyb, které vyžadují pokus zpracovat stejný řádek více než jednou.

  • Poté jsem se pokusil vybrat požadovaný další QueueID do proměnné, pomocí různých kombinací READPAST, UPDLOCK, HOLDLOCK a ROWLOCK, aby se výhradně zachoval řádek pro aktualizovat o tuto relaci. Všechny varianty, které jsem zkusil, trpěly stejnými problémy jako dříve a také, pro určité kombinace s READPAST, si stěžovaly:

    Zámek READPAST můžete zadat pouze v izolačních úrovních READ COMMITTED nebo REPEATABLE READ.

    To bylo matoucí, protože to bylo PŘEČTĚNO POTVRZENO. Už jsem se s tím setkal dříve a je to frustrující.

  • Od té doby, co jsem začal psát tuto otázku, Remus Rusani poslal novou odpověď na tuto otázku. Přečetl jsem jeho propojený článek a viděl jsem, že používá destruktivní čtení, protože ve své odpovědi řekl, že „není realisticky možné držet zámky po dobu webových volání“. Po přečtení toho, co jeho článek říká o horkých místech a stránkách vyžadujících uzamčení, aby bylo možné provést jakoukoli aktualizaci nebo smazání, se obávám, že i kdybych byl schopen vypracovat správné zámky pro to, co hledám, nebylo by to škálovatelné a mohlo by neřeší masivní souběžnost.

Teď si nejsem jistý, kam jít. Je pravda, že udržování zámků během zpracování řádku nelze dosáhnout (i když to nepodporovalo vysoké tps nebo masivní souběžnost)? Co mi chybí?

V naději, že lidé chytřejší než já a lidé zkušenější než já můžu pomoci, níže je testovací skript, který jsem používal. Je přepnuta zpět na metodu TOP 1 UPDATE, ale druhou metodu jsem nechal v, komentovanou, pro případ, že byste to také chtěli prozkoumat.

Vložte každou z nich do samostatné relace, spusťte relaci 1 a poté rychle všechny ostatní. Asi za 50 sekund bude test u konce. Podívejte se na Zprávy z každé relace a podívejte se, jak to fungovalo (nebo jak to selhalo). První relace zobrazí sadu řádků se snímkem pořízeným jednou za sekundu s podrobnostmi o přítomných zámcích a zpracovávaných položkách fronty. Funguje to někdy a jindy vůbec nefunguje.

Session 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;
*/

relace 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

relace 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

Sekce 4 a vyšší - tolik, kolik chcete

/* 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
15
ErikE

Potřebujete přesně 3 rady pro zamykání

  • PŘEČTĚTE SI
  • UPDLOCK
  • VIDLICE NA VESLO

Odpověděl jsem to dříve na SO: https://stackoverflow.com/questions/939831/sql-server-process-queue-race-condition/940001#940001

Jak říká Remus, použití servisního makléře je hezčí , ale tyto rady fungují

Vaše chyba týkající se úrovně izolace obvykle znamená replikaci nebo se jedná o NOLOCK.

10
gbn

SQL server funguje skvěle pro ukládání relačních dat. Pokud jde o pracovní frontu, není to tak skvělé. Viz tento článek, který je napsán pro MySQL, ale může se také použít zde. https://blog.engineyard.com/2011/5-subtle-ways-youre-using-mysql-as-a-queue-and-why-itll-bite-yo