为什么时间戳并不总是随着并发插入而增加?

Pau*_*ams 2 sql-server concurrency sql-server-2008-r2 timestamp

我看到时间戳(rowversion) 列出现了一些意外行为。我创建了一个测试表:

create table Test
(
    Test_Key int identity(1,1) primary key clustered,
    Test_Value int,
    Test_Thread int,
    ts timestamp
)

create nonclustered index IX_Test_Value on Test (Test_Value) -- probably irrelevant
Run Code Online (Sandbox Code Playgroud)

我启动了两个线程同时运行插入到这个表中。第一个线程正在运行以下代码:

declare @i int = 0
while @i < 100
begin
    insert into Test (Test_Value, Test_Thread) select n, 1 from dbo.fn_GenerateNumbers(10000)
    set @i = @i + 1
end
Run Code Online (Sandbox Code Playgroud)

第二个线程正在运行相同的代码,只是它正在select n, 2从函数中插入其线程 ID。

先说一下函数。这使用了一系列带有 ROW_NUMBER() 的交叉连接的公共表表达式来非常快速地按顺序返回大量数字。我从Itzik Ben-Gan的一篇文章中学到了这个技巧,所以要归功于他。我认为函数的实现并不重要,但无论如何我都会包含它:

CREATE FUNCTION dbo.fn_GenerateNumbers(@count int)
RETURNS TABLE WITH SCHEMABINDING
AS
RETURN
    WITH
        Nbrs_4( n ) AS ( SELECT 1 UNION SELECT 0 ),
        Nbrs_3( n ) AS ( SELECT 1 FROM Nbrs_4 n1 CROSS JOIN Nbrs_4 n2 ),
        Nbrs_2( n ) AS ( SELECT 1 FROM Nbrs_3 n1 CROSS JOIN Nbrs_3 n2 ),
        Nbrs_1( n ) AS ( SELECT 1 FROM Nbrs_2 n1 CROSS JOIN Nbrs_2 n2 ),
        Nbrs_0( n ) AS ( SELECT 1 FROM Nbrs_1 n1 CROSS JOIN Nbrs_1 n2 ),
        Nbrs  ( n ) AS ( SELECT 1 FROM Nbrs_0 n1 CROSS JOIN Nbrs_0 n2 )

    SELECT n
    FROM ( SELECT ROW_NUMBER() OVER (ORDER BY n) FROM Nbrs ) D ( n )
    WHERE n <= @count ; 
Run Code Online (Sandbox Code Playgroud)

这张桌子上有一identity列。我希望当我通过这个单调递增的主键从表中选择值时,我也会看到相同顺序的时间戳。时间戳可能不是连续的,因为可能还有其他更新,但它们至少是有序的。

然而,我所看到的却是不同的。插入按主键交错,但时间戳按线程顺序排列

Test_Key Test_Value Test_Thread ts
-------- ---------- ----------- ------------------
20227    227        1           0x000000006EDF3BC5
20228    228        1           0x000000006EDF3BC6
20229    229        1           0x000000006EDF3BC7
20230    230        1           0x000000006EDF3BC8
20231    1          2           0x000000006EDF41E9 -- thread 2 starts with a new ts
20232    2          2           0x000000006EDF41EB
20233    3          2           0x000000006EDF41EC
20234    4          2           0x000000006EDF41ED
--<snip lots of thread 2 inserts>
21538    1308       2           0x000000006EDF4710
21539    1309       2           0x000000006EDF4711
21540    1310       2           0x000000006EDF4712
21541    1311       2           0x000000006EDF4713
21542    231        1           0x000000006EDF3BC9 -- This is less than the prior row!
21543    232        1           0x000000006EDF3BCA -- Thread 1 is inserting
21544    233        1           0x000000006EDF3BCB -- from its last ts value
21545    234        1           0x000000006EDF3BCC
Run Code Online (Sandbox Code Playgroud)

我的问题是:

1)为什么时间戳并不总是随着并发插入而增加?

如果你能回答这个问题,加分:

2)为什么并发插入与主键重叠而不是一次插入? 每个插入都运行自己的隐式事务,所以我希望主键是为了单个线程的插入。我没想到主键是交错的。

我对复制的了解不够,无法回答这个问题:

3) 时间戳乱序会导致复制问题吗? 在上面的例子中,如果线程 2 先提交它的数据呢?当线程 1 完成时,它的时间戳都低于线程 2 插入的记录。

我查看了正在执行的请求并确认它们没有并行运行,所以我认为并行性不是问题。

请注意,此查询在默认 (READ COMMITTED) 隔离级别下运行。如果我将隔离级别提高到 SERIALIZABLE,当线程更改时,我仍然会以相反的顺序获得时间戳。

我正在 SQL Server 2008 R2 上对此进行测试。

为了检查时间戳订单,我做了一个select * from Test,我还使用了以下查询:

-- find timestamps out of sequential order
select t1.*, t2.*
from Test t1
    inner join Test t2
        on t2.Test_Key = t1.Test_Key + 1
where
    t2.ts <> t1.ts + 1

-- find timestamps that are less than the prior timestamp
select t1.*, t2.*
from Test t1
    inner join Test t2
        on t2.Test_Key = t1.Test_Key + 1
where
    t2.ts < t1.ts
Run Code Online (Sandbox Code Playgroud)

Seb*_*ine 8

IDENTITY 生成器没有很好的文档记录。但是,可以观察到一些似乎相关的行为:

  1. 身份生成不会受到交易的影响。这意味着一旦一个值被使用,它就不会被重用,即使导致它使用的事务被回滚。

  2. 并非每次使用都会导致更新被写回数据库的序列位置。例如,您可以在崩溃后看到这一点。崩溃后的下一个使用值通常比前一个高几个数字。

虽然没有证据(意思是文档),但可以假设出于性能原因,多行插入会获取标识值块并使用它们直到用完为止。另一个并发线程将获取下一个数字块。此时,标识值实际上不再反映插入的顺序。

另一方面,rowversion 数据类型是一个不断增加的数字,它将反映插入顺序。(时间戳是 rowversion 已弃用的同义词。)

因此,在您的情况下,您可以假设行是按照 rowversion 列的顺序插入的,并且无序标识值是由内存性能优化引起的。

顺便说一下,虽然 IDENTITY 生成器没有很好的文档记录,但新的 2012SEQUENCE功能是。在这里,您可以按顺序阅读有关上述行为的所有信息。

至于您对复制的关注:

  1. 事务复制使用数据库日志,不依赖于特定的列值。

  2. 合并复制使用 rowguid 列来标识行。这是一个被赋值一次并且在行的整个生命周期中不会改变的列。合并复制不使用 rowversion 列。事务一致性是通过在同步时使用正常锁定这一事实来强制执行的,因此事务要么对合并代理完全可见,要么完全不可见。

  3. 快照复制根本不查找更改。它只需要在同步时提交数据并将其复制过来。