[英]Record locking and concurrency issues
我的逻辑架构如下:
标头记录可以有多个子记录。
多台PC可以通过接受子记录详细信息的存储过程和值插入子记录。
多台PC可以通过存储过程查询未处理的头记录
所以基本上我的标题查询看起来像这样:
BEGIN TRANSACTION;
SET TRANSACTION ISOLATION LEVEL READ COMMITTED;
SELECT TOP 1
*
INTO
#unprocessed
FROM
Header h WITH (READPAST, UPDLOCK)
JOIN
Child part1 ON part1.HeaderID = h.HeaderID AND part1.Name = 'XYZ'
JOIN
Child part2 ON part1.HeaderID = part2.HeaderID AND
WHERE
h.Processed = 0x0;
UPDATE
Header
SET
Processed = 0x1
WHERE
HeaderID IN (SELECT [HeaderID] FROM #unprocessed);
SELECT * FROM #unprocessed
COMMIT TRAN
因此,上述查询可确保并发查询永远不会返回相同的记录。
我认为我的问题在于插入查询。 这就是我所拥有的:
DECLARE @HeaderID INT
BEGIN TRAN
--Create header record if it doesn't exist, otherwise get it's HeaderID
MERGE INTO
Header WITH (HOLDLOCK) as target
USING
(
SELECT
[Value] = @Value, --stored procedure parameter
[HeaderID]
) as source ([Value], [HeaderID]) ON target.[Value] = source.[Value] AND
target.[Processed] = 0
WHEN MATCHED THEN
UPDATE SET
--Get the ID of the existing header
@HeaderID = target.[HeaderID],
[LastInsert] = sysdatetimeoffset()
WHEN NOT MATCHED THEN
INSERT
(
[Value]
)
VALUES
(
source.[Value]
)
--Get new or existing ID
SELECT @HeaderID = COALESCE(@HeaderID , SCOPE_IDENTITY());
--Insert child with the new or existing HeaderID
INSERT INTO
[Correlation].[CorrelationSetPart]
(
[HeaderID],
[Name]
)
VALUES
(
@HeaderID,
@Name --stored procedure parameter
);
我的问题是插入查询经常被上面的选择查询阻止,我收到超时。 选择查询由代理调用,因此可以相当快地调用它。 有一个更好的方法吗? 注意,我可以控制数据库模式。
回答问题的第二部分
您只需要一台机器PC查询和处理每个标头记录。 永远不应该有一个实例,其中标题记录及其子项应由多台PC处理
看一下sp_getapplock 。
我在类似的场景中使用app锁。 我有一个必须处理的对象表,类似于您的标题表。 客户端应用程序同时运行多个线程。 每个线程执行一个存储过程,该过程从对象表返回下一个要处理的对象。 因此,存储过程的主要任务不是自己进行处理,而是返回队列中需要处理的第一个对象。 代码可能如下所示:
CREATE PROCEDURE [dbo].[GetNextHeaderToProcess]
AS
BEGIN
-- SET NOCOUNT ON added to prevent extra result sets from
-- interfering with SELECT statements.
SET NOCOUNT ON;
BEGIN TRANSACTION;
BEGIN TRY
DECLARE @VarHeaderID int = NULL;
DECLARE @VarLockResult int;
EXEC @VarLockResult = sp_getapplock
@Resource = 'GetNextHeaderToProcess_app_lock',
@LockMode = 'Exclusive',
@LockOwner = 'Transaction',
@LockTimeout = 60000,
@DbPrincipal = 'public';
IF @VarLockResult >= 0
BEGIN
-- Acquired the lock
-- Find the most suitable header for processing
SELECT TOP 1
@VarHeaderID = h.HeaderID
FROM
Header h
JOIN Child part1 ON part1.HeaderID = h.HeaderID AND part1.Name = 'XYZ'
JOIN Child part2 ON part1.HeaderID = part2.HeaderID
WHERE
h.Processed = 0x0
ORDER BY ....;
-- sorting is optional, but often useful
-- for example, order by some timestamp to process oldest/newest headers first
-- Mark the found Header to prevent multiple processing.
UPDATE Header
SET Processed = 2 -- in progress. Another procedure that performs the actual processing should set it to 1 when processing is complete.
WHERE HeaderID = @VarHeaderID;
-- There is no need to explicitly verify if we found anything.
-- If @VarHeaderID is null, no rows will be updated
END;
-- Return found Header, or no rows if nothing was found, or failed to acquire the lock
SELECT
@VarHeaderID AS HeaderID
WHERE
@VarHeaderID IS NOT NULL
;
COMMIT TRANSACTION;
END TRY
BEGIN CATCH
ROLLBACK TRANSACTION;
END CATCH;
END
应该从执行实际处理的过程调用此过程。 在我的情况下,客户端应用程序执行实际处理,在您的情况下,它可能是另一个存储过程。 我们的想法是,我们在短时间内获得了应用程序锁定。 当然,如果实际处理速度很快,您可以将其放入锁中,因此一次只能处理一个标题。
获取锁后,我们会查找最合适的标头进行处理,然后设置其Processed标志。 根据处理的性质,您可以立即将标志设置为1(已处理),或将其设置为某个中间值,如2(进行中),然后将其设置为1(已处理)。 在任何情况下,一旦标志不为零,将不再选择标题进行处理。
这些应用程序锁与DB在读取和更新行时放置的正常锁分开,它们不应干扰插入。 在任何情况下,它都应该像WITH (UPDLOCK)
一样锁定整个表。
回到问题的第一部分
您只需要为任何给定的“值”插入一个标题记录。 因此,如果插入两个子记录并提供相同的“值”,则只应创建一次标题。
您可以使用相同的方法:在插入过程开始时获取应用程序锁定(使用与查询过程中使用的应用程序锁定不同的名称)。 因此,您可以保证插入顺序发生,而不是同时发生。 顺便说一下,实际上最有可能的插入无论如何也不会同时发生。 DB将在内部顺序执行它们。 它们将彼此等待,因为每个插入都会锁定一个表以进行更新。 此外,每个插入都写入事务日志,所有对事务日志的写入也是顺序的。 因此,只需将sp_getapplock添加到插入过程的开头,并在MERGE中删除WITH (HOLDLOCK)
提示。
当过程没有返回任何行时,GetNextHeaderToProcess过程的调用者应该正确处理这种情况。 如果锁定获取超时,或者根本没有更多标头要处理,则会发生这种情况。 通常,处理部分会在一段时间后重试。
插入过程应检查锁定获取是否失败,并重试插入或以某种方式向调用者报告问题。 我通常会将生成的插入行的身份ID(在您的情况下为ChildID)返回给调用者。 如果procedure返回0,则表示插入失败。 呼叫者可以决定做什么。
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.