使用 sp_getapplock 实现队列。这是正确的吗?有没有更好的办法?

Vla*_*nov 5 sql-server-2008 sql-server t-sql locking

我一直在阅读 Paul White 撰写的关于SQL Server 隔离级别的一系列文章,并遇到了一个短语

为了强调这一点,无论发生什么并发修改,用 T-SQL 编写的伪约束都必须正确执行。应用程序开发人员可能会使用 lock 语句来保护敏感操作。T-SQL 程序员最接近风险存储过程和触发器代码的工具是相对很少使用的sp_getapplock系统存储过程。这并不是说它是唯一的,甚至是首选的选择,只是它存在并且在某些情况下可能是正确的选择。

我正在使用sp_getapplock,这让我想知道我是否正确使用它,或者有更好的方法来获得所需的效果。

我有一个 C++ 应用程序,可以 24/7 全天候循环处理所谓的“构建服务器”。有一个包含这些建筑物服务器列表的表格(大约 200 行)。可以随时添加新行,但这种情况并不经常发生。行永远不会被删除,但它们可以被标记为不活动。处理一个服务器可能需要几秒到几十分钟,每个服务器都不一样,有的“小”,有的“大”。一旦服务器被处理,应用程序必须等待至少 20 分钟才能再次处理它(服务器不应过于频繁地轮询)。应用程序启动 10 个并行执行处理的线程,但我必须保证没有两个线程试图同时处理同一个服务器. 两个不同的服务器可以而且应该同时处理,但每个服务器的处理频率不能超过 20 分钟一次。

下面是一个表的定义:

CREATE TABLE [dbo].[PortalBuildingServers](
    [InternalIP] [varchar](64) NOT NULL,
    [LastCheckStarted] [datetime] NOT NULL,
    [LastCheckCompleted] [datetime] NOT NULL,
    [IsActiveAndNotDisabled] [bit] NOT NULL,
    [MaxBSMonitoringEventLogItemID] [bigint] NOT NULL,
CONSTRAINT [PK_PortalBuildingServers] PRIMARY KEY CLUSTERED 
(
    [InternalIP] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]
) ON [PRIMARY]

CREATE NONCLUSTERED INDEX [IX_LastCheckCompleted] ON [dbo].[PortalBuildingServers]
(
    [LastCheckCompleted] ASC
)
INCLUDE 
(
    [LastCheckStarted],
    [IsActiveAndNotDisabled],
    [MaxBSMonitoringEventLogItemID]
) WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, SORT_IN_TEMPDB = OFF, DROP_EXISTING = OFF, ONLINE = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]
Run Code Online (Sandbox Code Playgroud)

应用程序中工作线程的主循环如下所示:

for(;;)
{
    // Choose building server for checking
    std::vector<SBuildingServer> vecBS = GetNextBSToCheck();
    if (vecBS.size() == 1)
    {
        // do the check and don't go to sleep afterwards
        SBuildingServer & bs = vecBS[0];
        DoCheck(bs);
        SetCheckComplete(bs);
    }
    else
    {
        // Sleep for a while
        ...
    }
}
Run Code Online (Sandbox Code Playgroud)

这里有两个函数GetNextBSToCheck并且SetCheckComplete正在调用相应的存储过程。

GetNextBSToCheck返回 0 或 1 行,其中包含接下来应处理的服务器的详细信息。它是一个最长时间没有被处理的服务器。如果这个“最旧的”服务器在不到 20 分钟前被处理,则不会返回任何行并且线程将等待一分钟。

SetCheckComplete 设置处理完成的时间,从而可以在 20 分钟后再次选择该服务器进行处理。

最后,存储过程的代码:

GetNextToCheck

