Bảng xếp hàng FIFO cho nhiều công nhân trong SQL Server


15

Tôi đã cố gắng trả lời câu hỏi stackoverflow sau đây:

Sau khi đăng một câu trả lời hơi ngây thơ, tôi nghĩ rằng tôi đã đặt tiền của mình vào miệng và thực sự kiểm tra kịch bản mà tôi đang đề xuất, để chắc chắn rằng tôi đã không gửi OP ra khỏi một cuộc rượt đuổi ngông cuồng. Chà, hóa ra nó khó hơn nhiều so với tôi nghĩ (không có gì ngạc nhiên với bất cứ ai, tôi chắc chắn).

Đây là những gì tôi đã thử và nghĩ về:

  • Đầu tiên tôi đã thử CẬP NHẬT TOP 1 với ĐẶT HÀNG BỞI trong bảng dẫn xuất, sử dụng ROWLOCK, READPAST. Điều này mang lại bế tắc và cũng xử lý các mặt hàng không theo thứ tự. Nó phải càng gần với FIFO càng tốt, loại bỏ các lỗi yêu cầu cố gắng xử lý cùng một hàng nhiều lần.

  • Sau đó tôi đã cố gắng chọn mong muốn QueueID tiếp theo vào một biến, sử dụng kết hợp khác nhau của READPAST, UPDLOCK, HOLDLOCK, và ROWLOCKđể độc quyền duy trì hàng để cập nhật bởi phiên đó. Tất cả các biến thể tôi đã thử chịu các vấn đề tương tự như trước đây cũng như đối với các kết hợp nhất định với READPAST, phàn nàn:

    Bạn chỉ có thể chỉ định khóa READPAST ở các mức cách ly READ READ CAMEDED hoặc REPEATABLE READ.

    Điều này đã gây nhầm lẫn bởi vì nó đã được đọc . Tôi đã gặp phải điều này trước đây và nó đang bực bội.

  • Kể từ khi tôi bắt đầu viết câu hỏi này, Remus Rusani đã đăng một câu trả lời mới cho câu hỏi. Tôi đã đọc bài viết được liên kết của anh ấy và thấy rằng anh ấy đang sử dụng các bài đọc phá hoại, vì anh ấy đã nói trong câu trả lời của mình rằng "thực tế không thể giữ khóa trong suốt thời gian của các cuộc gọi web." Sau khi đọc những gì bài báo của anh ấy nói về các điểm nóng và các trang yêu cầu khóa để thực hiện bất kỳ cập nhật hoặc xóa nào, tôi sợ rằng ngay cả khi tôi có thể tìm ra các khóa chính xác để làm những gì tôi đang tìm kiếm, nó sẽ không thể mở rộng và có thể mở rộng không xử lý đồng thời lớn.

Ngay bây giờ tôi không chắc chắn nơi để đi. Có đúng là việc duy trì các khóa trong khi hàng được xử lý không thể đạt được (ngay cả khi nó không hỗ trợ tps cao hoặc đồng thời lớn)? Tôi đang thiếu gì?

Với hy vọng rằng những người thông minh hơn tôi và những người có kinh nghiệm hơn tôi có thể giúp đỡ, dưới đây là kịch bản thử nghiệm mà tôi đang sử dụng. Nó được chuyển trở lại phương thức CẬP NHẬT HÀNG ĐẦU 1 nhưng tôi đã để lại phương thức khác, nhận xét, trong trường hợp bạn cũng muốn khám phá điều đó.

Dán từng thứ này vào một phiên riêng biệt, chạy phiên 1, sau đó nhanh chóng tất cả các phiên khác. Trong khoảng 50 giây, bài kiểm tra sẽ kết thúc. Nhìn vào Tin nhắn từ mỗi phiên để xem nó hoạt động như thế nào (hoặc nó thất bại như thế nào). Phiên đầu tiên sẽ hiển thị một hàng với một ảnh chụp nhanh được thực hiện một lần thứ hai chi tiết các khóa hiện tại và các mục hàng đợi đang được xử lý. Nó hoạt động đôi khi, và những lần khác không hoạt động.

Phiê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;
*/

Đợt 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

Buổi 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

Phiên 4 trở lên - bao nhiêu tùy thích

