尝试在Sql Server 2012中实现有序作业队列

时间:2015-04-25 07:02:16

标签: sql-server concurrency sql-server-2012

我想在Sql Server 2012中实现有序作业队列。以下是上下文:

  1. 许多代理同时从该队列中获取N个作业,其中每个代理的N可能不同(取决于特定代理的负载)。
  2. 应该按顺序完成工作。假设它是主键顺序(实际上它略有不同)。因此,代理商应该更喜欢较旧的工作。
  3. 某些作业限制了并发性。例如,如果作业A和B属于同一个并发组,则可以排队两者,但它禁止同时运行两者。因此,其中一个先运行(根据先前定义的顺序),然​​后才能运行第二个作业。
  4. 请查看以下SQL小提琴 - http://sqlfiddle.com/#!6/ca1a2f/1

    理想情况下,我会使用带有UPDATE TOP (N)提示和READPAST子句的单个OUTPUT语句。但是,UPDATE不承诺任何订单,因此我采用了不同的方法:

    1. 使用SELECT TOP (N)UPDLOCK提示将READPAST排序到临时表中。实际上,我保留了这些记录。
    2. 包含UPDATE子句的常规OUTPUT
    3. 但是,这样的批量更新可能会失败,因为批量中的作业是并发限制的。在这种情况下,我回过头来迭代保留的记录并尝试将它们标记为逐个运行,默默地跳过那些不符合并发限制的记录。

      以下是SQL Fiddle的完整查询:

      BEGIN TRAN
      
      exec sp_executesql N'
      DECLARE @Running TABLE (WorkItemId BIGINT)
      
      SELECT TOP (@Count) WorkItemId INTO #Candidates 
      FROM BackgroundJobWork WITH (UPDLOCK, READPAST) 
      WHERE Status = 0 
      ORDER BY WorkItemId
      
      BEGIN TRY
          PRINT '' *** BATCH *** ''
          UPDATE BackgroundJobWork SET Status = 3
          OUTPUT inserted.WorkItemId INTO @Running
          FROM BackgroundJobWork 
          WHERE WorkItemId IN (SELECT WorkItemId FROM #Candidates)
      END TRY
      BEGIN CATCH
          PRINT '' *** ONE BY ONE *** ''
          DECLARE @WorkItemId BIGINT
          DECLARE c CURSOR FAST_FORWARD FOR 
          SELECT WorkItemId FROM #Candidates ORDER BY WorkItemId
      
          OPEN c
          FETCH NEXT FROM c INTO @WorkItemId
      
          WHILE @@FETCH_STATUS = 0
          BEGIN
              BEGIN TRY
                  UPDATE BackgroundJobWork SET Status = 3
                  OUTPUT inserted.WorkItemId INTO @Running
                  FROM BackgroundJobWork 
                  WHERE WorkItemId = @WorkItemId
              END TRY
              BEGIN CATCH
              END CATCH
      
              FETCH NEXT FROM c INTO @WorkItemId
          END
      
          CLOSE c
          DEALLOCATE c
      END CATCH
      
      SELECT * FROM @Running
      ',N'@Count int',@Count=6
      
      ROLLBACK
      

      (回滚用于测试目的,状态0表示收到,状态3表示正在运行

      所以,有两种情况:

      1. 没有并发限制 - 批量更新不会失败
      2. 存在并发限制 - 回退到使用游标逐个更新
      3. 我的目标是测试同时运行此查询的两个代理不会相互干扰,即没有人会被锁定等待另一个完成。

        我首先运行以下查询来模拟并发限制的存在:

        UPDATE BackgroundJobWork 
        SET ConcurrencyGroupName = CONVERT(NVARCHAR(2), CASE 
          WHEN WorkItemId % 2 = 0 THEN NULL 
          ELSE WorkItemId % 4 
        END) WHERE Status < 100
        

        这产生以下结果:

        SELECT WorkItemId,Status,ConcurrencyGroupName FROM BackgroundJobWork 
        WHERE Status < 100 ORDER BY WorkItemId
        
        WorkItemId  Status  ConcurrencyGroupName
        1           0       1
        2           0       NULL
        3           0       3
        4           0       NULL
        5           0       1
        6           0       NULL
        7           0       3
        8           0       NULL
        9           0       1
        10          0       NULL
        11          0       3
        12          0       NULL
        

        如你所见:

        • 作业1,5,9属于并发限制1
        • Jobs 3,7,11属于并发限制3
        • 作业2,4,6,8,10,12不是并发限制

        UPDATE BackgroundJobWork SET ConcurrencyGroupName = NULL WHERE Status < 100删除所有并发限制。

        不幸的是,我不知道如何在SQL Fiddle中演示两个代理。以下是我在SSMS中的表现:

        1. 两个SQL脚本窗口,每个窗口都包含查询。
        2. 在第一个窗口中注释掉ROLLBACK声明。
        3. 从第一个窗口运行SQL。请注意,交易仍处于打开状态,即所有锁定仍然存在。
        4. 现在从第二个窗口运行SQL。
        5. 最后,通过执行ROLLBACK语句从第一个窗口回滚事务。
        6. 批量更新工作得很好 - 第二个窗口没有被第一个窗口中启动的打开事务锁定。我可以在第一个窗口看到作业1,2,3,4,5,6,在第二个窗口看到7,8,9,10,11,12 -

          但是,当我模拟并发限制(使用上述查询)时,第二个窗口被锁定,等待第一个窗口释放锁。

          我对此感到非常困惑。毕竟,每个窗口仅更新先前使用相应SELECT语句保留的记录!这些集合是不相交的 - UPDLOCKREADPAST保证它。

          追加 - 查询持有的锁

          我正在使用以下查询检查所持有的锁(在另一个SSMS窗口中):

          DECLARE @locks TABLE (spid INT,
                                dbid INT,
                                ObjId BIGINT,
                                IndId INT,
                                Type NVARCHAR(10),
                                Resource NVARCHAR(128),
                                Mode NVARCHAR(10),
                                Status NVARCHAR(32))
          INSERT INTO @locks EXECUTE sp_lock
          
          SELECT spid, OBJECT_NAME(ObjId) ObjectName, i.name, l.Type, Mode, COUNT(1) Count
          FROM @locks l
          LEFT JOIN sys.indexes i ON i.index_id = l.IndId AND i.object_id = l.ObjId
          WHERE Mode NOT IN ('S','IS') AND dbid = DB_ID('747_DFControl2')
          GROUP BY spid,OBJECT_NAME(ObjId),l.Type,i.name,Mode
          ORDER BY spid,OBJECT_NAME(ObjId),l.Type,i.name,Mode
          

          (747_DFControl2是我的数据库的名称)

          如果我在没有并发限制时运行它(即批量更新成功),我会得到以下输出:

          spid    ObjectName          name                    Type    Mode    Count
          60      BackgroundJobWork   IX_Status               KEY     X       12
          60      BackgroundJobWork   PK_BackgroundJobWork    KEY     X       6
          60      BackgroundJobWork   IX_Status               PAG     IX      1
          60      BackgroundJobWork   PK_BackgroundJobWork    PAG     IX      1
          60      BackgroundJobWork   NULL                    TAB     IX      1
          

          spid 60对应于第一个窗口(具有打开事务的窗口)。我们没有看到第二个窗口 - 它已成功回滚。

          这是启用并发限制并且第二个窗口(spid 63)等待释放锁的结果:

          spid    ObjectName          name                        Type    Mode    Count
          60      BackgroundJobWork   IX_ConcurrencyRestriction   KEY     X       2
          60      BackgroundJobWork   IX_Status                   KEY     X       12
          60      BackgroundJobWork   PK_BackgroundJobWork        KEY     X       6
          60      BackgroundJobWork   IX_ConcurrencyRestriction   PAG     IX      1
          60      BackgroundJobWork   IX_Status                   PAG     IX      1
          60      BackgroundJobWork   PK_BackgroundJobWork        PAG     IX      1
          60      BackgroundJobWork   NULL                        TAB     IX      1
          63      BackgroundJobWork   IX_ConcurrencyRestriction   KEY     X       1
          63      BackgroundJobWork   IX_Status                   KEY     X       12
          63      BackgroundJobWork   PK_BackgroundJobWork        KEY     X       6
          63      BackgroundJobWork   IX_ConcurrencyRestriction   PAG     IX      1
          63      BackgroundJobWork   IX_Status                   PAG     IX      1
          63      BackgroundJobWork   PK_BackgroundJobWork        PAG     IX      1
          63      BackgroundJobWork   NULL                        TAB     IX      1
          

          这并没有告诉我太多。

          有人可以向我解释为什么第二个查询实例被锁定了吗?

          修改

          从问题不清楚,当我打开并发限制时,为什么批量更新会失败。很明显,从SQL Fiddle开始,BackgroundJobWork表上有一个条件唯一索引:

          CREATE UNIQUE NONCLUSTERED INDEX IX_ConcurrencyRestriction ON BackgroundJobWork (ConcurrencyGroupName) 
          WHERE (Status=3 AND ConcurrencyGroupName IS NOT NULL)
          

0 个答案:

没有答案