ตารางคิว FIFO สำหรับพนักงานหลายคนใน SQL Server


15

ฉันพยายามตอบคำถาม stackoverflow ต่อไปนี้:

หลังจากโพสต์คำตอบที่ไร้เดียงสาฉันคิดว่าฉันจะเอาเงินของฉันไปที่ปากของฉันและทดสอบสถานการณ์จริง ๆที่ฉันแนะนำเพื่อให้แน่ใจว่าฉันไม่ได้ส่ง OP ออกไปในการไล่ล่าห่านป่า มันกลายเป็นว่าหนักกว่าที่ฉันคิดไว้มาก (ไม่แปลกใจสำหรับทุกคนฉันแน่ใจ)

นี่คือสิ่งที่ฉันได้ลองและคิดเกี่ยวกับ:

  • ครั้งแรกที่ฉันพยายาม TOP 1 UPDATE กับ ORDER BY ROWLOCK, READPASTภายในตารางมาใช้ สิ่งนี้ทำให้เกิดการหยุดชะงักและยังประมวลผลรายการที่ผิดปกติ จะต้องใกล้เคียงกับ FIFO เท่าที่จะทำได้ยกเว้นข้อผิดพลาดที่ต้องพยายามประมวลผลแถวเดียวกันมากกว่าหนึ่งครั้ง

  • ฉันก็พยายามเลือก QueueID ถัดไปต้องการลงในตัวแปรโดยใช้ชุดต่างๆของREADPAST, UPDLOCK, HOLDLOCKและROWLOCKเพื่อรักษาเฉพาะแถวสำหรับการปรับปรุงโดยเซสชั่นที่ รูปแบบทั้งหมดที่ฉันลองใช้นั้นประสบปัญหาเดียวกันกับที่เคยมีมาก่อนและสำหรับการรวมกันบางอย่างกับREADPAST:

    คุณสามารถระบุล็อค READPAST ในระดับการแยก READ COMMITTED หรือ REPEATABLE READ

    สิ่งนี้ทำให้เกิดความสับสนเพราะเป็นข้อผูกมัดที่ต้องอ่าน ฉันเคยพบเจอสิ่งนี้มาก่อนและมันน่าหงุดหงิด

  • ตั้งแต่ฉันเริ่มเขียนคำถามนี้ Remus Rusani โพสต์คำตอบใหม่สำหรับคำถาม ฉันอ่านบทความที่เชื่อมโยงของเขาและเห็นว่าเขากำลังใช้การอ่านที่ทำลายล้างเนื่องจากเขากล่าวในคำตอบของเขาว่า "เป็นไปไม่ได้ที่จะล็อกไว้ในระหว่างการโทรผ่านเว็บ" หลังจากอ่านสิ่งที่บทความของเขาพูดเกี่ยวกับฮอตสปอตและหน้าเว็บที่ต้องการการล็อกเพื่อทำการอัปเดตหรือลบฉันกลัวว่าแม้ว่าฉันจะสามารถใช้งานการล็อกที่ถูกต้องเพื่อทำสิ่งที่ฉันกำลังมองหามันจะไม่สามารถปรับขนาดได้ ไม่จัดการพร้อมกันมาก

ตอนนี้ฉันไม่แน่ใจว่าจะไปที่ไหน เป็นความจริงหรือไม่ว่าการดูแลรักษาล็อกในขณะที่แถวไม่สามารถดำเนินการได้ ฉันกำลังคิดถึงอะไร

ด้วยความหวังว่าผู้คนฉลาดกว่าฉันและผู้คนที่มีประสบการณ์มากกว่าที่ฉันสามารถช่วยได้ด้านล่างคือสคริปต์ทดสอบที่ฉันใช้ มันถูกเปลี่ยนกลับไปเป็นวิธีการอัปเดต TOP 1 แต่ฉันออกจากวิธีอื่นในความเห็นในกรณีที่คุณต้องการสำรวจด้วย

