SQL Server'da birden çok çalışan için FIFO kuyruk tablosu


15

Aşağıdaki stackoverflow sorusuna cevap vermeye çalışıyordum:

Biraz naif bir cevap gönderdikten sonra, paramı ağzımın olduğu yere koyacağımı ve aslında önerdiğim senaryoyu test edeceğimi düşündüm . Düşündüğümden çok daha zor olduğu ortaya çıktı (orada kimseye sürpriz yok, eminim).

İşte denedim ve düşündüm:

  • İlk kullanarak kullanarak türetilmiş bir tablo içinde ORDER BY ile TOP 1 GÜNCELLEME denedim ROWLOCK, READPAST. Bu, kilitlenmelere yol açtı ve ayrıca öğeleri işledi. Aynı satırı bir kereden fazla işlemeye teşebbüs gerektiren hataları engellemek için mümkün olduğunca FIFO'ya yakın olmalıdır.

  • Sonra çeşitli kombinasyonlarını kullanarak bir değişken içine istenen sonraki QueueID seçerek çalıştı READPAST, UPDLOCK, HOLDLOCK, ve ROWLOCKmünhasıran bu oturumda tarafından güncellenmesi satır korumak için. Denediğim tüm varyasyonlar, daha önce olduğu gibi aynı sorunlardan ve bazı kombinasyonlar için READPASTşikayetçi oldu:

    READPAST kilidini yalnızca READ COMMITTED veya REPEATABLE READ yalıtım seviyelerinde belirleyebilirsiniz.

    Çünkü bu kafa karıştırıcı oldu OKU İŞLENEN. Bunu daha önce de gördüm ve sinir bozucu.

  • Bu soruyu yazmaya başladığımdan beri, Remus Rusani soruya yeni bir cevap gönderdi. Bağlantılı makalesini okudum ve yıkıcı okumalar kullandığını gördüm, çünkü cevabında "web çağrıları boyunca kilitlere tutunmanın gerçekçi bir şekilde mümkün olmadığını" söyledi. Sıcak noktalar ve herhangi bir güncelleme veya silme yapmak için kilitleme gerektiren sayfalarla ilgili makalesini okuduktan sonra, aradığım şeyi yapmak için doğru kilitleri çalışabilsem bile, ölçeklenebilir olmayacağını ve büyük eşzamanlılığı ele almaz.

Şu anda nereye gideceğimi bilmiyorum. Satır işlenirken kilitleri korumaya ulaşılamadığı doğru mu (yüksek tps veya büyük eşzamanlılığı desteklemese bile)? Neyi kaçırıyorum?

Benden daha akıllı ve benden daha deneyimli insanların yardım edebileceği ümidiyle, aşağıda kullandığım test senaryosu. TOP 1 UPDATE yöntemine geri döndü, ancak ben de keşfetmek istiyorsanız, diğer yöntemi bıraktım, yorum yaptım.

Bunların her birini ayrı bir oturuma yapıştırın, 1. oturumu çalıştırın, ardından diğerlerini hızlı bir şekilde çalıştırın. Yaklaşık 50 saniye içinde test sona erecektir. Ne işe yaradığını (veya nasıl başarısız olduğunu) görmek için her oturumdaki Mesajlara bakın. İlk oturumda, mevcut kilitlerin ve işlenmekte olan kuyruk öğelerinin ayrıntılarını gösteren saniyede bir anlık görüntü alınan bir satır kümesi gösterilir. Bazen işe yarıyor ve diğer zamanlarda hiç çalışmıyor.

Sezon 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. Oturum

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

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

Oturum 4 ve üstü - istediğiniz kadar

