AskOverflow.Dev

AskOverflow.Dev Logo AskOverflow.Dev Logo

AskOverflow.Dev Navigation

  • 主页
  • 系统&网络
  • Ubuntu
  • Unix
  • DBA
  • Computer
  • Coding
  • LangChain

Mobile menu

Close
  • 主页
  • 系统&网络
    • 最新
    • 热门
    • 标签
  • Ubuntu
    • 最新
    • 热门
    • 标签
  • Unix
    • 最新
    • 标签
  • DBA
    • 最新
    • 标签
  • Computer
    • 最新
    • 标签
  • Coding
    • 最新
    • 标签
主页 / dba / 问题 / 112818
Accepted
Vladimir Baranov
Vladimir Baranov
Asked: 2015-09-02 03:45:24 +0800 CST2015-09-02 03:45:24 +0800 CST 2015-09-02 03:45:24 +0800 CST

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

  • 772

我一直在阅读 Paul White 关于SQL Server Isolation Levels的一系列帖子,并遇到了一个短语:

为了强调这一点,用 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]

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

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
        ...
    }
}

这里有两个函数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

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

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

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

另外,我想知道不使用 sp_getapplock. 第二个问题:有没有更好的方法?

sql-server sql-server-2008
  • 2 2 个回答
  • 7540 Views

2 个回答

  • Voted
  1. Best Answer
    Paul White
    2015-09-02T21:30:00+08:002015-09-02T21:30:00+08:00

    这种方法正确吗?

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

    过程中的注释以解释策略并注意相关过程名称可能有助于其他人将来的维护。

    有没有更好的办法?

    在我看来,没有。

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

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


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

    Remus Rusanu使用表作为队列

    • 5
  2. Solomon Rutzky
    2015-09-02T18:08:14+08:002015-09-02T18:08:14+08:00

    这种情况似乎与以下问题非常相似:

    “签出”记录以进行处理的策略

    在我的回答中,我提倡一个类似于你在这里的模型,但sp_applock只有在最初的概念不是防弹的情况下才包含作为故障安全的概念。

    “签出”过程的主要区别在于我使用 CTE 和子句组合了SELECT和查询。这与 上的适当查询提示一起,允许更新用于确定行是否有资格进行处理的字段,同时返回该值,以便可以将其返回到调用进程。结合这两个步骤,没有应用锁应该没问题。反过来,摆脱应用程序锁定应该允许更大的吞吐量,因为在当前模型中执行“签出”过程的任何单个线程(如问题中所述)都会导致其余 9 个线程等待,即使它们可以几乎同时抓住下一个排队的人。UPDATEOUTPUT(READPAST, ROWLOCK, UPDLOCK)SELECT

    关于问题末尾的以下陈述:

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

    我想说的是,无论您是保留当前方法还是切换到“通过 CTE + OUTPUT 子句组合 SELECT + UPDATE”(tm)方法,sp_getapplock在SetCheckComplete存储过程中使用在逻辑上都是不必要的。不需要它的原因是:

    • 在任何给定时间,只有一个线程可以签出记录
    • 使用应用程序锁定SetCheckComplete意味着 的值LastCheckCompleted可以对GetNextToCheck, 然而:
      • 在签入记录之前,该LastCheckStarted字段将 > the LastCheckCompleted,并且此状态会导致该记录因LastCheckStarted <= LastCheckCompleted条件而被过滤出“GetNext”查询
      • 签入后,LastCheckStarted <= LastCheckCompleted将不再过滤掉记录,但LastCheckCompleted < DATEADD(minute, -20, GETDATE())条件会将其过滤掉,因为根据定义,它在查询运行前几毫秒就完成了。

    所以,这SetCheckComplete真的完全独立于GetNextToCheck过程。只有GetNextToCheck过程需要添加任何数量的保护措施。

    删除应用程序锁SetCheckComplete不仅应该是完全安全的,而且还会增加吞吐量,因为它会减少对任意锁的争用@Resource(同样,无论您是保留当前模型还是切换到我建议的模型)。


    更新

    对此答案的评论问题:

    GetNext查询在 WHERE 中有两个条件。服务器是否可能首先检查一个条件:LastCheckCompleted < DATEADD(minute, -20, GETDATE())- 对于当前签出的项目,此条件为真。然后SetCheckComplete改变 的值LastCheckCompleted。然后服务器检查第二个条件LastCheckStarted <= LastCheckCompleted,它似乎也为真。最终结果:我们签出了我们刚刚签入的一行,而无需等待 20 分钟。

    我的理解是,在单个对象(堆或索引)内这是不可能的,但在多个对象之间这并非不可能。查看架构,您确实有一个关于LastCheckCompleted. 所以有两件事:

    1. 我认为这种情况极不可能发生,因为它需要在验证第一个条件之后LastCheckCompleted进行更新,但我认为表(聚集索引)会在非聚集索引之前首先更新,但是对于这种情况来说真的,它必须从非聚集索引中获取值,对吗?LastCheckCompleted
    2. 无论哪种方式,一个简单的解决方法来禁止这种可能性(除了使用更高级别的事务隔离级别之外),就是创建一个状态字段。至少有一点复杂的问题是您需要额外的信息(即资格由“未签出”和“19 分钟前签入”组成)。也许您可以将当前的两个时间字段保留为信息,并添加一个新DATETIME字段,NULL用于“签出”或签入时间。然后只需检查 new_field <= 20 分钟前,NULL(即签出)行无论如何都不会匹配(除非有人傻到转身ANSI_NULLS OFF;-)。
    • 1

