SQL问题 - 高容量转换和PK违规

时间:2009-07-02 18:07:35

标签: sql multithreading sql-server-2008

我正在撰写大量交易系统。我们以每秒300-500左右的速度接收消息,然后需要尽快将这些消息保存到数据库中。这些消息存放在消息队列中,然后从那里读取。

我实现了一个Competing Consumer模式,它从队列中读取并允许对消息进行多线程处理。但是,当应用程序运行时,我经常遇到主键违规。

我们正在运行SQL 2008.示例表结构将是:

TableA
{
    MessageSequence INT PRIMARY KEY,
    Data VARCHAR(50)
}

调用存储过程来保留此消息,如下所示:

BEGIN TRANSACTION

INSERT TableA(MessageSequence, Data )
SELECT @MessageSequence, @Data
WHERE NOT EXISTS
(
  SELECT TOP 1 MessageSequence FROM TableA WHERE MessageSequence = @MessageSequence
)

IF (@@ROWCOUNT = 0)
BEGIN

UPDATE TableA
SET Data = @Data
WHERE MessageSequence = @MessageSequence

END

COMMIT TRANSACTION

所有这些都在TRY ... CATCH块中,所以如果出现错误,它会回滚事务。

我尝试过使用表格提示,比如ROWLOCK,但它没有什么区别。由于Insert被评估为单个语句,因此我仍然得到“插入时的主键”问题似乎很荒谬。

有谁知道为什么会这样?您是否有任何想法可以指出我的解决方案?

7 个答案:

答案 0 :(得分:3)

为什么会这样?

SELECT TOP 1 MessageSequence FROM TableA WHERE MessageSequence = @MessageSequence

此SELECT将尝试定位该行,如果未找到,则EXISTS运算符将返回FALSE并且INSERT将继续。 Hoewever,INSERT的决定是基于SELECT时的一个状态,但在INSERT时不再保证是真的。换句话说,你有竞争条件,其中两个线程都可以查找相同的@MessageSequence,两者都返回NOT EXISTS并且两者都尝试INSERT,当只有第一个将成功时,第二个将导致PK违规。

我该如何解决?

最快的解决方法是向SELECT添加WITH (UPDLOCK)提示,这将强制保留@MessageSequence键上的锁定,从而使INSERT / SELECT以原子方式运行:

INSERT TableA(MessageSequence, Data )
   SELECT @MessageSequence, @Data
   WHERE NOT EXISTS (
      SELECT TOP 1 MessageSequence FROM TableA WITH(UPDLOCK) WHERE MessageSequence = @MessageSequence)

为了防止SQL做像页面锁定这样的花哨的东西,你也可以添加ROWLOCK提示。

然而,这不是我的建议。我的建议可能会让你感到懊恼,但是这样:做最有可能成功的操作并在失败时处理错误。 IE浏览器。如果您的业务案例使@MessageSequnce更有可能是新的,请尝试INSERT并在失败时处理PK。通过这种方式,您可以避免虚假的查找,并且当第一次尝试成功时,捕获/重试的成本会在很多情况下摊销。

此外,使用built-in queues that come with SQL Server也许值得研究。

答案 1 :(得分:1)

答案 2 :(得分:0)

它可能与事务隔离级别有关。你可能需要

SET TRANSACTION ISOLATION LEVEL READ COMMITTED

开始交易之前。

此外,如果您有更多的更新而不是插入,则应首先尝试更新并检查rowcount并执行第二次插入。

答案 3 :(得分:0)

这与帖子939831非常相似。最终你想要使用提示(ROWLOCK,READPAST,UPDLOCK)。 READPAST告诉sql server如果当前锁定,则跳转到下一条记录。 UPDLOCK告诉sql server读锁定将升级到更新锁。

当我实现类似的东西时,我通过threadID

锁定了下一条记录
UPDATE TOP (1)
    foo
SET
    ProcessorID = @PROCID
FROM
    OrderTable foo WITH (ROWLOCK, READPAST, UPDLOCK)
WHERE
    ProcessorID = 0

然后选择了记录

SELECT *
FROM foo WITH (NOLOCK)
WHERE ProcessorID = @PROCID

然后将其标记为已处理

UPDATE foo
SET ProcessorID = -1
WHERE ProcessorID = @PROCID

稍后在非工作时间执行执行删除操作的相对昂贵的操作以清除已处理记录的队列。

答案 4 :(得分:0)

以下陈述的原子性就是你所追求的:

INSERT TableA(MessageSequence, Data )
SELECT @MessageSequence, @Data
WHERE NOT EXISTS
(
  SELECT TOP 1 MessageSequence FROM TableA WHERE MessageSequence = @MessageSequence
)

根据this person,它取决于当前的隔离级别。

答案 5 :(得分:0)

在切线上,如果您正在考虑使用大量交易系统,您可能需要考虑为此类数据设计的tick数据库[我不确定您在此处存储的是什么“消息”],如所讨论的在这个帖子中,例如:http://www.elitetrader.com/vb/showthread.php?threadid=81345

这些通常是具有专有查询语言的内存解决方案。我们在商店使用kdb +。

答案 6 :(得分:0)

不确定您使用的是哪种Messaging产品 - 但是可能值得查看不在数据库级别但在MQ级别的事务。

当然,如果您使用的是TM(事务管理器),那么这两个操作:1)从MQ获取,2)写入DB在同一父提交下都被“括号”。

所以我不确定你是否在这里使用了隐式或显式或任何TM(例如,微软的DTC)。

  • MessageSequence是PK,因此来自MQ的相同Message可以被处理两次。
  • 当你从MQ执行'GET'时,确保提交GET(即不是db-commit,而是MQ-commit) - 这将确保下一个线程无法“弹出”相同的MessageID。将消息写入数据库。