我喜欢sp_getapplock
并在少数地方自己使用这种方法,因为它具有灵活性,并且您可以完全控制锁定逻辑和等待时间。
我看到的唯一问题是,在您的情况下,并发进程并不完全相同。
您拥有将数据从临时表移动到主表的 SP1。您的系统从不尝试运行此 SP 的多个实例。
另一个将数据插入临时表的 SP2可以同时运行多次,这样做很好。
很容易实现阻止任何 SP1 或 SP2 组合的任何并发运行的锁定。换句话说,如果 SP1 和 SP2 的锁定逻辑相同并且它们被视为相同,则很容易。但是,您不能同时运行多个 SP2 实例。
如何实现锁定以防止 SP1 和 SP2 并发运行,同时允许多个 SP2 实例同时运行,这一点并不明显。
还有另一种方法不尝试阻止 SP 的并发运行,但接受并期望同时运行是可能的。
一种方法是IDENTITY
在临时表中添加一列。或者一个自动填充的日期时间,如果你可以保证它是唯一的并且永远不会减少,这可能会很棘手。或rowversion
列。
SP2 中将数据插入临时表的逻辑不会改变。
SP1 内部将数据从临时表移动到主表的逻辑需要使用这些标识值。
首先从 staging 表中读取 identity 的当前最大值,并将其记住在一个变量中,比如@MaxID
. 该 SP1 中临时表中的所有后续 SELECT、UPDATE 和 DELETE 都应包含一个 filter WHERE ID <= @MaxID
。
这将确保如果在 SP1 运行时恰好有新行添加到暂存表中,则该行不会被处理并且将保留在暂存表中,直到 SP1 的下一次运行。
这种方法的缺点是你不能使用TRUNCATE
,你需要使用DELETE
with WHERE ID <= @MaxID
。
如果您可以接受多个 SP2 实例(和 SP1)相互等待(和 SP1),那么您可以使用sp_getapplock
类似于以下内容。我的存储过程中有这段代码。您应该将此逻辑放入 SP1 和 SP2 中。
我sp_releaseapplock
这里没有显式调用,因为锁的所有者设置为事务,并且引擎会在事务结束时自动释放锁。
您不必将重试逻辑放在存储过程中,它可以在运行这些存储过程的外部代码中。在任何情况下,您的代码都应该准备好重试。
CREATE PROCEDURE SP2 -- or SP1
AS
BEGIN
SET NOCOUNT ON;
SET XACT_ABORT ON;
BEGIN TRANSACTION;
BEGIN TRY
-- Maximum number of retries
DECLARE @VarCount int = 10;
WHILE (@VarCount > 0)
BEGIN
SET @VarCount = @VarCount - 1;
DECLARE @VarLockResult int;
EXEC @VarLockResult = sp_getapplock
@Resource = 'StagingTable_app_lock',
-- this resource name should be the same in SP1 and SP2
@LockMode = 'Exclusive',
@LockOwner = 'Transaction',
@LockTimeout = 60000,
-- I'd set this timeout to be about twice the time
-- you expect SP to run normally
@DbPrincipal = 'public';
IF @VarLockResult >= 0
BEGIN
-- Acquired the lock
-- for SP2
-- INSERT INTO StagingTable ...
-- for SP1
-- SELECT FROM StagingTable ...
-- TRUNCATE StagingTable ...
-- don't retry any more
BREAK;
END ELSE BEGIN
-- wait for 5 seconds and retry
WAITFOR DELAY '00:00:05';
END;
END;
COMMIT TRANSACTION;
END TRY
BEGIN CATCH
ROLLBACK TRANSACTION;
-- log error
END CATCH;
END
此代码保证在任何给定时刻只有一个过程在使用临时表。没有并发。所有其他实例将等待。
显然,如果您尝试不通过这些 SP1 或 SP2(它们首先尝试获取锁)来访问 staging 表,那么这种访问将不会被阻止。