/* 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
Bağlantılı makalede açıklanan sıralar saniyede yüzlerce veya daha düşük binlerce işleme ölçeklendirilebilir. Sıcak nokta çekişme sorunları yalnızca daha yüksek ölçekte geçerlidir. Üst düzey sistemde daha yüksek verim elde edebilen ve saniyede onbinlere ulaşan bilinen azaltma stratejileri vardır, ancak bu azalmalar dikkatli bir değerlendirmeye ihtiyaç duyar ve SQLCAT gözetiminde uygulanır.
Remus Rusanu

İlginç bir kırışıklık READPAST, UPDLOCK, ROWLOCKbenim QueueHistory tabloya veri yakalamak için benim komut dosyası ile hiçbir şey yapmıyor olmasıdır. Acaba StatusID işlenmedi mi? Kullanıyor WITH (NOLOCK)çalışması gerekir yani teorik ... ve daha önce iş mi! Neden şimdi çalışmadığından emin değilim, ama muhtemelen başka bir öğrenme deneyimi.
ErikE

Kodunuzu, çözmeye çalıştığınız kilitlenme ve diğer sorunları gösteren en küçük örneğe indirgeyebilir misiniz?
Nick Chammas

@Nick kodu azaltmaya çalışacağım. Diğer yorumlarınız hakkında, kümelenmiş dizinin bir parçası olan ve tarihten sonra sıralanan bir kimlik sütunu vardır. Bir "yıkıcı okuma" (ÇIKTI ile SİL) eğlendirmek için oldukça istekliyim ama istenen koşullardan biri, bir uygulama örneği başarısız olduğunda satırın otomatik olarak işlenmeye dönmesi idi. Yani burada sorum, bunun mümkün olup olmadığı.
ErikE

Tahribatlı okuma yaklaşımını deneyin ve dequeued öğeleri gerektiğinde yeniden enqueque olabilir nerede ayrı bir tabloya yerleştirin. Bu sorunu düzeltirse, bu yeniden tutma işleminin sorunsuz çalışmasına yatırım yapabilirsiniz.
Nick Chammas

Yanıtlar:


10

Tam olarak 3 kilit ipucuna ihtiyacınız var

  • READPAST
  • UPDLOCK
  • ıskarmoz

Bunu daha önce SO'da yanıtladım: /programming/939831/sql-server-process-queue-race-condition/940001#940001

Remus'un dediği gibi, hizmet komisyoncusu kullanmak daha güzel ama bu ipuçları işe yarıyor

Yalıtım düzeyi hakkındaki hatanız genellikle çoğaltma veya NOLOCK'un dahil olduğu anlamına gelir.


Komut dosyamda bu ipuçlarını yukarıda verildiği gibi kullanmak çıkmaz kilitler ve işlemler bozuyor. ( UPDATE SET ... FROM (SELECT TOP 1 ... FROM ... ORDER BY ...)) Bu, kilit tutan UPDATE kalıbımın çalışamayacağı anlamına mı geliyor? Ayrıca, kombine an READPASTile HOLDLOCKsize hata alıyorum. Bu sunucuda çoğaltma yok ve yalıtım düzeyi READ COMMITTED.
ErikE

2
@ErikE - Tablonun nasıl sorgulandığı kadar tablonun nasıl yapılandırıldığı da önemlidir. Kuyruk olarak kullandığınız tablo, ayıklanacak bir sonraki öğenin açık olmaması için ayıklama sırasına göre kümelenmelidir . Bu çok önemlidir. Yukarıda kodunuzu gözden geçirme, herhangi bir kümelenmiş dizin tanımlanmış görmüyorum.
Nick Chammas

@Nick mükemmel mantıklı geliyor ve neden düşünmediğimi bilmiyorum. Uygun PK kısıtlamasını ekledim (ve yukarıdaki komut dosyamı güncelledim) ve hala kilitlenme var. Ancak, öğeler, kilitlenmemiş öğelerin tekrar işlemesini engelleyerek doğru sırada işlendi.
ErikE

@ErikE - 1. Sıranız yalnızca kuyruğa alınmış öğeler içermelidir. Dequeuing ve item, kuyruk tablosundan silmek anlamına gelmelidir. Bunun yerine StatusIDbir öğeyi ayıklamak için güncelleniyor olduğunu görüyorum . Bu doğru mu? 2. Dequeue siparişiniz açık olmalıdır. Öğeleri sıraya göre sıralıyorsanız GETDATE(), yüksek hacimlerde birden fazla öğenin aynı anda ayıklama için eşit derecede uygun olması muhtemeldir. Bu, kilitlenmelere yol açacaktır. IDENTITYBelirsiz bir dequeue sırasını garanti etmek için kümelenmiş dizine bir eklemeyi öneririm .
Nick Chammas

1

SQL sunucusu ilişkisel verileri depolamak için harika çalışır. Bir iş kuyruğuna gelince, o kadar da iyi değil. MySQL için yazılan ancak burada da bu makaleye bakın. https://blog.engineyard.com/2011/5-subtle-ways-youre-using-mysql-as-a-queue-and-why-itll-bite-you


Teşekkürler, Eric. Soruya ilk cevabımda, SQL Server Service Broker'ı kullanmanızı öneriyordum çünkü kuyruk olarak tablo yönteminin gerçekten veritabanının ne için yapılmış olduğunu bilmiyorum. Ama bence bu artık iyi bir öneri değil çünkü SB gerçekten sadece mesajlar için. Veritabanına konulan verilerin ACID özellikleri, onu (ab) kullanmayı denemek için çok çekici bir kap haline getirir. Genel bir kuyruk olarak işlev görecek alternatif, düşük maliyetli bir ürün önerebilir misiniz? Ve yedeklenebilir vb.?
ErikE

8
Makale, kuyruk işlemede bilinen bir yanlışlıktan suçlu: devlet ve olayları tek bir tabloda birleştirin (aslında makale yorumlarına bakarsanız, bir süre önce buna itiraz ettiğimi göreceksiniz). Bu sorunun tipik belirtisi 'işlenmiş / işlenir' alanıdır. Olaylarla durumunu birleştiren (yani. Devlet tablo yapma 'kuyruk') büyük boyutlara 'kuyruk' büyüyen sonuçlanır (devlet tablo beri olan kuyruk). Olayları gerçek bir kuyruğa ayırmak 'boşaltan' (boş giden) bir kuyruğa yol açar ve bu çok daha iyi davranır .
Remus Rusanu

Makale tam olarak bunu önermiyor mu? Kuyruk tablosunun SADECE çalışmaya hazır öğeleri var mı?
ErikE

2
@ErikE: bu paragrafa atıfta bulunuyorsunuz, değil mi? aynı zamanda büyük tablo sendromundan kaçınmak da gerçekten çok kolay. Yeni e-postalar için ayrı bir tablo oluşturun ve bunları işlemeyi bitirdiğinizde, bunları uzun süreli depolama alanına ekleyin ve ardından kuyruk tablosundan SİLİN. Yeni e-postalar tablosu genellikle çok küçük kalacaktır ve üzerindeki işlemler hızlı olacaktır . Bununla ilgili kavga , 'büyük kuyruklar' sorunu için geçici bir çözüm olarak verildiğidir . Bu öneri makalenin açılışında olmalıydı, temel bir konudur.
Remus Rusanu

Devlet ve olay arasında net bir ayrım içinde düşünmeye başlarsanız, vdown'a çok daha kolay bir yol başlarsınız. Yukarıdaki öneri bile tabloya ve kuyruğa yeni e-postalar eklemeye dönüşecektir . İşleniyor, kuyruğu yoklar ve tablodaki durumu güncelleremailsnew_emailsnew_emailsemails . Bu aynı zamanda kuyruklarda dolaşan 'şişman' durum problemini de önler. Eğer biz dağıtılan işleme ve söz ediyorum gerçek iletişim ile kuyruklar, (örn. SSB) daha sonra işler paylaşılan devlet olarak daha compplicate dağıtımı yapılan sistemlerde problemlidir olsun.
Remus Rusanu
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.