/* 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
Các hàng đợi như được mô tả trong bài viết được liên kết có thể mở rộng đến hàng trăm hoặc thấp hơn hàng nghìn thao tác mỗi giây. Các vấn đề tranh chấp điểm nóng chỉ có liên quan ở quy mô cao hơn. Có những chiến lược giảm thiểu được biết đến có thể đạt được thông lượng cao hơn trên hệ thống cao cấp, đi vào hàng chục nghìn mỗi giây, nhưng những giảm thiểu đó cần được đánh giá cẩn thận và được triển khai dưới sự giám sát của SQLCAT .
Remus Rusanu

Một nếp nhăn thú vị là với READPAST, UPDLOCK, ROWLOCKkịch bản của tôi để thu thập dữ liệu vào bảng QueueHistory thì không làm gì cả. Tôi tự hỏi nếu đó là vì StatusID không được cam kết? Về WITH (NOLOCK)mặt lý thuyết, nó nên hoạt động ... và nó đã hoạt động trước đây! Tôi không chắc tại sao bây giờ nó không hoạt động, nhưng có lẽ đó là một kinh nghiệm học tập khác.
ErikE

Bạn có thể giảm mã của mình xuống mẫu nhỏ nhất thể hiện sự bế tắc và các vấn đề khác mà bạn đang cố gắng giải quyết không?
Nick Chammas

@Nick Tôi sẽ cố gắng giảm mã. Về các bình luận khác của bạn, có một cột danh tính là một phần của chỉ mục được nhóm và được sắp xếp sau ngày. Tôi khá sẵn lòng để giải trí một "đọc phá hủy" (XÓA với OUTPUT) nhưng một trong những yêu cầu được yêu cầu là, trong trường hợp ứng dụng bị lỗi, hàng sẽ tự động quay lại xử lý. Vì vậy, câu hỏi của tôi ở đây là liệu điều đó là có thể.
ErikE

Hãy thử cách tiếp cận đọc phá hủy và đặt các vật phẩm bị khử trong một bảng riêng biệt từ đó chúng có thể được đặt lại nếu cần thiết. Nếu điều đó khắc phục nó, thì bạn có thể đầu tư để làm cho quá trình tái ngộ này hoạt động trơn tru.
Nick Chammas

Câu trả lời:


10

Bạn cần chính xác 3 gợi ý khóa

  • SYN SÀNG
  • UPDLOCK
  • ROWLOCK

Tôi đã trả lời điều này trước đây trên SO: /programming/939831/sql-server- Process-queue-race-condition/940001#940001

Như Remus nói, sử dụng dịch vụ môi giới tốt hơn nhưng những gợi ý này có tác dụng

Lỗi của bạn về mức độ cô lập thường có nghĩa là sao chép hoặc NOLOCK có liên quan.


Sử dụng những gợi ý trên kịch bản của tôi như đã nêu ở trên sẽ mang lại những bế tắc và các quy trình không theo thứ tự. ( UPDATE SET ... FROM (SELECT TOP 1 ... FROM ... ORDER BY ...)) Điều này có nghĩa là mẫu CẬP NHẬT của tôi với việc giữ khóa không thể hoạt động? Ngoài ra, thời điểm bạn kết hợp READPASTvới HOLDLOCKbạn nhận được lỗi. Không có bản sao trên máy chủ này và mức cô lập được ĐỌC.
ErikE

2
@ErikE - Cũng quan trọng như cách bạn truy vấn bảng là cách cấu trúc bảng. Bảng bạn đang sử dụng như một hàng đợi phải được nhóm lại theo thứ tự dequeue sao cho mục tiếp theo được xử lý là không rõ ràng . Điều này là rất quan trọng. Đọc lướt mã của bạn ở trên, tôi không thấy bất kỳ chỉ mục cụm nào được xác định.
Nick Chammas

@Nick điều đó có ý nghĩa hoàn toàn nổi bật và tôi không biết tại sao tôi không nghĩ về nó. Tôi đã thêm các ràng buộc PK thích hợp (và cập nhật tập lệnh của tôi ở trên) và vẫn gặp bế tắc. Tuy nhiên, các mục hiện đã được xử lý theo đúng thứ tự, cấm xử lý lặp lại cho các mục bị khóa.
ErikE

@ErikE - 1. Hàng đợi của bạn chỉ nên chứa các mục được xếp hàng. Dequeuing và mục có nghĩa là xóa nó khỏi bảng xếp hàng. Tôi thấy rằng bạn đang cập nhật để thay thế StatusIDmột mục. Đúng không? 2. Thứ tự dequeue của bạn phải rõ ràng. Nếu bạn đang xếp hàng các mặt hàng theo GETDATE(), thì với khối lượng lớn, rất có khả năng nhiều mặt hàng sẽ đủ điều kiện như nhau để giải quyết cùng một lúc. Điều này sẽ dẫn đến bế tắc. Tôi đề nghị thêm một IDENTITYvào chỉ mục được nhóm để đảm bảo một thứ tự dequeue rõ ràng.
Nick Chammas

1

Máy chủ SQL hoạt động tuyệt vời để lưu trữ dữ liệu quan hệ. Đối với một hàng đợi công việc, nó không quá tuyệt vời. Xem bài viết này được viết cho MySQL nhưng nó cũng có thể áp dụng ở đây. https://blog.engineyard.com/2011/5-subussy-ways-youre-USE-mysql-as-a-queue-and-why-itll-bite-you


Cảm ơn, Eric. Trong câu trả lời ban đầu của tôi cho câu hỏi, tôi đã đề nghị sử dụng Nhà môi giới dịch vụ SQL Server vì tôi biết rằng thực tế là phương thức xếp hàng theo bảng không thực sự là cơ sở dữ liệu được tạo ra để làm gì. Nhưng tôi nghĩ đó không phải là một đề xuất tốt nữa vì SB thực sự chỉ dành cho tin nhắn. Các thuộc tính ACID của dữ liệu được đặt trong cơ sở dữ liệu làm cho nó trở thành một thùng chứa rất hấp dẫn để thử (ab) sử dụng. Bạn có thể đề xuất một sản phẩm thay thế, chi phí thấp sẽ hoạt động tốt như một hàng đợi chung không? Và có thể được sao lưu, vv vv?
ErikE

8
Bài viết có tội về một sai lầm đã biết trong xử lý hàng đợi: kết hợp trạng thái và sự kiện vào một bảng duy nhất (thực sự nếu bạn nhìn vào các bình luận bài viết bạn sẽ thấy tôi đã phản đối điều này một thời gian trước đây). Triệu chứng điển hình của vấn đề này là trường 'được xử lý / xử lý'. Kết hợp trạng thái với các sự kiện (nghĩa là biến bảng trạng thái thành 'hàng đợi') dẫn đến việc tăng 'hàng đợi' lên kích thước lớn (vì bảng trạng thái hàng đợi). Việc tách các sự kiện thành một hàng đợi thực sự dẫn đến một hàng đợi 'thoát' (trống rỗng) và điều này hoạt động tốt hơn nhiều .
Remus Rusanu

Bài viết không đề xuất chính xác rằng: bảng xếp hàng chỉ có các mục sẵn sàng cho công việc.?
ErikE

2
@ErikE: bạn đang đề cập đến đoạn này, phải không? Thật dễ dàng để tránh hội chứng một bàn lớn. Chỉ cần tạo một bảng riêng cho các email mới và khi bạn xử lý xong chúng, XÁC NHẬN chúng vào bộ lưu trữ dài hạn và sau đó XÓA chúng khỏi bảng xếp hàng. Bảng các email mới thường sẽ rất nhỏ và các thao tác trên đó sẽ nhanh chóng . Cuộc tranh cãi của tôi với điều này được đưa ra như một cách giải quyết cho vấn đề 'hàng đợi lớn'. Khuyến nghị này nên có trong phần mở đầu của bài báo, là một vấn đề cơ bản .
Remus Rusanu

Nếu bạn bắt đầu suy nghĩ trong một sự tách biệt rõ ràng giữa trạng thái và sự kiện thì bạn bắt đầu vdown một con đường dễ dàng hơn nhiều. Ngay cả phần giới thiệu ở trên cũng sẽ thay đổi thành chèn email mới vào emailsbảng vào new_emailshàng đợi. Xử lý các cuộc thăm dò new_emailshàng đợi và cập nhật trạng thái trong emailsbảng . Điều này cũng tránh được vấn đề về trạng thái 'béo' khi đi trong hàng đợi. Nếu chúng ta sẽ nói về xử lý phân tán và hàng đợi thực sự , với giao tiếp, (ví dụ SSB) thì mọi thứ sẽ trở nên phức tạp hơn vì trạng thái chia sẻ là vấn đề trong các hệ thống bị phân tán.
Remus Rusanu
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.