วางแต่ละรายการเหล่านี้ลงในเซสชันแยกต่างหากเรียกใช้เซสชัน 1 จากนั้นแยกรายการอื่น ๆ ทั้งหมดอย่างรวดเร็ว ในประมาณ 50 วินาทีการทดสอบจะสิ้นสุด ดูข้อความจากแต่ละเซสชันเพื่อดูว่ามันทำงานอย่างไร (หรือความล้มเหลว) เซสชั่นแรกจะแสดงชุดของ rowset ที่มีสแนปชอตหนึ่งครั้งที่มีรายละเอียดการล็อคที่มีอยู่และรายการคิวที่กำลังดำเนินการ มันใช้งานได้บางครั้งและเวลาอื่นไม่ทำงานเลย

เซสชั่น 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;
*/

เซสชั่น 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

เซสชั่น 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

เซสชันที่ 4 ขึ้นไป - มากเท่าที่คุณต้องการ

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

2
คิวตามที่อธิบายไว้ในบทความที่เชื่อมโยงสามารถขยายเป็นหลายร้อยหรือหลายพันการดำเนินการต่อวินาที ประเด็นการถกเถียงประเด็นฮอตสปอตมีความเกี่ยวข้องในระดับที่สูงขึ้นเท่านั้น มีกลยุทธ์การบรรเทาผลกระทบที่เป็นที่รู้จักซึ่งสามารถบรรลุปริมาณงานที่สูงขึ้นในระบบระดับสูงไปสู่หมื่นต่อวินาที แต่การบรรเทาผลกระทบเหล่านั้นจำเป็นต้องมีการประเมินอย่างรอบคอบและดำเนินการภายใต้การกำกับดูแลของSQLCAT
Remus Rusanu

รอยย่นที่น่าสนใจอย่างหนึ่งคือREADPAST, UPDLOCK, ROWLOCKสคริปต์ของฉันสำหรับเก็บข้อมูลลงในตาราง QueueHistory ไม่ได้ทำอะไรเลย ฉันสงสัยว่าเป็นเพราะ StatusID ไม่ได้มุ่งมั่นหรือไม่ มันใช้ในWITH (NOLOCK)ทางทฤษฎีควรทำงาน ... และมันทำงานได้ก่อน! ฉันไม่แน่ใจว่าทำไมมันไม่ทำงานในตอนนี้ แต่อาจเป็นประสบการณ์การเรียนรู้อื่น
ErikE

คุณสามารถลดรหัสของคุณเป็นตัวอย่างที่เล็กที่สุดที่แสดงถึงการหยุดชะงักและปัญหาอื่น ๆ ที่คุณพยายามแก้ไขได้หรือไม่?
Nick Chammas

@Nick ฉันจะพยายามลดรหัส เกี่ยวกับความคิดเห็นอื่น ๆ ของคุณมีคอลัมน์ข้อมูลประจำตัวที่เป็นส่วนหนึ่งของดัชนีคลัสเตอร์และเรียงลำดับตามวันที่ ฉันค่อนข้างเต็มใจที่จะสร้างความบันเทิงให้กับ "การอ่านแบบทำลายล้าง" (DELETE with OUTPUT) แต่หนึ่งในความต้องการที่ขอมานั้นก็คือในกรณีที่แอปพลิเคชันล้มเหลว ดังนั้นคำถามของฉันที่นี่คือว่าเป็นไปได้
ErikE

ลองใช้วิธีอ่านแบบทำลายและวางรายการที่ถูก dequeued ในตารางแยกจากที่พวกเขาอาจได้รับการจัดคิวอีกครั้งหากจำเป็น หากการแก้ไขนั้นคุณสามารถลงทุนในการทำให้กระบวนการจัดคิวงานใหม่นี้ทำงานได้อย่างราบรื่น
Nick Chammas

คำตอบ:


10

คุณต้องตรง 3 คำแนะนำล็อค

  • READPAST
  • UPDLOCK
  • ROWLOCK

