Ole*_*Dok 7 sql-server-2005 sql-server
我有一个综合测试,它重现了我们在生产环境中的一些错误。以下是重现它的 2 个脚本:
第一
DBCC TRACEOFF(-1,3604,1200) WITH NO_INFOMSGS;
SET NOCOUNT ON;
IF @@TRANCOUNT > 0 ROLLBACK
IF object_id('test') IS NOT NULL
DROP TABLE test
IF object_id('TMP_test') IS NOT NULL
DROP TABLE TMP_test
IF object_id('test1') IS NOT NULL
DROP TABLE test1
CREATE TABLE test(Id INT IDENTITY PRIMARY KEY)
GO
INSERT test DEFAULT VALUES
GO 2000
WHILE 1 = 1
BEGIN
CREATE TABLE TMP_test(Id INT PRIMARY KEY)
INSERT TMP_test SELECT * FROM test
WAITFOR DELAY '0:00:00.1'
BEGIN TRAN
EXEC sp_rename 'test', 'test1'
EXEC sp_rename 'TMP_test', 'test'
EXEC sp_rename 'test1', 'TMP_test'
DROP TABLE TMP_test
commit
END
Run Code Online (Sandbox Code Playgroud)
第二
SET NOCOUNT ON;
DECLARE @c INT
WHILE 1 = 1
BEGIN
SELECT @c = COUNT(*) FROM Test IF @@ERROR <> 0 BREAK
SELECT @c = COUNT(*) FROM Test IF @@ERROR <> 0 BREAK
/* and repeat this 10-20 times more*/
SELECT @c = COUNT(*) FROM Test IF @@ERROR <> 0 BREAK
END
Run Code Online (Sandbox Code Playgroud)
所以,问题是,当我在一个会话中运行第一个脚本并让它继续运行,然后在单独的会话中运行第二个脚本时,我会得到这种类型的错误:
消息 208,级别 16,状态 1,第 13 行 无效的对象名称“测试”。
问题是 -为什么我会COMMIT
在第一个脚本的循环结束时看到此错误,而如果有,则永远不会得到一个ROLLBACK
?
我有一种感觉,它以某种方式与情况有关,当脚本提交时,仍然存在带有名称的表,test
但它是一个不同的对象,第二个脚本必须重新编译自己。这是好的。但是为什么会出现漏表错误呢?AFAIK - 当我在事务中重命名表时 - 它会将 Sch-M 锁定到 tran 端?
有人可以回答或指导我阅读我可以深入阅读并理解原因的技术论文吗?
非常有趣和棘手的问题。我找不到任何关于这种行为的官方文档,我怀疑可能没有(尽管如果有人纠正我,我会喜欢它!)。
我的研究使我相信,正是计划编译步骤容易受到这种竞争条件的影响。请注意,我能够在没有错误的情况下运行您的测试查询一个小时,但是如果我反复启动您的流程,我有时会立即收到错误消息。当它确实遇到错误时,它总是在计划编译时立即这样做。或者,您可以将“OPTION RECOMPILE”添加到循环中的 COUNT(*),强制在每次试验时编译新计划。使用这种方法,每次运行您的脚本时,我几乎都会立即看到错误。
我能够通过一系列受控步骤重现错误,这些步骤似乎在每次试验中都会遇到错误,无需设置循环并依赖随机性。
我还提出了一个潜在的修复程序(使用 ALTER TABLE...SWITCH),这可能会在您的生产环境中试用。现在,进入细节!
以下是重现的步骤:
-- Instructions: Process each step one at a time, following any instructions in that step
-- (1) Initial setup: Clean any relics and create the test table
DBCC TRACEOFF(-1,3604,1200) WITH NO_INFOMSGS;
SET NOCOUNT ON;
IF @@TRANCOUNT > 0 ROLLBACK
IF object_id('test') IS NOT NULL
DROP TABLE test
IF object_id('TMP_test') IS NOT NULL
DROP TABLE TMP_test
IF object_id('test1') IS NOT NULL
DROP TABLE test1
CREATE TABLE test(Id INT IDENTITY CONSTRAINT PK_test PRIMARY KEY)
GO
INSERT test DEFAULT VALUES
GO 2000
-- (2) Run the COUNT(*)
-- This will acquire the Sch-S lock, and we use HOLDLOCK to retain that lock.
-- This simulates a race condition where this query is running at the time the first rename occurs.
BEGIN TRANSACTION
SELECT COUNT(*) FROM Test WITH (HOLDLOCK)
GO
-- (3) In another window, run the block of code that performs the renames
-- This will be blocked, waiting for a Sch-M lock on "test" until we complete step 4 below
CREATE TABLE TMP_test(Id INT PRIMARY KEY)
INSERT TMP_test SELECT * FROM test
EXEC sp_rename 'test', 'test1'
EXEC sp_rename 'TMP_test', 'test'
EXEC sp_rename 'test1', 'TMP_test'
DROP TABLE TMP_test
GO
-- (4) Now, commit the original COUNT(*) and immediately fire off the query again
-- We use OPTION (RECOMPILE) to make sure we need to compile a new query plan
-- The COMMIT releases the Sch-S lock, allowing (3) to acquire the Sch-M lock
-- This batch will now be waiting for the Sch-S lock again, on the same object_id,
-- but that object_id will no longer point to the correct object by the time the lock
-- is acquired.
COMMIT
SELECT COUNT(*) FROM Test OPTION (RECOMPILE)
GO
Run Code Online (Sandbox Code Playgroud)
使用这些步骤,我们可以深入了解导致错误的原因。为此,我对以下事件进行了跟踪:SP:StmtStarting、SP:StmtCompleted、Lock:Acquired、Lock:Released。
我发现按顺序发生以下情况(但省略了中间的一些细节):
但是,如果在不需要计划编译时(例如在运行循环时)发生类似的事件序列,SQL Server 实际上足够智能,可以检测到 object_id 已更改。我也跟踪了那个案例,我发现了第二个 COUNT(*) 执行以下序列的情况
因此,正如您所假设的那样,这种自适应逻辑看起来确实到位。但看起来它可能只适用于查询执行,而不适用于查询编译。
正如所承诺的,这里是第 (3) 节的一个替代片段,它似乎提供了一种重命名表的方法来解决并发问题(至少在我的机器上!):
-- (3) In another window, run the block of code that performs the "renames"
-- Instead of sp_rename, we use ALTER TABLE...SWITCH
-- This appears to be a more robust way of performing the same logic
CREATE TABLE test1(Id INT PRIMARY KEY)
CREATE TABLE TMP_test(Id INT PRIMARY KEY)
INSERT TMP_test SELECT * FROM test
ALTER TABLE test SWITCH TO test1
ALTER TABLE TMP_test SWITCH TO test
ALTER TABLE test1 SWITCH TO TMP_test
DROP TABLE TMP_test
DROP TABLE test1
GO
Run Code Online (Sandbox Code Playgroud)
最后,这是我发现的另一个链接,它可以帮助我思考如何在名称正在更改的表的上下文中考虑锁定:
归档时间: |
|
查看次数: |
226 次 |
最近记录: |