使用自增键插入的多行的序号

Sequential numbers for many rows inserted with auto-increment key

我想知道如何(如果可能的话)以一种我绝对确定一个线程插入的行的键将获得序号的方式插入行。

例如,如果同时执行 2 个线程:

我一定要确保不要得到:

显然,如果每个线程执行某种循环并执行 5 次 "INSERT INTO..." 命令,它将无法工作,因为另一个线程可以插入。

但是即使一个线程只使用一个 INSERT 命令插入许多行,是否足以保证键是顺序的?

如果是,你能帮我找到它的记录在哪里吗?因为我没有。

如果不是,如何确保?


编辑为什么我关心获得连续的数字:

这主要是一个性能问题,我们实际上有一个整数不是自动增加一行中的列,每个线程锁定该行,手动将列值增加它需要的任何数字,然后释放该行。

问题是一次只能插入一个线程,通过测试我们发现在新的 table 自动增量列中插入行,让 sql 服务器管理身份分配,要快得多。 锁定整个 table 不是一个选项,因为它会导致与锁定公共行相同的问题。

我之所以要确保插入的行 ID 对于单个线程插入是连续的,是为了减少代码重构的需要,代码重构实际上是通过只保留第一个数字和计数来工作的,因此代码可以推断出什么是其他数字。

连续的数字不是业务问题,所以如果不可能做到,我们只需要将每个行号保存在一个数组中,但有更多代码可以通过这种方式重构,所以我'我尽量避免它。

请记住,我完全知道设计可能并不理想,但我使用的是遗留 "big ball of mud" 系统,我无法对其进行太多重新设计。

使用交易。 该事务将锁定 table 直到您提交,因此在前一个事务结束之前不会启动其他事务,因此标识值是安全的

可以对整个table设置独占锁如下。

请注意 TABLOCKX 中的 X - 这使得锁定成为独占。

BEGIN TRANSACTION;

-- select one dummy row, and set an exclusive lock 
-- on the whole table (TABLOCKX)

SELECT TOP 1 * 
FROM yourtab
WITH (TABLOCKX, HOLDLOCK);

-- Your INSERT statements goes here:
INSERT INTO yourtab(....) ... 

COMMIT TRANSACTION;

哪个版本的 SQL 服务器?从 SQL Server 2012 开始,您可以创建所谓的 序列。 这些是为调用者创建唯一编号的对象。 保证序列是线程安全的。

在你的例子中,你需要一个递增 5 的序列:

CREATE SEQUENCE myseq 
AS int
START WITH 1
INCREMENT BY 5

在您的线程中,您可以使用 NEXT VALUE 检索下一个值:

DECLARE @var int ;
SET @var = NEXT VALUE FOR myseq ;

然后您可以使用此值并将值插入您的 table。有了这个,你就可以不用锁定你的 tables.

我很好奇,想测试一下。在装有 SQL Server 2014 Express 的虚拟机上,答案是:


生成的IDENTITY保证在多个线程插入值时是连续的。即使它是一次插入多行的单个 INSERT 语句。 (在默认事务隔离级别下)


您可以在 SQL Server 2008 上对其进行测试,但即使您没有看到相同的行为,依赖它也是不明智的,因为它在 2014 年肯定发生了变化。

这是重现测试的完整脚本。

Table

CREATE TABLE [dbo].[test](
    [ID] [int] IDENTITY(1,1) NOT NULL,
    [dt] [datetime2](7) NOT NULL,
    [V] [int] NOT NULL,
 CONSTRAINT [PK_test] PRIMARY KEY CLUSTERED 
(
    [ID] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]
) ON [PRIMARY]

GO

插入脚本

WAITFOR TIME '22:23:24';
-- set the time to about a minute in the future
-- open two windows in SSMS and run this script (F5) in both of them
-- they will start running at the same time specified above in parallel.

-- insert 1M rows in chunks of 1000 rows

-- in the first SSMS window uncomment these lines:
--DECLARE @VarV int = 0;
--WHILE (@VarV < 1000)

