如何通过在SQL Server中获取错误(或跳过错误)来解除锁定

如何通过在SQL Server中获取错误(或跳过错误)来解除锁定,sql,sql-server,locking,Sql,Sql Server,Locking,我试图不锁定SQL Server中的特定查询 用例-高负载并行处理 我需要一种使用SQL Server及其transactionnal系统处理“工作队列”的方法,以确保工作已完成(在出现未经处理的故障(如IIS池崩溃/回收或应用程序崩溃)时,集成SQL Server事务回滚) 系统必须能够处理许多工人(我称他们为“WorkerApp”),他们必须做一些随机工作(“工作项”)并进行并行处理,一个工作项在任何情况下都不应运行两次(即使是高负载) 我希望有一个错误(任何东西,甚至是“SQL受害者”)或

我试图不锁定SQL Server中的特定查询

用例-高负载并行处理 我需要一种使用SQL Server及其transactionnal系统处理“工作队列”的方法,以确保工作已完成(在出现未经处理的故障(如IIS池崩溃/回收或应用程序崩溃)时,集成SQL Server事务回滚)

系统必须能够处理许多工人(我称他们为“WorkerApp”),他们必须做一些随机工作(“工作项”)并进行并行处理,一个工作项在任何情况下都不应运行两次(即使是高负载)

我希望有一个错误(任何东西,甚至是“SQL受害者”)或任何方式来理解正在使用的行,而不是真正的锁,这将导致块/死锁。。。这是我真的不想要的,因为这只会导致我的用例中性能不佳

此示例的SQL结构和值初始化: 第一个模拟长期工作的脚本 第二个有问题的脚本并行运行并锁定(这是我试图避免的) 我正在努力实现的目标 目标是第二个脚本在尝试更新序列化程序保存的记录时立即运行并失败(或者有办法知道更新由于锁定而未完成)


任何其他能够保护每个工作项(处于“IsRunning”状态)的解决方案对我来说都是有趣的。序列化只是一种尝试。

这就是SQL引擎的工作方式,UPDATE语句将始终要求表上的独占意图锁

查询从锁中崩溃的唯一方法是发生死锁,或者类似Bogdan Sahlean用
SET LOCK\u TIMEOUT 0
所述的死锁,但我非常不喜欢这种行为

在您的场景中,不会发生死锁,因为第一个查询的更新在第二个查询进入时已经发生。第一次查询完成后,第二次查询将正常执行(之前“只是”等待很长时间)


如果遇到等待锁的每个进程都必须崩溃,那么您的用户体验将非常低,只会收到错误消息,而不是简单的慢消息。

这就是SQL引擎的工作方式,UPDATE语句将始终要求表上具有独占意图的锁

查询从锁中崩溃的唯一方法是发生死锁,或者类似Bogdan Sahlean用
SET LOCK\u TIMEOUT 0
所述的死锁,但我非常不喜欢这种行为

在您的场景中,不会发生死锁,因为第一个查询的更新在第二个查询进入时已经发生。第一次查询完成后,第二次查询将正常执行(之前“只是”等待很长时间)


如果遇到等待锁定的每个进程都必须崩溃,那么您的用户体验将非常低,只会收到错误消息,而不是简单的缓慢。

[1]第一件事:不要使用带有
选择TOP(1)的两步方法…
然后
更新…
再加上一个事务,我将使用单个
更新
,因此:

UPDATE TOP(1)
    worker.Item
SET
    IsRunning = 1,
    @ItemId = ItemId 
WHERE IsRunning = 0;
SELECT @ItemId
注:

1.1
更新顶部(1)
将更新最多一行
,其中IsRunning=0

1.2
@ItemId=ItemId
(是的,这是可能的)将
@ItemId
标量变量中复制
ItemId
列的值

[2]如果您希望在遵循源代码时获得错误/异常

UPDATE 
    worker.Item
SET
    IsRunning = 0
WHERE
    ItemId = 1
当当前行被另一个并发连接/Tx锁定时,我将使用
设置锁定超时
,因此:{
-1
(默认)意味着如果行具有授予并发连接的不兼容锁定,
0
(非默认)表示它不会等待,而是会引发错误/异常}:

...
SET LOCK_TIMEOUT -1 -- Default behaviour
SELECT TOP 1
    @workId = ItemId
FROM
    worker.Item
