问题:什么事情可能导致以下问题?
我们有一个存储过程,用于将价格记录更新或插入表格中。很直接。但是,在间歇性的时间,我们会尝试插入已存在的行时发生主键冲突。如您所见,我们正在检查是否存在记录,然后根据需要进行更新或插入。
这种情况很少发生,但最后一次发生时我们在3分钟的窗口内发生了18次。我们更新了比18更多的行,所以不是每次都有。这个SP经常被调用。我们检查并且当时没有进行索引维护。调用此SP的应用程序只是循环通过队列来更新/插入这些价格,并且只有一个应用程序实例正在运行。
这是在具有3台服务器的2016年可用性组上运行。
ALTER PROCEDURE [dbo].[mw_UpdatePrice] @CustTypeID INT
,@ID INT
,@Price MONEY
,@OldPrice MONEY
,@ExpirationDate DATETIME
,@PriceStatusID INT
,@PriceStatusDesc VARCHAR(80)
,@FreeFreightShipviaServiceLevelID INT = NULL
,@FreeFreightShipViaServiceLevelDescription VARCHAR(150) = NULL
,@FreeFreightShipViaServiceLevelRank INT = NULL
AS
BEGIN
SET NOCOUNT ON;
IF EXISTS (
SELECT 1
FROM dbo.Price
WHERE ID = @ID
AND CustTypeID = @CustTypeID
)
BEGIN
UPDATE dbo.Price
SET Price = @Price
,OldPrice = @OldPrice
,ExpirationDate = @ExpirationDate
,PriceStatusID = @PriceStatusID
,PriceStatusDescription = @PriceStatusDesc
,ServiceLevelName = @FreeFreightShipViaServiceLevelDescription
,ServiceLevelId = @FreeFreightShipviaServiceLevelID
,ServiceLevelRank = @FreeFreightShipViaServiceLevelRank
WHERE ID = @ID
AND CustTypeID = @CustTypeID
END
ELSE
BEGIN
INSERT dbo.Price (
ID
,CustTypeID
,Price
,OldPrice
,ExpirationDate
,PriceStatusID
,PriceStatusDescription
,ServiceLevelName
,ServiceLevelID
,ServiceLevelRank
)
VALUES (
@ID
,@CustTypeID
,@Price
,@OldPrice
,@ExpirationDate
,@PriceStatusID
,@PriceStatusDesc
,@FreeFreightShipViaServiceLevelDescription
,@FreeFreightShipviaServiceLevelID
,@FreeFreightShipViaServiceLevelRank
)
END
END
答案 0 :(得分:1)
正如评论所提到的,这个存储过程并不是安全的并发调用 - 它具有插入和更新方案的竞争条件。这可以通过确保对存储过程的每次调用都包含在单独的事务中(在SP中添加begin / end tran,或从应用程序代码启动事务),并将HOLDLOCK
应用于SELECT和INSERT语句:
SELECT 1
FROM dbo.Price WITH (UPDLOCK, HOLDLOCK)
WHERE ID = @ID
AND CustTypeID = @CustTypeID
...
INSERT dbo.Price WITH (HOLDLOCK)
...
即使将其重构为MERGE
语句,仍然需要HOLDLOCK
来防止此问题。示例here。
HOLDLOCK
会锁定整个表,并会降低此SP的并发吞吐量。
除了并发问题之外,如果ANSI_NULLS打开,可能会发生PRIMARY KEY违规的另一个可能原因,可能允许应用程序逻辑错误,其中使用@CustTypeID或@ID的NULL值调用SP,导致应用程序以一种非预期的方式行事。例如,使用@CustTypeID = NULL和@ID = 1调用将始终导致INSERT,这可能不是预期的行为。
修改强>
根据@DavidBrowne
添加UPDLOCK