Sql Server支持嵌套事务:也就是说在前一事务未完成之前可启动一个新的事务,只有在外层的Commit Tran语句才会导致数据库的永久更改。
请尝试执行以下语句:
BEGIN TRAN tr0
BEGIN TRAN tr1
ROLLBACK TRAN tr1
ROLLBACK TRAN tr0
执行结果:服务器: 消息 3903,级别 16,状态 1,行 5
ROLLBACK TRANSACTION 请求没有对应的 BEGIN TRANSACTION。
原因分析:
1) Sql Server把每个连接开启的事务数目记录在全局变量@@trancount中,就象计数器一样,每个Begin Tran语句会让@@trancount自增1,每个Commit Tran语句会让@@trancount自减1,只有最外层的Commit Tran(当@@trancount=1)会将更改影响到数据库中,而不再存储在事务日志中。
2) Sql Server只会记录外层事务名称如果企图回滚任一内层事务,错误就会出现。
3) 非常遗憾的是,不管嵌套的事务层次有多深,不带保存点的Rollback Tran语句将直接把@@trancount设置为0
解决思路:
1) 采用保存点:Sql Server提供了一种用于回滚部分事务的机制:Save Tran ,它不会对@@trancount产生任何影响,只是标记回滚事务时可以到达的点。
(但是这种方法不适用于“分布式事务”的远程调用,因为分布式事务不支持事务保存点: save transaction )
--定义一个是否为嵌套事务的标志
DECLARE @nestedFlag BIT
IF(@@trancount>0)
BEGIN
--是嵌套事务:使用保存点,避免再次嵌套
SET @nestedFlag=1
SAVE TRAN TestA
END
ELSE
BEGIN
--不是嵌套事务:开启一个事务
SET @nestedFlag=0
BEGIN TRAN TestA
END
--执行业务操作,如果出错就回滚事务点,并立即返回
IF(@@error<>0)
BEGIN
ROLLBACK TRAN TestA
RETURN 0
END
--如果不是嵌套事务才提交
IF(@nestedFlag=0)
BEGIN
COMMIT TRAN TestA
END
2) 如果存储过程 可能用于分布式事务,先判断是否被外层事务包括,如果是, 建议不管是否出错都commit, 然后用RAISERROR抛出一个异常,并返回一个错误码,由外部事务去判断是否要回滚。
--可在分布式事务中调用的存储过程样例.
create procedure p_trans_test
as
begin
declare @trancount int;
set @trancount = @@trancount;
begin transaction test1
begin try
print 1-- 业务逻辑
end try
begin catch
if (@trancount = 0)
begin
rollback transaction test1;
return -1
end
else
begin
commit transaction test1;
-- 抛出异常与错误码,由外部调用者去处理是否回滚.
RAISERROR ('Error raised in TRY block.', -- Message text.
16, -- Severity.
1 -- State.
);
return -1;
end
end catch
return 0;
end