7

在浏览 SO 时,我发现以下关于插入尚不存在的记录的“最佳”方法的问题/讨论。令我印象深刻的陈述之一是 [Remus Rusanu] 说:

两种变体都不正确。您将插入重复的@value1、@value2 对,保证。

尽管我确实同意检查与插入“分离”的语法(并且不存在显式锁定/事务管理);我很难理解为什么以及何时对于看起来像这样的其他建议语法是正确的

INSERT INTO mytable (x)
SELECT @x WHERE NOT EXISTS (SELECT * FROM mytable WHERE x = @x);

我不想开始(另一个)最好/最快的讨论,我也不认为语法可以“替换”唯一索引/约束(或 PK),但我真的需要知道这种结构在什么情况下会导致双打'过去一直在使用这种语法,并且想知道将来继续这样做是否不安全。

我认为发生的情况是 INSERT 和 SELECT 都在同一个(隐式)事务中。查询将对相关记录(键)进行 IX 锁定,并且在整个查询完成之前不会释放它,因此只有在插入记录之后。这个锁会阻止所有其他连接进行相同的 INSERT,因为在我们的插入完成之前它们自己无法获得锁;只有这样他们才能获得锁,并开始自己验证记录是否已经存在。

恕我直言,找出答案的最佳方法是通过测试,我在笔记本电脑上运行了以下代码一段时间:

创建表

CREATE TABLE t_test (x int NOT NULL PRIMARY KEY (x))

在许多并行连接上运行以下)

SET NOCOUNT ON

WHILE 1 = 1
    BEGIN
        INSERT t_test (x)
        SELECT x = DatePart(ms, CURRENT_TIMESTAMP)
         WHERE NOT EXISTS ( SELECT *
                              FROM t_test old
                             WHERE old.x = DatePart(ms, CURRENT_TIMESTAMP) )
    END

到目前为止,唯一需要注意的是:

  • 没有遇到错误(还)
  • CPU 运行非常热 =)
  • 表快速保存了 300 条记录(由于日期时间的 3 毫秒“精度”),之后不再发生实际插入,正如预期的那样。

更新:

原来我上面的例子没有做我打算做的事情。而不是多个连接尝试同时插入相同的记录,我只是让它在第一秒后不插入已经存在的记录。由于复制粘贴和在下一个连接上执行查询可能需要大约一秒钟,所以从来没有重复的危险。这天剩下的时间我都戴着我的驴耳朵……

无论如何,我已经调整了测试以更符合手头的问题(使用同一张表)

SET NOCOUNT ON

DECLARE @midnight datetime
SELECT @midnight = Convert(datetime, Convert(varchar, CURRENT_TIMESTAMP, 106), 106)

WHILE 1 = 1
    BEGIN
        INSERT t_test (x)
        SELECT x = DateDiff(ms, @midnight, CURRENT_TIMESTAMP)
         WHERE NOT EXISTS ( SELECT *
                              FROM t_test old
                             WHERE old.x = DateDiff(ms, @midnight, CURRENT_TIMESTAMP))
    END

瞧,输出窗口现在包含大量错误

消息 2627,级别 14,状态 1,第 8 行违反主键约束“PK__t_test__3BD019E521C3B7EE”。无法在对象“dbo.t_test”中插入 > 重复键。重复键值为 (57581873)。

仅供参考:正如 Andomar 所指出的,添加 HOLDLOCK 和/或 SERIALIZABLE 提示确实“解决”了问题,但结果却导致了很多死锁......当我想通的时候,这不是很好,但也不是出乎意料。

猜猜我有很多代码审查要做......


重置 auto_increment 是否会重新排序 id?

英语不是我的母语,所以如果我的问题不清楚,请原谅我,我会尽力澄清。

我必须在 MySQL 中进行更改,我对结果有点担心,因为更改将生效。我有一张表,其 id 从 1 到 800,然后有一个很大的跳跃到 10000、10001 等。我读了多个类似这样的问题,你可以使用

ALTER TABLE tablename AUTO_INCREMENT = 1

我的问题是:

  1. 这会重新排序当前的键吗?
  2. 当我的 id 恢复到 10000 时会发生什么,id 会被覆盖吗?(该列是主要的)

如果它确实重新排序了我的键或它们被覆盖(我不能发生这种情况),那么保持我的表完整但将 id 重置为 801 的最佳解决方案是什么?我绝对需要保持结果不变!!!

感谢您花时间阅读/回答。干杯。

4

2 回答 2

4

感谢您发布单独的问题。你有几个误解:

查询将对相关记录(键)进行 IX 锁定,并且在整个查询完成之前不会释放它

INSERT 将锁定插入的行,X 锁定(像 IX 这样的意图锁定只能在锁定层次结构上的父实体上请求,而不能在记录上请求)。这个锁必须一直保持到事务提交(严格的两阶段锁要求 X 锁总是只在事务结束时才被释放)。