ฉันตอบเรื่องนี้ก่อนหน้านี้ใน SO: /programming/939831/sql-server-process-queue-race-condition/940001#940001

รีมัสบอกว่าการใช้บริการนายหน้านั้นดีกว่าแต่คำแนะนำเหล่านี้ใช้ได้ผล

ข้อผิดพลาดของคุณเกี่ยวกับระดับการแยกมักหมายถึงการจำลองแบบหรือเกี่ยวข้องกับ NOLOCK


การใช้คำแนะนำเหล่านี้ในสคริปต์ของฉันตามที่ระบุไว้ข้างต้นทำให้เกิดการหยุดชะงักและกระบวนการไม่เป็นระเบียบ ( UPDATE SET ... FROM (SELECT TOP 1 ... FROM ... ORDER BY ...)) นี่หมายความว่ารูปแบบ UPDATE ของฉันที่มีการล็อคการล็อคไม่สามารถใช้งานได้หรือไม่ นอกจากนี้ช่วงเวลาที่คุณรวมREADPASTกับHOLDLOCKคุณได้รับข้อผิดพลาด ไม่มีการจำลองแบบบนเซิร์ฟเวอร์นี้และระดับการแยกถูกอ่านยอมรับแล้ว
ErikE

2
@ErikE - มีความสำคัญเทียบเท่ากับวิธีที่คุณสืบค้นตารางคือโครงสร้างของตาราง ตารางที่คุณใช้เป็นคิวจะต้องคลัสเตอร์เพื่อ dequeue ดังกล่าวว่ารายการต่อไปที่จะ dequeued เป็นที่ชัดเจน นี่เป็นสิ่งสำคัญ อ่านรหัสของคุณด้านบนฉันไม่เห็นดัชนีใด ๆ ที่กำหนดไว้ในคลัสเตอร์
Nick Chammas

@ นิกที่ทำให้รู้สึกเด่นอย่างสมบูรณ์แบบและฉันไม่รู้ว่าทำไมฉันไม่คิดอย่างนั้น ฉันเพิ่มข้อ จำกัด PK ที่เหมาะสม (และอัปเดตสคริปต์ของฉันด้านบน) และยังคงมีการหยุดชะงัก อย่างไรก็ตามรายการได้รับการประมวลผลในลำดับที่ถูกต้องยกเว้นการประมวลผลซ้ำสำหรับรายการที่หยุดชะงัก
ErikE

@ErikE - 1. คิวของคุณควรมีรายการที่อยู่ในคิวเท่านั้น การถอนและรายการควรหมายถึงการลบออกจากตารางคิว ฉันเห็นว่าคุณกำลังอัปเดตStatusIDเพื่อ dequeue รายการแทน ถูกต้องหรือไม่ 2. ใบสั่ง dequeue ของคุณต้องไม่คลุมเครือ หากคุณกำลังเข้าคิวรายการโดยGETDATE()ที่ปริมาณสูงเป็นไปได้มากว่าหลายรายการจะมีสิทธิ์เท่าเทียมกันสำหรับการถอนในเวลาเดียวกัน สิ่งนี้จะนำไปสู่การหยุดชะงัก ฉันขอแนะนำให้เพิ่มIDENTITYดัชนีในคลัสเตอร์เพื่อรับประกันคำสั่ง dequeue ที่ไม่คลุมเครือ
Nick Chammas

1

เซิร์ฟเวอร์ SQL ใช้งานได้ดีสำหรับการจัดเก็บข้อมูลเชิงสัมพันธ์ สำหรับคิวงานมันไม่ได้ยอดเยี่ยมนัก ดูบทความนี้เขียนขึ้นสำหรับ MySQL แต่ยังสามารถใช้ที่นี่ https://blog.engineyard.com/2011/5-subtle-ways-youre-using-mysql-as-a-queue-and-why-itll-bite-you