WHERE 
    IsRunning = 1
;

-- Here is where I don't want it to lock
SET LOCK_TIMEOUT 0 -- Raise an exception if row is locked
UPDATE 
    worker.Item
SET
    IsRunning = 0
WHERE
    ItemId = 1
...
结果:第二条语句将引发以下异常

[3]此外,如果第二个脚本的目标是跳过锁定行(通过其他并发连接/Tx),那么一个解决方案是使用(以及
行锁定
)表提示和
读取提交
可重复读取
隔离级别,因此:

[3.1]

SET TRANSACTION ISOLATION LEVEL READ COMMITTED;
...
SELECT TOP 1
    @workId = ItemId
FROM
    worker.Item WITH(READPAST, ROWLOCK)
WHERE 
    IsRunning = 1
SET TRANSACTION ISOLATION LEVEL READ COMMITTED;
...
;WITH cte
AS (
SELECT  TOP(1)
        q.workId, q.IsRunning
FROM    Work.Item AS q WITH (ROWLOCK, READPAST)
WHERE   q.IsRunning = 0
ORDER BY q.workId
)
UPDATE  cte 
SET     @workId = workId,
        IsRunning = 1;

[3.2]

SET TRANSACTION ISOLATION LEVEL READ COMMITTED;
...
SELECT TOP 1
    @workId = ItemId
FROM
    worker.Item WITH(READPAST, ROWLOCK)
WHERE 
    IsRunning = 1
SET TRANSACTION ISOLATION LEVEL READ COMMITTED;
...
;WITH cte
AS (
SELECT  TOP(1)
        q.workId, q.IsRunning
FROM    Work.Item AS q WITH (ROWLOCK, READPAST)
WHERE   q.IsRunning = 0
ORDER BY q.workId
)
UPDATE  cte 
SET     @workId = workId,
        IsRunning = 1;
[4]无论如何,最初的要求并不明确。如果以上答案都不合适,那么您应该添加更多信息

[5]我将使用以下方法:

我将使用
IsProcessed BIT NOT NULL约束DF_Item_IsProcessed'默认值(0)
而不是
IsRunning BIT
,然后在事务中,我将使用[1]中的单步方法,因此:

SET XACT_ABORT ON
BEGIN TRY
BEGIN TRAN

    DECLARE @Id INT;
    WITH cte
    AS (
    SELECT  TOP(1)
            q.Id, q.IsProcessed
    FROM    Work.Item AS q WITH (ROWLOCK, READPAST)
    WHERE   q.IsProcessed = 0
    ORDER BY q.Id
    )
    UPDATE  cte 
    SET     @Id = Id,
            IsProcessed = 1;

    ...
    source code to process item @Id
    ...

COMMIT 
END TRY
BEGIN CATCH
IF @@TRANCOUNT > 0
BEGIN
    ROLLBACK
END
... other code for ex/err management ...
END CATCH
这样,每个
Tx
将锁定不同的行=
@Id
readpass
),如果发生错误
XACT\u ABORT
/
CATCH
将自动回滚当前
Tx
IsProcessed
将返回初始值
0
<代码>工作。项目应在
已处理
上具有索引:

CREATE UNIQUE INDEX IUN_Work_Item_IsProcessed_Id
ON Work.Item (IsProcessed, Id)
另一个索引选项是,如果IsProcessed=0的数量与IsProcessed=1相比[非常]小,则创建一个过滤索引:

CREATE UNIQUE INDEX IUN_Work_Item_Id_IsProcessed0
ON Work.Item (Id)
INCLUDE (IsProcessed)
WHERE IsProcessed = 0

注意:有关筛选索引创建和DML执行期间的正确设置,请参阅。在我看来,处理过的行应该被删除(参见Remus的方法),这样做有效。条目表将保持较小。

[1]第一件事:我将使用一个
选择TOP(1)
然后
更新…
加上一个事务的两步方法,而不是使用一个
更新
,因此:

UPDATE TOP(1)
    worker.Item
SET
    IsRunning = 1,
    @ItemId = ItemId 
WHERE IsRunning = 0;
SELECT @ItemId
注:

1.1
更新顶部(1)
将更新最多一行
,其中IsRunning=0

1.2
@ItemId=ItemId
(是的,可以这样做)将复制到
@ItemId
标量变量B中