我有一个用C#编写的自动脚本,它在SQL Server 2014上运行存储过程。存储过程运行多个select,update和insert语句,并利用try catch回滚模式捕获并回滚整个事务。 #39;是个例外。
看起来与此类似:
BEGIN TRY
BEGIN TRANSACTION TransName
--Lots of SQL!
COMMIT TRANSACTION TransName
END TRY
BEGIN CATCH
ROLLBACK TRANSACTION TransName;
THROW
END CATCH
调用该过程的我的C#看起来与此类似:
using (SqlCommand Command = new SqlCommand(query, Connection))
{
// Retry several times if the query fails.
for (var retry = 0; retry < 5 && !Success; ++retry)
{
try
{
Command.ExecuteNonQuery();
Success = true;
}
catch (SqlException e)
{
// Handling for Timeout or deadlocks.
// If not a timeout or deadlock and retry hasn't happened 4 times already.
if (!(e.Number == 1205 || e.Number == 1204 || e.Number == -2) || retry == 4)
{
LogException(e);
}
else if (e.Number == 1205 || e.Number == 1204)
{
// Wait to avoid hammering the database.
Thread.Sleep(500);
}
else if (e.Number == -2)
{
// Wait to avoid hammering the database.
Thread.Sleep(5000);
}
Success = false;
}
}
}
我已经进行了循环,以确保在遇到死锁或超时时SQL会通过,因为它是一个自动脚本。
在我的脚本日志中,我可以看到存储过程没有记录任何异常,但是程序触及的表中没有任何数据存在我的问题:
是否可以在T-SQL中捕获异常,然后使用T-SQL THROW
语句再次抛出异常,但是C#客户端不会抛出异常?
如果我能澄清任何事情,请告诉我。谢谢!
答案 0 :(得分:3)
SQL中的try...catch
的工作方式略有不同,我过去所做的就是在存储过程中使用OUTPUT
变量:
ALTER PROCEDURE dbo.yourStoredProcedure
(-- your parameters
@errNumber INT OUTPUT,
@errLine INT OUTPUT,
@errMessage VARCHAR(MAX) OUTPUT)
AS
BEGIN
SET @errNumber = 0
SET @errLine = 0
SET @errMessage = ''
BEGIN TRY
BEGIN TRANSACTION TransName
--Lots of SQL!
COMMIT TRANSACTION TransName
END TRY
BEGIN CATCH
ROLLBACK TRANSACTION TransName;
SELECT @errNumber = ERROR_NUMBER()
, @errLine = ERROR_LINE()
, @errMessage = ERROR_MESSAGE()
END CATCH
END
GO
您需要调整C#中的try
以添加参数并读取返回值
try
{
SqlParameter errNumber = new SqlParameter("@errNumber", 0);
SqlParameter errLine = new SqlParameter("@errLine", 0);
SqlParameter errMessage = new SqlParameter("@errMessage", "");
Command.ExecuteNonQuery();
int SqlError = (int)(errNumber.Value);
int SqlLine = (int)(errNumber.Value);
string SqlMessage = (string)errMessage.Value;
if (SqlError == 0 ) { Success = true; }
else {
Success = false;
// whatever else you want to do with the error data
}
}
您的SqlException
捕获仍会捕获不在过程TRY...CATCH
中的错误,并且您还应该有一个通用Catch(Exception ex)
块以及其他错误,最后不会忘记finally {}
进行任何可能需要的清理工作。
更新05/03/2017
在大多数情况下,在try...catch
内包装交易会导致无法承认的交易。因此,我们可以翻转包装以在事务中使用try-catch。如果发现错误,那么我们应该能够获得错误值,如果事务再次存在(@@transcount >0)
,它将被回滚,并且@@ transcount将减少为0.在try-catch块关闭之后我们再次检查@@ transount并提交(如果存在)
BEGIN TRANSACTION TransName
BEGIN TRY
--Lots of SQL!
END TRY
BEGIN CATCH
SELECT @errNumber = ERROR_NUMBER()
, @errLine = ERROR_LINE()
, @errMessage = ERROR_MESSAGE()
IF (@@TRANCOUNT > 0) ROLLBACK TRANSACTION TransName
END CATCH
IF (@@TRANCOUNT > 0) COMMIT TRANSACTION TransName
答案 1 :(得分:1)
此博客对此内容进行了介绍:http://www.dbdelta.com/the-curious-case-of-undetected-sql-exceptions/
如果返回第一行后发生T-SQL错误,ExecuteScalar将不会引发异常。另外,如果由于行返回语句错误并且在T-SQL中捕获了错误而没有返回任何行,则ExecuteScalar返回空对象而不会引发异常。
由于相同的原因,ExecuteNonQuery可能发生相同的问题。
答案 2 :(得分:0)
您是否在路上捕获并记录其他异常类型?如果抛出SqlException以外的其他内容会发生什么?它被记录了吗?
关于重试逻辑 - 我也会处理InvalidOperationException。如果Connection未打开,ExecuteNonQuery将抛出InvalidOperationException。例如,由于短暂的网络中断或其他原因,连接可能会进入ConnectionState.Broken状态。类似于重试死锁和超时的方法,我会捕获InvalidOperationException,检查连接状态,如果它没有打开 - 重新打开它并重试。
答案 3 :(得分:0)
首先,谢谢@MadMyche的建议,添加输出参数帮助我知道捕获因为某些原因从未被击中。
发生的事情是,当查询运行时,它偶尔会超时并返回到C#代码的重试循环中,当发生这种情况时,查询中打开的事务没有被关闭。当重试循环最终循环回来并成功完成查询时,它将关闭sql连接,当发生这种情况时,SQL引擎会通过并关闭并对任何打开的事务执行回滚,这会删除已保存的数据
我发现这篇文章解释了会发生什么并提供解决方案:
然后我发现另一篇文章重新强化了解决方案:
http://www.sommarskog.se/error_handling/Part1.html
我要解决的是在proc的开头设置XACT_ABORT:
SET XACT_ABORT ON;
设置XACT_ABORT“指定当Transact-SQL语句引发运行时错误时SQL Server是否自动回滚当前事务”(参见documentation)
如果将XACT_ABORT设置为ON,则事务将在c#客户端关闭连接之前回滚,以便不会回滚任何打开的事务。