【问题标题】:Duplicate key violation with composite primary key on high frequency insert高频插入时复合主键的重复键违规
【发布时间】:2018-02-22 08:38:02
【问题描述】:

我有一张如下表:

CREATE TABLE [Alg].[Sequence](
    [Id] [int] IDENTITY(1,1) NOT NULL,
    [SequenceId] [int] NOT NULL,
    [CustomerId] [bigint] NOT NULL,
    [Data] [text] NULL,
 CONSTRAINT [PK_Sequence] PRIMARY KEY CLUSTERED 
(
    [SequenceId] ASC,
    [CustomerId] ASC
)

这是另一个表的插入/更新触发器的块,我将数据插入到Sequence表:

--insert data into sequence table
SELECT @MaxSeqId =  ISNULL(MAX(SequenceId),0)
FROM Alg.[Sequence] WITH (ROWLOCK)
WHERE CustomerId = @CustomerId

INSERT INTO Alg.[Sequence]
VALUES (
@MaxSeqId + 1
,@CustomerId
,@SendingData
,GETDATE()
)

所以每当高频插入过程(在有触发器的表上),就会发生违反重复键错误。我试过ROWLOCK,但没有用。我怎样才能防止这种情况发生?

更新

有人问我为什么不使用内置序列,我尝试过但找不到如何将序列与复合主键一起使用。我不希望 SequenceId 列是标识,实际上,我想保留 SequenceId 作为每个 CustomerId 的标识。您可以看到我的另一个 question 与此相关

【问题讨论】:

  • 为什么要创建自己的序列表?推荐使用序列对象。在此处查找更多信息:docs.microsoft.com/en-us/sql/t-sql/statements/…
  • IDENTITY(1,1) 不为你做增量,所以你不需要插入它?求朋友。
  • 我已经更新了问题相关的序列对象。
  • 您绝对可以而且应该在这里使用序列。我不假装知道您的目标是什么(我也不理解您的反对意见),但您实际上是在重新创建序列的损坏版本。如果您(几乎)可以使用您编写的代码来做到这一点,那么您可以使用序列正确地做到这一点。序列只是获得唯一整数的另一种方式。就这些。您仍然会为每个客户获得一个可用的序列号,只是碰巧它也是唯一的。

标签: sql sql-server sql-server-2014


【解决方案1】:

这是一个糟糕的设计。你真的应该使用序列。真的。

这是一个糟糕的设计的原因之一是很容易出现像你这样的错误。以下是如何使该代码实际工作:

begin transaction

SELECT @MaxSeqId =  ISNULL(MAX(SequenceId),0)
FROM Alg.[Sequence] WITH (ROWLOCK, UPDLOCK, SERIALIZABLE)
WHERE CustomerId = @CustomerId

INSERT INTO Alg.[Sequence](SequenceId, CustomerId, Data)
VALUES (
@MaxSeqId + 1
,@CustomerId
,@SendingData
)

commit transaction

UPDLOCK 指示 SQL Server 对读取的行设置限制性锁定,并忽略版本存储。 SERIALIZABLE 指示 SQL 使用范围锁定,这样即使没有找到行,也会对键范围进行 U 锁,以防止并发 SELECT 发现没有行,直到第一个会话执行 INSERT 并提交事务。

稍微更好的模式是在序列生成器周围使用应用程序锁,在生成密钥之前调用sp_getapplock,然后立即调用sp_releaseapplock。这样并发会话就不必等到第一个会话的事务提交后再生成下一个键值。

【讨论】:

  • 感谢大卫,我更新了为什么我不能使用序列对象的问题,正如我在更新中指出的那样,我需要将 SequenceId 作为每个不同 CustomerId 的标识。我猜你的最后一段建议使用序列的方法,这与我的情况一致吗?
  • 是的,上面添加了额外的UPDLCOKSERIALIZABLE 的脚本正在运行,但是我希望看到一个带有序列对象的示例,它保证每个customerId 的单独sequenceId 增量没有任何违规。跨度>
  • @ibubi A sequence 不能这样使用,它只是一个序列,不能细分为多个类别(例如按客户 ID)。如果您真的需要这样的构造,我建议您创建一个计数器表(请参阅我的答案中的链接)。
  • 通常的解决方案是删除每个客户拥有自己的序列的“要求”,并在所有客户中使用单个序列。如果客户的订单编号为 1,2,4,5, 最终无关紧要。 . .或 1923,2334,2556,5585, 。 . .. 或者,您可以为每个客户提供一个序列。
【解决方案2】:

您可以尝试以下方法:

SELECT @MaxSeqId =  ISNULL(MAX(SequenceId),0)
FROM Alg.[Sequence] WITH (ROWLOCK, UPDLOCK, HOLDLOCK)
WHERE CustomerId = @CustomerId

INSERT INTO Alg.[Sequence]
VALUES (
@MaxSeqId + 1
,@CustomerId
,@SendingData
,GETDATE()
)

这将保持对序列表的锁定(范围锁定或表锁定),直到事务完成。请确保这在事务中运行。如果如您所说,这是从触发器内部运行的,那么这应该是隐式的。

请注意,以这种方式确定最大 ID 效率不高。如果您不介意编号中偶尔出现的空白,那么IDENTITY 列就足够了。如果您不希望出现间隙,计数器表可能会有所帮助(例如 here)。

【讨论】:

猜你喜欢
  • 2015-04-20
  • 2017-04-20
  • 1970-01-01
  • 2016-08-02
  • 1970-01-01
  • 1970-01-01
  • 2021-09-03
  • 2020-10-21
  • 1970-01-01
相关资源
最近更新 更多