Vla*_*nov 17 sql-server-2008 sql-server
我一直在阅读 MSDN 关于TRY...CATCH和XACT_STATE。
它有以下示例,用于XACT_STATE在构造CATCH块中TRY…CATCH确定是提交还是回滚事务:
USE AdventureWorks2012;
GO
-- SET XACT_ABORT ON will render the transaction uncommittable
-- when the constraint violation occurs.
SET XACT_ABORT ON;
BEGIN TRY
BEGIN TRANSACTION;
-- A FOREIGN KEY constraint exists on this table. This
-- statement will generate a constraint violation error.
DELETE FROM Production.Product
WHERE ProductID = 980;
-- If the delete operation succeeds, commit the transaction. The CATCH
-- block will not execute.
COMMIT TRANSACTION;
END TRY
BEGIN CATCH
-- Test XACT_STATE for 0, 1, or -1.
-- If 1, the transaction is committable.
-- If -1, the transaction is uncommittable and should
-- be rolled back.
-- XACT_STATE = 0 means there is no transaction and
-- a commit or rollback operation would generate an error.
-- Test whether the transaction is uncommittable.
IF (XACT_STATE()) = -1
BEGIN
PRINT 'The transaction is in an uncommittable state.' +
' Rolling back transaction.'
ROLLBACK TRANSACTION;
END;
-- Test whether the transaction is active and valid.
IF (XACT_STATE()) = 1
BEGIN
PRINT 'The transaction is committable.' +
' Committing transaction.'
COMMIT TRANSACTION;
END;
END CATCH;
GO
Run Code Online (Sandbox Code Playgroud)
我不明白的是,我为什么要关心并检查XACT_STATE返回的内容?
请注意,该标志在示例中XACT_ABORT设置为ON。
如果TRY块内有足够严重的错误,则控制将传递到CATCH. 所以,如果我在里面CATCH,我知道事务有问题,在这种情况下唯一明智的做法就是回滚它,不是吗?
但是,来自 MSDN 的这个示例暗示,在某些情况下,控制权被传递到CATCH并且提交事务仍然有意义。有人可以提供一些实际的例子,什么时候可以发生,什么时候有意义?
我没有看到在什么情况下可以将控制传递到内部CATCH的事务中,该事务可以在XACT_ABORT设置为时提交ON。
MSDN 文章中SET XACT_ABORT有一个示例,当事务中的某些语句成功执行而某些语句在XACT_ABORT设置为时失败OFF,我理解这一点。但是,SET XACT_ABORT ON怎么会XACT_STATE()在CATCH块内返回 1呢?
最初,我会像这样编写这段代码:
USE AdventureWorks2012;
GO
-- SET XACT_ABORT ON will render the transaction uncommittable
-- when the constraint violation occurs.
SET XACT_ABORT ON;
BEGIN TRY
BEGIN TRANSACTION;
-- A FOREIGN KEY constraint exists on this table. This
-- statement will generate a constraint violation error.
DELETE FROM Production.Product
WHERE ProductID = 980;
-- If the delete operation succeeds, commit the transaction. The CATCH
-- block will not execute.
COMMIT TRANSACTION;
END TRY
BEGIN CATCH
-- Some severe problem with the transaction
PRINT 'Rolling back transaction.';
ROLLBACK TRANSACTION;
END CATCH;
GO
Run Code Online (Sandbox Code Playgroud)
考虑到 Max Vernon 的回答,我会写这样的代码。他表明在尝试之前检查是否存在活动事务是有意义的ROLLBACK。不过,随着SET XACT_ABORT ON该CATCH块可以有不惜一切注定交易或没有交易。所以,无论如何都没有什么可做的COMMIT。我错了吗?
USE AdventureWorks2012;
GO
-- SET XACT_ABORT ON will render the transaction uncommittable
-- when the constraint violation occurs.
SET XACT_ABORT ON;
BEGIN TRY
BEGIN TRANSACTION;
-- A FOREIGN KEY constraint exists on this table. This
-- statement will generate a constraint violation error.
DELETE FROM Production.Product
WHERE ProductID = 980;
-- If the delete operation succeeds, commit the transaction. The CATCH
-- block will not execute.
COMMIT TRANSACTION;
END TRY
BEGIN CATCH
-- Some severe problem with the transaction
IF (XACT_STATE()) <> 0
BEGIN
-- There is still an active transaction that should be rolled back
PRINT 'Rolling back transaction.';
ROLLBACK TRANSACTION;
END;
END CATCH;
GO
Run Code Online (Sandbox Code Playgroud)
事实证明,如果设置为,则无法从CATCH块内部提交事务。XACT_ABORTON
来自 MSDN 的示例有些误导,因为检查暗示XACT_STATE在某些情况下可以返回 1 并且可能对COMMIT事务进行处理。
IF (XACT_STATE()) = 1
BEGIN
PRINT 'The transaction is committable.' +
' Committing transaction.'
COMMIT TRANSACTION;
END;
Run Code Online (Sandbox Code Playgroud)
这不是真的,如果设置为,XACT_STATE则永远不会在CATCH块内返回 1 。XACT_ABORTON
似乎 MSDN 示例代码主要是为了说明XACT_STATE()函数的使用,而不管XACT_ABORT设置如何。示例代码看起来足够通用,可以同时使用XACT_ABORTset toON和OFF。只是XACT_ABORT = ON检查IF (XACT_STATE()) = 1变得不必要了。
Erland Sommarskog有一组非常详细的关于SQL Server 中的错误和事务处理的文章。在第2部分-错误的分类,他提出了一个全面的表放在一起错误的所有类以及它们是如何通过SQL服务器进行处理,以及如何TRY ... CATCH和XACT_ABORT改变行为。
IF (XACT_STATE()) = 1
BEGIN
PRINT 'The transaction is committable.' +
' Committing transaction.'
COMMIT TRANSACTION;
END;
Run Code Online (Sandbox Code Playgroud)
表中的最后一列回答了问题。同TRY-CATCH和同XACT_ABORT ON交易在所有可能的情况下都是注定的。
问题范围之外的一个注释。正如 Erland 所说,这种一致性是设置XACT_ABORT为的原因之一ON:
我已经建议您的存储过程应该包含命令
SET XACT_ABORT, NOCOUNT ON。如果您查看上表,您会发现XACT_ABORT实际上存在更高级别的一致性。例如,交易总是注定要失败。在下文中,我将展示许多我设置XACT_ABORT为 的示例OFF,以便您了解为什么应该避免使用此默认设置。
TL;DR/执行摘要:关于问题的这一部分:
我没有看到在什么情况下可以将控制传递到内部
CATCH的事务中,该事务可以在XACT_ABORT设置为时提交ON。
我现在已经对此进行了大量测试,但我找不到任何在块内XACT_STATE()返回when和is的 session 属性的情况。事实上,根据SET XACT_ABORT的当前 MSDN 页面:1CATCH@@TRANCOUNT > 0 XACT_ABORTON
当 SET XACT_ABORT 为 ON 时,如果 Transact-SQL 语句引发运行时错误,则整个事务将终止并回滚。
该声明似乎与您的推测和我的发现一致。
MSDN 文章中
SET XACT_ABORT有一个示例,当事务中的某些语句执行成功而某些语句XACT_ABORT设置为时失败OFF
没错,但在该例子语句是不是一个内TRY块。TRY块中的那些相同语句仍然会阻止执行导致错误的语句之后的任何语句,但假设XACT_ABORT是这样OFF,当控制权传递给CATCH块时,事务仍然在物理上有效,因为所有先前的更改都没有错误地发生并且可以提交,如果这是愿望,或者可以回滚。另一方面,如果XACT_ABORT是,ON则任何先前的更改都会自动回滚,然后您可以选择:a) 发出ROLLBACK这主要是对这种情况的接受,因为事务已经回滚减去重置@@TRANCOUNT为0,或者 b) 出现错误。没有太多选择,是吗?
这个难题的一个可能重要的细节在该文档中并不明显,因为SET XACT_ABORT该会话属性,甚至该示例代码,自SQL Server 2000以来一直存在(版本之间的文档几乎相同),早于TRY...CATCH构造在SQL Server 2005中引入再次是看文档和例子看(没有的TRY...CATCH),使用XACT_ABORT ON原因进行即时交易的回滚:有“不可提交”(请注意,无交易状态,没有提及在该SET XACT_ABORT文档中的所有“不可提交”交易状态)。
我认为可以得出以下结论:
TRY...CATCHSQL Server 2005 中构造的引入产生了对新事务状态(即“不可提交”)和XACT_STATE()获取该信息的函数的需求。XACT_STATE()在CATCH块真正才有意义,如果两个以下为真:XACT_ABORT是OFF(否则XACT_STATE()应该总是返回-1,@@TRANCOUNT这就是你所需要的)CATCH如果调用是嵌套的,则块中有逻辑,或者链上的某个地方有逻辑,这会进行更改(COMMIT甚至任何 DML、DDL 等语句),而不是执行ROLLBACK. (这是一个非常不典型的用例)** 请参阅底部更新 3 部分的注释,关于 Microsoft 的非官方建议始终检查XACT_STATE()而不是@@TRANCOUNT,以及为什么测试表明他们的推理没有成功。TRY...CATCH在 SQL Server 2005 中引入的构造在很大程度上已经过时了XACT_ABORT ONsession 属性,因为它提供了对事务的更大程度的控制(您至少可以选择COMMIT,前提是XACT_STATE()不返回-1)。XACT_ABORT ON,与@@ERROR在每个语句之后检查相比,它提供了一种简单可靠的方法来在发生错误时停止处理。XACT_STATE()是错误的,或者充其量是误导,因为它显示检查XACT_STATE() = 1when XACT_ABORTis ON。长部分;-)
是的,MSDN 上的示例代码有点令人困惑(另请参阅:@@TRANCOUNT (Rollback) vs. XACT_STATE ) ;-)。而且,我觉得它具有误导性,因为它要么显示出毫无意义的东西(出于您所询问的原因:您是否甚至可以在CATCH块中拥有“可提交的”交易when XACT_ABORTis ON),或者即使可能,它仍然专注于很少有人想要或需要的技术可能性,而忽略了人们更有可能需要它的原因。
如果 TRY 块中存在足够严重的错误,则控制将传递到 CATCH。所以,如果我在 CATCH 中,我知道事务有问题,在这种情况下唯一明智的做法就是回滚它,不是吗?
我认为,如果我们确保我们在某些词语和概念的含义上达成一致,这会有所帮助:
“严重的错误”:明确地说,TRY...CATCH将捕获大多数错误。将不会被捕获的列表列在链接的 MSDN 页面上,在“不受 TRY…CATCH 构造影响的错误”部分下。
“如果我是CATCH里面,我知道交易已经出现了问题”(EM PHA的加入):如果“交易”你指的是逻辑工作单元,通过您通过分组语句转换成明确的事务确定,然后很可能是的。我认为我们大多数 DB 人员都倾向于同意回滚是“唯一明智的做法”,因为我们可能对我们如何以及为什么使用显式事务有类似的看法,并设想哪些步骤应该构成一个原子单元工作的。
但是,如果您指的是分组到显式事务中的实际工作单元,那么不,您不知道事务本身存在问题。您只知道在显式定义的事务中执行的语句引发了错误。但它可能不是 DML 或 DDL 语句。即使它是一个 DML 语句,事务本身可能仍然是可提交的。
鉴于上述两点,我们可能应该区分您“不能”提交的事务和您“不想”提交的事务。
当XACT_STATE()返回 a 时1,这意味着交易是“可提交的”,您可以在COMMIT或之间进行选择ROLLBACK。您可能不想提交它,但如果出于某些难以想出的例子,您想要提交,至少您可以,因为事务的某些部分确实成功完成。
但是当XACT_STATE()返回 a 时-1,您确实需要这样做,ROLLBACK因为事务的某些部分进入了错误状态。现在,我确实同意,如果控制权已传递给 CATCH 块,那么检查就足够了@@TRANCOUNT,因为即使您可以提交事务,为什么要提交?
但是,如果您在示例的顶部注意到, 的设置XACT_ABORT ON会稍微改变一些东西。你可以有一个经常出现偏差,之后做BEGIN TRAN时将控制传递给CATCH块XACT_ABORT是OFF和XACT_STATE()将返回1。但是,如果 XACT_ABORT 是ON,则事务因任何 'ol 错误而“中止”(即无效),然后XACT_STATE()将返回-1。在这种情况下,XACT_STATE()在CATCH块内检查似乎没用,因为它似乎总是返回-1when XACT_ABORTis ON。
那么是XACT_STATE()为了什么呢?一些线索是:
的 MSDN 页面TRY...CATCH,在“不可提交的事务和 XACT_STATE”部分下,说:
当错误发生在 TRY 块内时,通常会在 TRY 块外结束事务的错误会导致事务进入不可提交状态。
SET XACT_ABORT的 MSDN 页面,在“备注”部分下,说:
当 SET XACT_ABORT 为 OFF 时,在某些情况下,只会回滚引发错误的 Transact-SQL 语句,并且事务继续处理。
和:
对于大多数 OLE DB 提供程序(包括 SQL Server)的隐式或显式事务中的数据修改语句,必须将 XACT_ABORT 设置为 ON。
BEGIN TRANSACTION的 MSDN 页面,在“备注”部分下,说:
如果在提交或回滚语句之前执行以下操作,则由 BEGIN TRANSACTION 语句启动的本地事务将升级为分布式事务:
- 执行引用链接服务器上远程表的 INSERT、DELETE 或 UPDATE 语句。如果用于访问链接服务器的 OLE DB 提供程序不支持 ITransactionJoin 接口,则 INSERT、UPDATE 或 DELETE 语句将失败。
最适用的用法似乎是在链接服务器 DML 语句的上下文中。我相信我几年前就遇到过这种情况。我不记得所有的细节,但它与远程服务器不可用有关,并且出于某种原因,该错误没有在 TRY 块中被捕获并且从未被发送到 CATCH,所以它确实如此不应该有的 COMMIT。当然,这可能是没有XACT_ABORT设置ON而不是没有检查的问题XACT_STATE(),或者可能两者兼而有之。我确实记得读过一些内容,说如果您使用链接服务器和/或分布式事务,那么您需要使用XACT_ABORT ON和/或XACT_STATE(),但我现在似乎找不到该文档。如果我找到它,我会用链接更新它。
尽管如此,我已经尝试了几件事,但无法找到一个场景,该场景具有XACT_ABORT ON并将控制权传递给CATCH带有XACT_STATE()报告的块1。
尝试这些示例以查看XACT_ABORT对 的值的影响XACT_STATE():
SET XACT_ABORT OFF;
BEGIN TRY
BEGIN TRAN;
SELECT 1/0 AS [DivideByZero]; -- error, yo!
COMMIT TRAN;
END TRY
BEGIN CATCH
SELECT @@TRANCOUNT AS [@@TRANCOUNT],
XACT_STATE() AS [XactState],
ERROR_MESSAGE() AS [ErrorMessage]
IF (@@TRANCOUNT > 0)
BEGIN
ROLLBACK;
END;
END CATCH;
GO ------------------------------------------------
SET XACT_ABORT ON;
BEGIN TRY
BEGIN TRAN;
SELECT 1/0 AS [DivideByZero]; -- error, yo!
COMMIT TRAN;
END TRY
BEGIN CATCH
SELECT @@TRANCOUNT AS [@@TRANCOUNT],
XACT_STATE() AS [XactState],
ERROR_MESSAGE() AS [ErrorMessage]
IF (@@TRANCOUNT > 0)
BEGIN
ROLLBACK;
END;
END CATCH;
GO ------------------------------------------------
SET XACT_ABORT ON;
BEGIN TRY
SELECT 1/0 AS [DivideByZero]; -- error, yo!
END TRY
BEGIN CATCH
SELECT @@TRANCOUNT AS [@@TRANCOUNT],
XACT_STATE() AS [XactState],
ERROR_MESSAGE() AS [ErrorMessage]
END CATCH;
Run Code Online (Sandbox Code Playgroud)
更新
虽然不是原始问题的一部分,但基于对这个答案的这些评论:
我一直在阅读 Erland 关于错误和事务处理的文章,他说这
XACT_ABORT是OFF出于遗留原因的默认设置,通常我们应该将其设置为ON.
......
“......如果你按照建议并在 SET XACT_ABORT ON 的情况下运行,交易将永远注定失败。”
在XACT_ABORT ON到处使用之前,我会问:这里到底得到了什么?我没有发现有必要这样做,并且通常提倡您只在必要时使用它。ROLLBACK通过使用@Remus's answer 中显示的模板,或者我多年来一直使用的模板,无论您是否希望可以轻松处理它,该模板本质上是相同的,但没有保存点,如本答案所示(其中处理嵌套调用):
更新 2
我做了更多的测试,这一次通过创建一个小的 .NET 控制台应用程序,在应用程序层创建一个事务,在执行任何SqlCommand对象(即 via using (SqlTransaction _Tran = _Connection.BeginTransaction()) { ...)之前,以及使用批处理中止错误而不仅仅是一个语句-aborting 错误,发现:
@@TRANCOUNT仍 > 0 的事务。COMMIT因为这会产生错误,说交易是“不可提交的”。你也不能忽略它/什么都不做,因为当批处理完成时会产生一个错误,说明批处理完成了一个挥之不去的、不可提交的事务,它将被回滚(所以,嗯,如果它无论如何都会自动回滚,为什么要抛出错误?)。因此,您必须发出一个明确的ROLLBACK,可能不是在立即CATCH块中,而是在批处理结束之前。TRY...CATCH构造中, when XACT_ABORTis OFF,如果它们发生在TRY块之外,则会自动终止事务的错误(例如批量中止错误)将撤消工作但不会终止事务,使其成为“不可提交”。发出 aROLLBACK更像是结束交易所需的手续,但工作已经回滚了。XACT_ABORTis 时ON,大多数错误都作为批处理中止,因此行为如上面的要点(#3)中所述。XACT_STATE(),至少在一个CATCH块中,-1如果在错误发生时有一个活动事务,则将显示批处理中止错误。XACT_STATE()1即使没有活动事务,有时也会返回。如果@@SPID(除其他外)与SELECT一起在列表中XACT_STATE(),则XACT_STATE()在没有活动事务时将返回 1。此行为始于 SQL Server 2012,并存在于 2014 年,但我尚未在 2016 年测试过。考虑到以上几点:
XACT_STATE()中CATCH块的时候XACT_ABORT是ON因为返回的永远是值-1。XACT_STATE()中CATCH块时XACT_ABORT是OFF更有意义,因为返回值将至少有一些变化,因为它会返回1的语句中止错误。但是,如果您像我们大多数人一样编写代码,那么这种区别是没有意义的,因为您ROLLBACK无论如何都会因为发生错误而调用。COMMIT在CATCH块,然后检查的价值XACT_STATE(),并确保SET XACT_ABORT OFF;。XACT_ABORT ON似乎对TRY...CATCH构造几乎没有好处。XACT_STATE()比简单检查更有意义的检查的情况@@TRANCOUNT。XACT_STATE()返回1的CATCH块时XACT_ABORT是ON。我认为这是一个文档错误。XACT_ABORT ON,这是一个有争议的问题,因为TRY块中发生的错误将自动回滚更改。TRY...CATCH构造的好处XACT_ABORT ON在于不会自动取消整个事务,因此允许提交事务(只要XACT_STATE()返回1)(即使这是一个边缘情况)。XACT_STATE()返回-1时间的示例XACT_ABORT是OFF:
SET XACT_ABORT OFF;
BEGIN TRY
BEGIN TRAN;
SELECT CONVERT(INT, 'g') AS [ConversionError];
COMMIT TRAN;
END TRY
BEGIN CATCH
DECLARE @State INT;
SET @State = XACT_STATE();
SELECT @@TRANCOUNT AS [@@TRANCOUNT],
@State AS [XactState],
ERROR_MESSAGE() AS [ErrorMessage];
IF (@@TRANCOUNT > 0)
BEGIN
SELECT 'Rollin back...' AS [Transaction];
ROLLBACK;
END;
END CATCH;
Run Code Online (Sandbox Code Playgroud)
更新 3
与 UPDATE 2 部分中的第 6 项相关(即XACT_STATE()当没有活动事务时返回的可能不正确的值):
奇怪/错误的行为始于 SQL Server 2012(到目前为止已针对 2012 SP2 和 2014 SP1 进行了测试)
在 SQL Server 2005、2008 和 2008 R2 版本中,XACT_STATE()在触发器或INSERT...EXEC场景中使用时未报告预期值:无法可靠地使用 xact_state() 来确定事务是否注定失败 (存档页面)。但是,(我只在2008 R2测试)这3个版本,XACT_STATE()并没有错误地报告1在使用时SELECT用@@SPID。
那里 是 是针对此处提到的行为提交的 Connect 错误,但已关闭为“按设计”:XACT_STATE() 可以在 SQL 2012 中返回不正确的事务状态 (由于无能力和/或严重疏忽的站点迁移,链接不再有效)。但是,测试是在从 DMV 中进行选择时完成的,并且得出的结论是,这样做自然会产生系统生成的交易,至少对于某些 DMV。MS 在最终回复中还指出:
请注意,IF 语句以及不带 FROM 的 SELECT 不会启动事务。
例如,如果您没有先前存在的事务,则运行 SELECT XACT_STATE() 将返回 0。
鉴于以下示例,这些陈述是不正确的:
SELECT @@TRANCOUNT AS [TRANCOUNT], XACT_STATE() AS [XACT_STATE], @@SPID AS [SPID];
GO
DECLARE @SPID INT;
SET @SPID = @@SPID;
SELECT @@TRANCOUNT AS [TRANCOUNT], XACT_STATE() AS [XACT_STATE], @SPID AS [SPID];
GO
Run Code Online (Sandbox Code Playgroud)
因此,我提交了一个新的反馈错误:
XACT_STATE() 在 SELECT 中使用某些系统变量但没有 FROM 子句时返回 1
请注意,在“XACT_STATE() 可以在 SQL 2012 中返回不正确的事务状态”中,直接链接在上面的连接项中,Microsoft(好吧,代表)指出:
@@trancount 返回 BEGIN TRAN 语句的数量。因此,它不是是否存在活跃交易的可靠指标。如果存在活动的自动提交事务,XACT_STATE() 也会返回 1,因此是更可靠的指示是否存在活动事务的指标。
但是,我找不到不信任的理由@@TRANCOUNT。以下测试表明@@TRANCOUNT它确实1在自动提交事务中返回:
--- begin setup
GO
CREATE PROCEDURE #TransactionInfo AS
SET NOCOUNT ON;
SELECT @@TRANCOUNT AS [TranCount],
XACT_STATE() AS [XactState];
GO
--- end setup
DECLARE @Test TABLE (TranCount INT, XactState INT);
SELECT * FROM @Test; -- no rows
EXEC #TransactionInfo; -- 0 for both fields
INSERT INTO @Test (TranCount, XactState)
EXEC #TransactionInfo;
SELECT * FROM @Test; -- 1 row; 1 for both fields
Run Code Online (Sandbox Code Playgroud)
我还在一个带有触发器的真实表上进行了测试,即使没有启动显式事务@@TRANCOUNT,触发器内也能准确报告1。
我会以不同的方式处理这个问题。XACT_ABORT_ON是一把大锤,你可以使用更精细的方法,参见异常处理和嵌套事务:
create procedure [usp_my_procedure_name]
as
begin
set nocount on;
declare @trancount int;
set @trancount = @@trancount;
begin try
if @trancount = 0
begin transaction
else
save transaction usp_my_procedure_name;
-- Do the actual work here
lbexit:
if @trancount = 0
commit;
end try
begin catch
declare @error int, @message varchar(4000), @xstate int;
select @error = ERROR_NUMBER(), @message = ERROR_MESSAGE(), @xstate = XACT_STATE();
if @xstate = -1
rollback;
if @xstate = 1 and @trancount = 0
rollback
if @xstate = 1 and @trancount > 0
rollback transaction usp_my_procedure_name;
raiserror ('usp_my_procedure_name: %d: %s', 16, 1, @error, @message) ;
end catch
end
go
Run Code Online (Sandbox Code Playgroud)
这种方法将在可能的情况下仅回滚在 TRY 块内执行的工作,并将状态恢复到进入 TRY 块之前的状态。通过这种方式,您可以进行复杂的处理,例如迭代游标,而不会在出现错误时丢失所有工作。唯一的缺点是,通过使用事务保存点,你被限制使用任何与保存点不兼容的东西,比如分布式事务。
防御性编程要求您编写处理尽可能多的已知状态的代码,从而减少出现错误的可能性。
检查 XACT_STATE() 以确定是否可以执行回滚是一个很好的做法。盲目尝试回滚意味着您可能会在 TRY...CATCH 中无意中导致错误。
在 TRY...CATCH 中回滚可能失败的一种方式是,如果您没有明确启动事务。复制和粘贴代码块可能很容易导致这种情况。