Sql server SQL Server中的长期应用程序级悲观锁定

Sql server SQL Server中的长期应用程序级悲观锁定,sql-server,tsql,Sql Server,Tsql,我有一个存储在SQL Server表中的资源(例如“文档”) 用户需要能够获得此资源上的应用程序级锁(而不是数据库锁),并可能永远保持该锁 我已经想出了一个策略来处理这个问题,但我不确定它是否正确,并希望得到一些建议 在这个特定的应用程序中,用户和资源的数量将非常少,因此不会有太多的争用。但知道处理这种情况的最恰当或“恰当”的方法仍然是件好事 记录锁的表格非常简单: CREATE TABLE [dbo].[ResourceLock] ( [Id] [int]

我有一个存储在SQL Server表中的资源(例如“文档”)

用户需要能够获得此资源上的应用程序级锁(而不是数据库锁),并可能永远保持该锁

我已经想出了一个策略来处理这个问题,但我不确定它是否正确,并希望得到一些建议

在这个特定的应用程序中,用户和资源的数量将非常少,因此不会有太多的争用。但知道处理这种情况的最恰当或“恰当”的方法仍然是件好事

记录锁的表格非常简单:

CREATE TABLE [dbo].[ResourceLock]
(
    [Id]            [int]           NOT NULL IDENTITY(1,1),
    [ResourceId]    [int]           NOT NULL,
    [UserId]        [int]           NOT NULL,
    [Created]       [datetime2](7)  NOT NULL,
    CONSTRAINT [PK_OrderLock] PRIMARY KEY CLUSTERED
    (
        [Id] ASC
    )
)
然后,查询将尝试解除对资源的锁定:

--Incoming Parameters 
DECLARE @ResourceId INT;
DECLARE @UserId INT;

--Query Variables
DECLARE @LockId INT;
DECLARE @InsertedIds TABLE (Id INT);

BEGIN TRANSACTION;

SELECT  @LockId = [ResourceLock].[Id]
--TABLOCKX the table so it can't be accessed until after the transaction is complete.
FROM    [ResourceLock] WITH (TABLOCKX)
WHERE   [ResourceLock].[ResourceId] = @ResourceId;

IF @LockId IS NULL
BEGIN
    INSERT INTO [ResourceLock]
    (
        [ResourceId],
        [UserId],
        [Created]
    )
    OUTPUT inserted.Id INTO @InsertedIds(Id)
    VALUES
    (
        @ResourceId,
        @UserId,
        GETUTCDATE()
    );

    SELECT  @LockID = Id
    FROM    @InsertedIds;
END

SELECT  [ResourceLock].*
FROM    [ResourceLock]
WHERE   [ResourceLock].[Id] = @LockId;

COMMIT TRANSACTION;
首先,它检查资源上是否已经存在锁。作为该检查的一部分,
SELECT
指定
TABLOCKX
阻止此查询的另一个实例或任何其他查询访问此表,直到此事务完成

如果没有现有的锁,它会为用户在资源上创建一个新锁

最后,它返回现有锁或新创建的锁

然后,应用程序将简单地将请求用户与持有锁的用户进行比较,并让用户知道他们是否已获得锁,或者锁是否由其他人持有

当其他查询尝试写入资源时,它们将使用如下检查:

--Incoming Parameters 
DECLARE @ResourceId INT;
DECLARE @UserId INT;

IF EXISTS
(
    SELECT  [ResourceLock].*
    FROM    [ResourceLock]
    WHERE   [ResourceLock].[ResourceId] = @ResourceId
    AND     [ResourceLock].[UserId] = @UserId
)
BEGIN
    --UPDATE the resource
END
我主要关心的是TABLOCKX的使用,因为它非常繁重。这感觉像是用另一种更有效的方法可以实现的,但我在搜索时没有找到类似的东西


我想我的搜索尝试受到了阻碍,因为我这里有两种类型的锁定。锁定SQL Server和应用程序资源锁定。

如果没有
HOLDLOCK
,我不确定您的解决方案是否可以工作。你测试过了吗

我想up可以通过
UPLOCK、HOLDLOCK、ROWLOCK来实现

您可以缩短它,并且不需要显式事务

INSERT INTO [ResourceLock] ([ResourceId], [UserId], [Created]) 
select @ResourceId, @UserId, GETUTCDATE()  
where not exists ( select 1 
                     from [ResourceLock] with (UPLOCK, HOLDLOCK, ROWLOCK) 
                    where [ResourceId] = @ResourceId
                  )

另一种方法是将@ResourceId本身视为要“锁定”的虚拟资源的标识符,这样您就可以自由地处理在操作表中记录此长期悲观锁定所需的任何DML。但是,在这种情况下,最好避免通常的阻塞/锁定等,因为它们不是非常可伸缩的,并且可能会引入死锁和其他挑战

您可以通过sp_getapplock使用SQL Server内置的锁管理器来临时锁定“资源”,这样您就可以在一段时间内单独处理该资源。但是,如果可以的话,最好不要在表中实际锁定数据行。正如您将在下面看到的,sp_getapplock可以用作一个“门”,如果您可以“锁定”虚拟资源,那么现在您就“进入”,并且您可以在该资源的范围内操作,而无需实际锁定/阻止/保留基表。基表上的锁不仅包含行锁,还包含对象锁和模式锁。不太好

如果愿意的话,使用sp_getapplock将允许您拥有自定义互斥锁,并且实际上不会锁定用户表,特别是使用像TABLOCKX这样的钝工具。当然,这一策略依赖于您和您的应用程序通过某种自定义锁语义来操作和使用sp_getapplock和sp_releaseapplock,但您已经想这样做了

--Construct unique name for 'resource'      
DECLARE @resourceName nvarchar(255) = 'RESOURCEID:' + CAST(@ResourceId AS NVARCHAR(10))

DECLARE @retVal INT = 0;

EXEC @retVal = sp_getapplock
  @Resource = @resourceName
  ,  @LockMode = 'Exclusive'
  ,  @LockOwner = 'Session' 
  ,  @LockTimeout = 10000;

IF @retVal <= 1
  BEGIN
    /*
     Do whatever you want here, including logging into your "long-term" lock 
     table, etc. You now have a pessimistic short-term lock on @resourceName, 
     and you can act with impunity in any related tables while you hold this 
    "lock", but you are not actually locking the database resources such as 
    your resource locking/logging table.
    */
  END

EXEC sp_releaseapplock
  @Resource = @resourceName
  @LockOwner = 'Session';
--为“资源”构造唯一名称
声明@resourceName-nvarchar(255)='RESOURCEID:'+CAST(@RESOURCEID-AS-nvarchar(10))
声明@retVal INT=0;
EXEC@retVal=sp_getapplock
@Resource=@resourceName
,@LockMode='Exclusive'
,@LockOwner='Session'
,@LockTimeout=10000;

如果@retVal有一个带有单个单元格的表可以在我看来为您完成这项工作。只需检查它是否为null,如果为null,则表示当前没有其他人在查询。这样就不需要使用
TABLOCKX
。您还可以使用该表单元格的事务来处理并发访问。如果资源只能由单个用户锁定,那么更明显的是,它上面有一个
唯一的
索引,这将作为逻辑错误的后盾。(捕捉约束冲突以查看是否存在锁也是可能的,但这有点笨拙。)我同意另一条评论,即表锁似乎不必要地过于繁重。
SERIALIZABLE
事务已经负责锁定适当的行,即使这些行还不存在。不需要使用全局锁。您应该事先声明表是应用程序级锁。@Jeroenmoster我的帖子说它很重;)这就是为什么我发布帖子征求意见的主要原因。谢谢你的提示,你介意为它创建一个答案吗?我的经验总是倾向于,在用户要求一些锁定系统后2周,他们开始询问绕过锁定的方法(“用户离开去吃午饭/那天/一份新工作”是通常的理由)。不久,你的锁就没有什么意义了,你必须处理冲突。试着说服你的用户,这几乎是不可避免的,因此你最好开始寻找冲突解决规则,而不是试图实现高压锁。这太棒了。我一直在用它。@AndyJ我想你漏掉了一点。锁定互斥锁,然后可以对实际的长期管理表进行读/写操作。互斥锁只是防止在执行此操作时出现各种物理锁定问题。因此,sp_getapplock--阅读长期锁定表——采取你想要的任何行动;sp_releaseapplock;。我的解决方案中没有提到使用会话锁作为长期解决方案。它是一种工具,使您能够将整个“过程”或“数据集”视为虚拟资源,以锁定套利