我正在撰写大量交易系统。我们以每秒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被评估为单个语句,因此我仍然得到“插入时的主键”问题似乎很荒谬。
有谁知道为什么会这样?您是否有任何想法可以指出我的解决方案?
答案 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)。