相关问题

  • 死锁的主要原因是什么,可以预防吗?

  • 我在索引上放了多少“填充”?

  • 是否有开发人员遵循数据库更改的“最佳实践”类型流程?

  • 如何确定是否需要或需要索引

  • 从 SQL Server 2008 降级到 2005

Sidebar

Stats

  • 问题 205573
  • 回答 270741
  • 最佳答案 135370
  • 用户 68524
  • 热门
  • 回答
  • Marko Smith

    连接到 PostgreSQL 服务器:致命:主机没有 pg_hba.conf 条目

    • 12 个回答
  • Marko Smith

    如何让sqlplus的输出出现在一行中?

    • 3 个回答
  • Marko Smith

    选择具有最大日期或最晚日期的日期

    • 3 个回答
  • Marko Smith

    如何列出 PostgreSQL 中的所有模式?

    • 4 个回答
  • Marko Smith

    列出指定表的所有列

    • 5 个回答
  • Marko Smith

    如何在不修改我自己的 tnsnames.ora 的情况下使用 sqlplus 连接到位于另一台主机上的 Oracle 数据库

    • 4 个回答
  • Marko Smith

    你如何mysqldump特定的表?

    • 4 个回答
  • Marko Smith

    使用 psql 列出数据库权限

    • 10 个回答
  • Marko Smith

    如何从 PostgreSQL 中的选择查询中将值插入表中?

    • 4 个回答
  • Marko Smith

    如何使用 psql 列出所有数据库和表?

    • 7 个回答
  • Martin Hope
    Jin 连接到 PostgreSQL 服务器:致命:主机没有 pg_hba.conf 条目 2014-12-02 02:54:58 +0800 CST
  • Martin Hope
    Stéphane 如何列出 PostgreSQL 中的所有模式? 2013-04-16 11:19:16 +0800 CST
  • Martin Hope
    Mike Walsh 为什么事务日志不断增长或空间不足? 2012-12-05 18:11:22 +0800 CST
  • Martin Hope
    Stephane Rolland 列出指定表的所有列 2012-08-14 04:44:44 +0800 CST
  • Martin Hope
    haxney MySQL 能否合理地对数十亿行执行查询? 2012-07-03 11:36:13 +0800 CST
  • Martin Hope
    qazwsx 如何监控大型 .sql 文件的导入进度? 2012-05-03 08:54:41 +0800 CST
  • Martin Hope
    markdorison 你如何mysqldump特定的表? 2011-12-17 12:39:37 +0800 CST
  • Martin Hope
    Jonas 如何使用 psql 对 SQL 查询进行计时? 2011-06-04 02:22:54 +0800 CST
  • Martin Hope
    Jonas 如何从 PostgreSQL 中的选择查询中将值插入表中? 2011-05-28 00:33:05 +0800 CST
  • Martin Hope
    Jonas 如何使用 psql 列出所有数据库和表? 2011-02-18 00:45:49 +0800 CST

热门标签

sql-server mysql postgresql sql-server-2014 sql-server-2016 oracle sql-server-2008 database-design query-performance sql-server-2017

Explore

  • 主页
  • 问题
    • 最新
    • 热门
  • 标签
  • 帮助

Footer

AskOverflow.Dev

关于我们

  • 关于我们
  • 联系我们

Legal Stuff

  • Privacy Policy

Language

  • Pt
  • Server
  • Unix

© 2023 AskOverflow.DEV All Rights Reserve