【问题标题】:Write buffering in SQL ServerSQL Server 中的写入缓冲
【发布时间】:2017-12-20 18:23:25
【问题描述】:

我有一个在 SQL Server 2008 上运行的程序。有一个创建新货件的功能。创建货件所需的最少数据是 idno、subno 和 class。主键是 idno + idsub。我创建了一个存储过程来创建一个新的货件,如下所示

ALTER Proc [dbo].[spDP_CreateNewShipment](@Class char(10),@idno int = 0, @idsub smallint = 1)
as

If @idno = 0
Begin
Select @idno = Max(idno)+1 From Shipment
End 
Insert Into Shipment
(idno,idsub, class)
Values (@idno,@idsub,@class)

如果我将@idno 传递给0,它应该使用下一个可用的idno 创建一个货件。这很好用,只是有时两批新货物会得到相同的新 idno。似乎时间必须完全相同,以至于这几乎是不可能的,但它确实发生了。我能想到的唯一另一种可能性是插入可能会被缓冲并且可能不会立即发生。我对 SQL Server 设置知之甚少。有谁知道可能导致写入不会立即发生的设置?

【问题讨论】:

  • 这是 identity 列的用途,您应该使用一个。正如您所观察到的,如果进程 1 在进程 2 读取 Max(idno) 但尚未插入之后读取 Max(idno),您将得到重复。
  • 不要重新发明轮子。使用 IDENTITY 列。它将更简单、更快、更可靠、更容易维护、更难意外损坏等等……
  • @MatBailie Identity 不起作用,因为 idno 不是唯一的。主键是 idno + idsub。
  • 如果您对您的结构进行更多解释,我可以保证 IDENTITY 列将起作用。您可能还需要一张表,但这本身就表明当前的设计存在缺陷(将尺寸和事实混为一谈就是一个可能的例子)。您完全尝试这样做确实是一种严重的代码气味。它应该被视为一个警告,您可能正在尝试解决另一个问题的症状,一个真正应该首先解决的问题。
  • 你的意思是12345,112345,2是同一批货,但是在一个流程中的位置不同?这应该意味着您有“真实的”shipment 表,每批货物只有一行idno 作为标识列)。然后,shipment_sub 表的状态会随时间发生变化。在您的情况下,您也许可以改用shipment_mastershipment。然后您的程序插入到shipment_master 以获取新的idno? (如果您使用示例数据更新您的问题,那么我可以在答案中给出一个具体示例,考虑到对您现有系统的最小更改。)

标签: sql sql-server tsql


【解决方案1】:

简短的回答是:在这种情况下您不需要它,请改用 identity 主键。

由于race condition,您遇到了这个问题。在您生成@idno 之后,另一个线程也会生成相同的@idno。然后两个线程都试图插入具有相同@idno 的行,因此第二个插入违反了primary key 唯一性规则。

Identity 主键允许您只插入非 Id-data-columns 并将 next-id-generation 留给数据库。然后你可以问它产生了什么Id。

对于insert 代码示例,请查看SO answer。类似的东西:

if @idno = 0 begin
    Insert Into Shipment(idsub, class)
    Output inserted.idno   -- it outputs to the client idno of the inserted row
    Values (@idno,@idsub,@class)
end
else begin
    -- @idno != 0 case
    -- are you SURE you wanna insert with @idno generated on client?

    -- if you HAVE TO, then you probably should use 
    -- GUID (aka uniqueidentifier) for idno column
    -- instead of identity int
end

有关identity 列的更多信息,请查看msdn。简而言之,您声明此列的下一个值是基于一些初始值seed 和建议的step 生成的。最常见的(seed, step)(1, 1),但您可以随意设置。

【讨论】:

  • 我不能使用标识列,因为 idno 不是唯一的。该函数的工作方式是,如果您传递 idno = 0,它应该创建一个新的 idno。如果你传递它 idno 0,那么它正在创建一个具有相同 idno 但不同 idsub 作为参数传递的子货件。
  • “使用事务”的答案显然会起作用——但它肯定有一个缺陷。另一种方法是将 idno 分离到另一个表中,其中 idno 是标识列,其中包含主表中的外键。在这种情况下,当 idno = 0 时,您将新行插入到 idno 表并获取其 id,然后像“idno != 0”一样进一步使用它。
  • 哦,刚刚看到 MatBailie 的评论也有同样的建议 :)
  • 感谢您的回复。将整个事情放在交易中的不利之处是什么?
【解决方案2】:

将您的语句包装成transaction 并设置最高隔离级别。像这样。

ALTER Proc [dbo].[spDP_CreateNewShipment](@Class char(10),@idno int = 0, @idsub smallint = 1)
as
SET TRANSACTION ISOLATION LEVEL SERIALIZABLE;
BEGIN TRANSACTION
If @idno = 0
Begin
Select @idno = Max(idno)+1 From Shipment
End 
Insert Into Shipment
(idno,idsub, class)
Values (@idno,@idsub,@class)
COMMIT TRANSACTION

【讨论】:

  • 我按照您的建议将其包装在事务中。如果问题消失,我会将其标记为答案。
【解决方案3】:

在事务中执行操作可能会更好;

begin transaction
    If @idno = 0
    Begin
        Select @idno = Max(idno)+1 From Shipment
    End 
    Insert Into Shipment (idno,idsub, class) Values (@idno,@idsub,@class)
commit transaction

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2011-10-30
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多