CREATE PROCEDURE [dbo].[GetNextToCheck]
AS
BEGIN
    SET NOCOUNT ON;

    BEGIN TRANSACTION;
    BEGIN TRY
        DECLARE @VarInternalIP varchar(64) = NULL;
        DECLARE @VarMaxBSMonitoringEventLogItemID bigint = NULL;

        DECLARE @VarLockResult int;
        EXEC @VarLockResult = sp_getapplock
            @Resource = 'PortalBSChecking_app_lock',
            @LockMode = 'Exclusive',
            @LockOwner = 'Transaction',
            @LockTimeout = 60000,
            @DbPrincipal = 'public';

        IF @VarLockResult >= 0
        BEGIN
            -- Acquired the lock
            -- Find BS that wasn't checked for the longest period
            SELECT TOP 1
                @VarInternalIP = InternalIP
                ,@VarMaxBSMonitoringEventLogItemID = MaxBSMonitoringEventLogItemID
            FROM
                dbo.PortalBuildingServers
            WHERE
                LastCheckStarted <= LastCheckCompleted
                -- this BS is not being checked right now
                AND LastCheckCompleted < DATEADD(minute, -20, GETDATE())
                -- last check was done more than 20 minutes ago
                AND IsActiveAndNotDisabled = 1
            ORDER BY LastCheckCompleted
            ;

            -- Start checking the found BS
            UPDATE dbo.PortalBuildingServers
            SET LastCheckStarted = GETDATE()
            WHERE InternalIP = @VarInternalIP;
            -- There is no need to explicitly verify if we found anything.
            -- If @VarInternalIP is null, no rows will be updated
        END;

        -- Return found BS, 
        -- or no rows if nothing was found, or failed to acquire the lock
        SELECT
            @VarInternalIP AS InternalIP
            ,@VarMaxBSMonitoringEventLogItemID AS MaxBSMonitoringEventLogItemID
        WHERE
            @VarInternalIP IS NOT NULL
            AND @VarMaxBSMonitoringEventLogItemID IS NOT NULL
        ;

        COMMIT TRANSACTION;
    END TRY
    BEGIN CATCH
        ROLLBACK TRANSACTION;
    END CATCH;

END
Run Code Online (Sandbox Code Playgroud)

SetCheckComplete

CREATE PROCEDURE [dbo].[SetCheckComplete]
    @ParamInternalIP varchar(64)
AS
BEGIN
    SET NOCOUNT ON;

    BEGIN TRANSACTION;
    BEGIN TRY

        DECLARE @VarLockResult int;
        EXEC @VarLockResult = sp_getapplock
            @Resource = 'PortalBSChecking_app_lock',
            @LockMode = 'Exclusive',
            @LockOwner = 'Transaction',
            @LockTimeout = 60000,
            @DbPrincipal = 'public';

        IF @VarLockResult >= 0
        BEGIN
            -- Acquired the lock
            -- Completed checking the given BS
            UPDATE dbo.PortalBuildingServers
            SET LastCheckCompleted = GETDATE()
            WHERE InternalIP = @ParamInternalIP;
        END;

        COMMIT TRANSACTION;
    END TRY
    BEGIN CATCH
        ROLLBACK TRANSACTION;
    END CATCH;

END
Run Code Online (Sandbox Code Playgroud)

如您所见,我sp_getapplock用来保证在任何给定时间只有这两个存储过程的一个实例正在运行。我想我需要sp_getapplock在这两个过程中使用,因为选择“最旧”服务器的查询使用LastCheckCompleted时间,该时间由SetCheckComplete.

我认为这段代码确实保证没有两个线程试图同时处理同一个服务器,但如果你能指出这段代码和整体方法的任何问题,我将不胜感激。那么,第一个问题:这种方法是否正确?

另外,我想知道是否可以在不使用 sp_getapplock. 第二个问题:有没有更好的办法?

Pau*_*ite 5

这种方法是否正确?

是的。它满足问题中陈述的所有目标。

在程序中添加注释以解释策略并注明相关程序名称可能有助于其他人将来进行维护。

有没有更好的办法?

在我看来,不。

获取单个锁是一个非常快的操作,并且会产生非常清晰的逻辑。我不清楚在第二个过程中获取锁是多余的,但即使是,省略它你真正得到了什么?您实施的简单性和安全性吸引了我。

替代方案要复杂得多,并且可能会让您想知道您是否真的涵盖了所有情况,或者未来内部引擎细节是否可能会发生变化,从而打破(可能是微妙和未说明的)假设。


如果您需要更传统的排队实现,以下参考非常有用:

使用表作为队列莱姆斯Rusanu