ขอบคุณเอริค ในการตอบคำถามเดิมของฉันฉันแนะนำให้ใช้ SQL Server Service Broker เพราะฉันรู้ว่าวิธี table-as-queue ไม่ใช่สิ่งที่ฐานข้อมูลสร้างขึ้นจริง ๆ แต่ฉันคิดว่านั่นไม่ใช่คำแนะนำที่ดีอีกต่อไปเพราะ SB เป็นเพียงข้อความจริงๆ คุณสมบัติ ACID ของข้อมูลที่ใส่ในฐานข้อมูลทำให้เป็นคอนเทนเนอร์ที่น่าดึงดูดมากสำหรับการใช้ (ab) คุณสามารถแนะนำผลิตภัณฑ์ทดแทนราคาต่ำที่จะทำงานได้ดีเหมือนกับคิวทั่วไปหรือไม่? และสามารถสำรอง ฯลฯ ฯลฯ ?
ErikE

8
บทความนี้มีความผิดเกี่ยวกับการเข้าใจผิดในการประมวลผลคิว: รวมสถานะและเหตุการณ์ไว้ในตารางเดียว (จริง ๆ แล้วถ้าคุณดูที่ความคิดเห็นของบทความคุณจะเห็นว่าฉันได้คัดค้านเรื่องนี้มาแล้ว) อาการทั่วไปของปัญหานี้คือช่อง 'ประมวลผล / ประมวลผล' การรวมสถานะเข้ากับเหตุการณ์ (เช่นการทำให้ตารางสถานะเป็น 'คิว') ส่งผลให้เกิดการขยาย 'คิว' เป็นขนาดใหญ่ (เนื่องจากตารางสถานะ เป็นคิว) การแยกกิจกรรมออกเป็นคิวจริงนำไปสู่คิวที่ 'ท่อระบายน้ำ' (ว่างเปล่า) และสิ่งนี้จะทำงานได้ดีกว่ามาก
Remus Rusanu

บทความไม่แนะนำอย่างชัดเจนว่า: ตารางคิวมีรายการพร้อมใช้งานเท่านั้น
ErikE

2
@ErikE: คุณอ้างถึงย่อหน้านี้ใช่มั้ย นอกจากนี้ยังง่ายมากที่จะหลีกเลี่ยงกลุ่มอาการดาวน์ซินโดรม เพียงสร้างตารางแยกต่างหากสำหรับอีเมลใหม่และเมื่อคุณประมวลผลเสร็จแล้วให้แทรกลงในที่เก็บข้อมูลระยะยาวแล้วลบออกจากตารางคิว ตารางของอีเมลใหม่โดยทั่วไปจะอยู่ที่ขนาดเล็กมากและดำเนินการเมื่อวันมันจะเป็นไปอย่างรวดเร็ว การทะเลาะกันของฉันกับเรื่องนี้คือการได้รับการแก้ไขปัญหาสำหรับ 'คิวใหญ่' ข้อเสนอแนะนี้ควรได้รับในการเปิดตัวของบทความเป็นปัญหาพื้นฐาน
Remus Rusanu

หากคุณเริ่มคิดในการแยกสถานะกับเหตุการณ์ชัดเจนแล้วคุณเริ่ม vdown เส้นทางที่ง่ายขึ้นมาก แม้แต่การแนะนำอีกครั้งข้างต้นก็จะเปลี่ยนเป็นการแทรกอีเมลใหม่ลงในemailsตารางและในnew_emailsคิว การประมวลผลการเลือกตั้งnew_emailsคิวและการปรับปรุงรัฐในemailsตาราง นอกจากนี้ยังช่วยหลีกเลี่ยงปัญหาของ 'ไขมัน' ที่เดินทางเป็นคิว หากเราจะพูดถึงการประมวลผลแบบกระจายและคิวจริงด้วยการสื่อสาร (เช่น SSB) ดังนั้นสิ่งต่าง ๆ จะมีความซับซ้อนมากขึ้นเนื่องจากสถานะที่ใช้ร่วมกันเป็นปัญหาในระบบที่รบกวน
Remus Rusanu
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.