-- in the second SSMS window uncomment these lines:
--DECLARE @VarV int = 10000;
--WHILE (@VarV < 11000)

BEGIN

    WITH e1(n) AS
    (
        SELECT 1 UNION ALL SELECT 1 UNION ALL SELECT 1 UNION ALL 
        SELECT 1 UNION ALL SELECT 1 UNION ALL SELECT 1 UNION ALL 
        SELECT 1 UNION ALL SELECT 1 UNION ALL SELECT 1 UNION ALL SELECT 1
    ) -- 10
    ,e2(n) AS (SELECT 1 FROM e1 CROSS JOIN e1 AS b)    -- 10*10
    ,e3(n) AS (SELECT 1 FROM e1 CROSS JOIN e2)         -- 10*100
    ,CTE_rn
    AS
    (
        SELECT ROW_NUMBER() OVER (ORDER BY n) AS rn
        FROM e3
    )
    INSERT INTO [dbo].[test]
        ([dt]
        ,[V])
    SELECT
        SYSDATETIME() AS dt
        ,@VarV
    FROM CTE_rn;

    SET @VarV = @VarV + 1;

END;

验证结果

WITH
CTE
AS
(
    SELECT 
        [V]
        ,MIN(ID) AS MinID
        ,MAX(ID) AS MaxID
        ,MAX(ID) - MIN(ID) + 1 AS DiffID
    FROM [dbo].[test]
    GROUP BY V
)
SELECT
    DiffID
    ,COUNT(*) AS c
FROM CTE
GROUP BY DiffID
ORDER BY c DESC;

此查询为每个 V(每块 1000 个插入行)计算 MINMAX ID。如果所有 IDENTITY 值都是按顺序生成的,那么 MAXMIN ID 之间的差值始终正好是 1000。正如我们在结果中看到的那样,情况并非如此:

结果

+--------+------+
| DiffID |  c   |
+--------+------+
|   1000 | 1940 |
|   2000 |    6 |
|   3000 |    3 |
|   1759 |    2 |
|   1477 |    2 |
|   1522 |    1 |
|   1524 |    1 |
|   1529 |    1 |
|   1538 |    1 |
|   1546 |    1 |
|   1548 |    1 |
|   1584 |    1 |
|   1585 |    1 |
|   1589 |    1 |
|   1597 |    1 |
|   1606 |    1 |
|   1611 |    1 |
|   1612 |    1 |
|   1620 |    1 |
|   1630 |    1 |
|   1631 |    1 |
|   1635 |    1 |
|   1658 |    1 |
|   1663 |    1 |
|   1675 |    1 |
|   1731 |    1 |
|   1749 |    1 |
|   1009 |    1 |
|   1038 |    1 |
|   1049 |    1 |
|   1055 |    1 |
|   1086 |    1 |
|   1102 |    1 |
|   1144 |    1 |
|   1218 |    1 |
|   1225 |    1 |
|   1263 |    1 |
|   1325 |    1 |
|   1367 |    1 |
|   1372 |    1 |
|   1415 |    1 |
|   1451 |    1 |
|   1761 |    1 |
|   1793 |    1 |
|   1832 |    1 |
|   1904 |    1 |
|   1919 |    1 |
|   1924 |    1 |
|   1954 |    1 |
|   1973 |    1 |
|   1984 |    1 |
|   2381 |    1 |
+--------+------+

在大多数情况下,的确,IDENTITY 个值是按顺序分配的,但在 2000 年中的 60 个情况下,它们不是。


如何处理?

我个人更喜欢使用 sp_getapplock,而不是锁定 table 或增加事务隔离级别。

但是,最终结果是相同的 - 您必须确保 INSERT 语句不是 运行 并行。


在 SQL Server 2012+ 中,值得测试新 SEQUENCE 功能的行为。具体来说,sp_sequence_get_range 存储过程从序列对象生成一系列序列值。让我们把这个练习留给 reader.