请注意,INSERT 获得的锁不会阻止更多的插入,即使是同一个键。防止重复的唯一方法是唯一索引,并且强制唯一性的机制不是基于锁的。是的,在主键上,由于它的唯一性,可以防止重复,但作用是不同的,即使锁定确实起作用。

在您的示例中,由于新插入行上的 X 与 S 锁冲突,操作将序列化,因为 INSERT 上的 SELECT 块。另一个需要考虑的想法是 300 条 INT 类型的记录可以放在一个页面上,并且会进行很多优化(例如,使用扫描而不是多次搜索)并且会改变测试结果。请记住,一个有很多正面但没有证据的假设仍然只是一个猜想......

要测试问题,您需要确保 INSERT 不会阻塞并发 SELECT。在 RCSI 或快照隔离下运行是实现这一目标的一种方法(并且可能在生产中不由自主地“实现”它并破坏做出上述所有假设的应用程序......) WHERE 子句是另一种方式。一个非常大的表和二级索引是另一种方式。

所以这是我测试它的方法:

set nocount on;
go

drop database test;
go

create database test;
go

use test;
go

create table test (id int primary key, filler char(200));
go

-- seed 10000 values, fill some pages
declare @i int = 0;
begin transaction
while @i < 10000
begin
    insert into test (id) values (@i);
    set @i += 1;
end
commit;

现在从几个并行连接运行它(我使用了 3 个):

use test;
go

set nocount on;
go

declare @i int;
while (1=1)
begin
    -- This is not cheating. This ensures that many concurrent SELECT attempt 
    -- to insert the same values, and all of them believe the values are 'free'
    select @i = max(id) from test with (readpast);
    insert into test (id)
    select id
        from (values (@i), (@i+1), (@i+2), (@i+3), (@i+4), (@i+5)) as t(id)
        where t.id not in (select id from test);
end

以下是一些结果:

Msg 2627, Level 14, State 1, Line 6
Violation of PRIMARY KEY constraint 'PK__test__3213E83FD9281543'. Cannot insert duplicate key in object 'dbo.test'. The duplicate key value is (130076).
The statement has been terminated.
Msg 2627, Level 14, State 1, Line 6
Violation of PRIMARY KEY constraint 'PK__test__3213E83FD9281543'. Cannot insert duplicate key in object 'dbo.test'. The duplicate key value is (130096).
The statement has been terminated.
Msg 2627, Level 14, State 1, Line 6
Violation of PRIMARY KEY constraint 'PK__test__3213E83FD9281543'. Cannot insert duplicate key in object 'dbo.test'. The duplicate key value is (130106).
The statement has been terminated.
Msg 2627, Level 14, State 1, Line 6
Violation of PRIMARY KEY constraint 'PK__test__3213E83FD9281543'. Cannot insert duplicate key in object 'dbo.test'. The duplicate key value is (130121).
The statement has been terminated.
Msg 2627, Level 14, State 1, Line 6
Violation of PRIMARY KEY constraint 'PK__test__3213E83FD9281543'. Cannot insert duplicate key in object 'dbo.test'. The duplicate key value is (130141).
The statement has been terminated.
Msg 2627, Level 14, State 1, Line 6
Violation of PRIMARY KEY constraint 'PK__test__3213E83FD9281543'. Cannot insert duplicate key in object 'dbo.test'. The duplicate key value is (130151).
The statement has been terminated.
Msg 2627, Level 14, State 1, Line 6
Violation of PRIMARY KEY constraint 'PK__test__3213E83FD9281543'. Cannot insert duplicate key in object 'dbo.test'. The duplicate key value is (130176).
The statement has been terminated.
Msg 2627, Level 14, State 1, Line 6

即使有锁定、没有快照隔离、没有 RCSI。当每个 SELECT 尝试插入 @i+1...@i+5 时,他们都会发现这些值不存在,然后他们都会继续进行 INSERT。一名幸运的获胜者将成功,其余的将导致PK违例。频繁地。我@i=MAX(id)故意使用 来显着增加冲突的追逐,但这不是必需的。我将把弄清楚为什么所有违规都发生在值 %5+1 上的问题留作练习。

于 2013-09-30T14:05:37.163 回答
3

您正在从单个连接进行测试,因此您根本没有测试并发性。从不同的窗口运行脚本两次,您将开始看到冲突。

产生冲突的原因有很多:

  • 默认情况下,直到(隐式)事务结束时才会持有锁。使用with (holdlock)查询提示来更改此行为。
  • 您的查询的并发问题称为“幻读”。默认事务隔离级别是“已提交读”,它不能防止幻读。使用with (serializable)查询提示来提高隔离级别。(尽量避免该set transaction isolation level命令,因为连接返回连接池时不会清除隔离级别。)

始终强制执行主键约束。因此,您的查询将尝试插入重复行并通过抛出重复键错误而失败。

一个好的方法是使用您的查询(这将在 99% 的时间内有效)并让客户端以优雅的方式处理偶尔出现的重复键异常。

维基百科对隔离级别有很好的解释。

于 2013-09-30T13:45:38.410 回答