對於帶有事務的嵌套存儲過程的處理網上的確有很多資料可參考。但大多是片面或過於字面化。對不清楚如何解決這種問題的同仁來說沒有多大用處。最近正好工作不忙,就研究了下,發現sqlserver的嵌套事務存儲過程在處理時確實有一定難度。原因在於微軟的sqlserver在帶有嵌套的事務方面存在以下幾點特徵:
1。 sqlserver 忽略提交內部事務。即無論內層事務是否提交,外層事務提交後,所有內層事務都會提交,反之外層事務回滾了,內層事務即使提交過,也會回滾。以外層事務的最後執行的操作(commit or rollback)爲準。
2。每個commit的調用都只對應最後執行的begin transaction。意思就是每個commit對應與自己開始的begin transaction。打個比方:內層commit對應的是內層begin transaction,而不會影響外層。即使有更多層的嵌套也是如此。
3。rollback transaction 中的 transaction_name (事務名)只能引用最外層的transaction_name。引用內層或更多嵌套內層的事務名是不允許的。比如:內層begin transaction xx(事務名),內層rollback transaction 時不能引用xx,而只能引用最外層的事務名。
4。@@trancount記錄事務的嵌套級數。每begin transaction一次 都會使@@trancount加1,每commit transaciton一次都會使@@trancount減1,而rollback transaction 則會直接使@@trancount爲0,無論是嵌套事務還是單一事務。可通過檢查@@trancount是否爲0來確實是否還在事務中,大於0表明還在事務中。
由於以上幾的特徵的限制,使得嵌套的事務處理不那麼容易。單一的事務存儲過程是很好解決的。但如果需要調用已經存在的存儲過程且自身已有事務處理了,那麼問題就出現了(二層嵌套事務的存儲過程執行結果):
原因在於:每個事務在開始與提交時@@trancount要對應,如果開始事務的@@trancount與commit時的@@trancount不對應,就會出現以上的問題。說白了就是開始事務時@@trancount爲1 ,那麼在commit時也要爲1,commit之後纔會使@@trancount減1。
要解決嵌套事務的存儲過程,避免以上的問題,個人總結兩個方式:
1。使用@@trancount + 事務保存點 解決。 外層事務傳遞當前@@trancount值給內層事務,根據傳遞過來的@@trancount值是否大於0來選擇是否保存事務點。大於0則保存,用於內層回滾到此事務點,等於0說明沒開始事務,則直接開始內層的事務處理。內層事務處理中如果有錯誤,則根據傳遞過來的@@trancount值判斷是回滾整個事務還是回滾到事務的保存點。
2。使用事務保存點 + rollback transactin 事務保存點 + 強制內層事務提交 解決。 此方式與1差別不多大,只是不使用@@trancount全局函數。內層事務開始後立即保存事務點。出錯後直接回滾到事務保存點,再交由外層的事務來決定最後的執行操作。
以下是第一種方式的sql腳本的詳細過程:
--測試用的數據庫test
use test;
GO
--刪除測試用的表
if EXISTS
(
SELECT * FROM sys.objects WHERE object_id = OBJECT_ID(N'AgeValues')
)
begin
DROP TABLE AgeValues;
end
GO
--內層存儲過程已存在則先刪除再創建
IF exists
(
SELECT * from sys.objects WHERE object_id = object_id(N'InnerProcedure')
)
begin
drop PROCEDURE InnerProcedure;
end
--外層存儲過程已存在則先刪除再創建
IF exists
(
SELECT * from sys.objects where object_id = object_id(N'OuterProcedure')
)
begin
drop PROCEDURE OuterProcedure;
end
--創建測試用的表
CREATE table AgeValues(age int not null check (age < 200));
GO
--設置不返回受影響的行數
SET NOCOUNT on;
GO
/*******************************************************************************
作者:董輝
類型:新建
日期:2012-7-17 16:16
說明:用於解決嵌套事務的存儲過程的方式 1
*******************************************************************************/
--內層事務存儲過程,可直接執行,也可被其它帶有事務的存儲過程調用
create procedure InnerProcedure
(
@TranCount int, --當前@@trancount值
@in_AgeValue int, --年齡
@out_Result varchar(500) output --結果(OK正確,其它錯誤)
)
as
begin
begin try --開始捕獲異常
print ('開始內層事務前,@@trancount計數爲:' + cast(@@trancount as varchar));
begin transaction InnerTran --開始事務
save transaction InnerTranPoint --保存事務點,用於回滾
print ('開始內層事務後,@@trancount計數爲:' + cast(@@trancount as varchar));
print ('開始執行InnerProcedure存儲過程的數據處理......');
insert INTO AgeValues VALUES(@in_AgeValue);
IF @@error <> 0 --如果執行有錯誤,則@@error顯示錯誤碼,無錯誤時顯示0
begin
if @TranCount > 0
begin
set @out_Result = '執行出錯,錯誤碼爲:' + cast(@@error AS VARCHAR);
print ('被其它事務存儲過程調用時,在執行回滾事務前,@@trancount計數爲:' + cast(@@trancount as varchar));
rollback transaction InnerTranPoint; --回滾到事務的保存點
print ('被其它事務存儲過程調用時,在執行回滾事務後,@@trancount計數爲:' + cast(@@trancount as varchar));
print ('被其它事務存儲過程調用時,在執行提交事務前,@@trancount計數爲:' + cast(@@trancount as varchar));
commit transaction InnerTran; --把當前的事務提交
print ('被其它事務存儲過程調用時,在執行提交事務後,@@trancount計數爲:' + cast(@@trancount as varchar));
end
else if @TranCount = 0
begin
set @out_Result = '執行出錯,錯誤碼爲:' + cast(@@error AS VARCHAR);
print ('直接調用時,在執行回滾事務前,@@trancount計數爲:' + cast(@@trancount as varchar));
rollback transaction InnerTran;
print ('直接調用時,在執行回滾事務後,@@trancount計數爲:' + cast(@@trancount as varchar));
end
end
else --沒有錯誤時則直接提交當前事務,並設置返回信息爲1
begin
set @out_Result = 'OK';
print ('執行沒有出錯時,在提交事務前,@@trancount計數爲:' + cast(@@trancount as varchar));
commit transaction InnerTran;
print ('執行沒有出錯時,在提交事務後,@@trancount計數爲:' + cast(@@trancount as varchar));
end
end try --結束捕獲異常
begin catch --處理異常
if @TranCount > 0
begin
set @out_Result = 'catch內捕獲:執行出錯,錯誤碼爲:' + cast(ERROR_NUMBER() AS VARCHAR) + ' 錯誤信息爲:' + error_message();
print ('被其它事務存儲過程調用時,在執行回滾事務前,@@trancount計數爲:' + cast(@@trancount as varchar));
rollback transaction InnerTranPoint; --回滾到事務的保存點
print ('被其它事務存儲過程調用時,在執行回滾事務後,@@trancount計數爲:' + cast(@@trancount as varchar));
print ('被其它事務存儲過程調用時,在執行提交事務前,@@trancount計數爲:' + cast(@@trancount as varchar));
commit transaction InnerTran; --把當前的事務提交
print ('被其它事務存儲過程調用時,在執行提交事務後,@@trancount計數爲:' + cast(@@trancount as varchar));
end
else if @TranCount = 0
begin
set @out_Result = '執行出錯,錯誤碼爲:' + cast(ERROR_NUMBER() AS VARCHAR) + ' 錯誤信息爲:' + error_message();
print ('直接調用時,在執行回滾事務前,@@trancount計數爲:' + cast(@@trancount as varchar));
rollback transaction InnerTran;
print ('直接調用時,在執行回滾事務後,@@trancount計數爲:' + cast(@@trancount as varchar));
end
end catch --處理異常
end
go
--外層事務存儲過程,用來調用其它帶事務的存儲過程
create procedure OuterProcedure
(
@in_Age int --年齡
)
as
begin
declare @TranCount int; --當前@@trancount值
declare @Result varchar(500); --用來接收返回的信息
print ('外層事務開始前,@@trancount計數爲:' + cast(@@trancount as varchar));
begin transaction OuterTran --開始事務
print ('外層事務開始後,@@trancount計數爲:' + cast(@@trancount as varchar));
print ('開始執行OuterProcedure存儲過程的數據處理......');
set @TranCount = @@trancount; --記錄當前@@trancount值
exec InnerProcedure @TranCount,@in_Age,@Result output;
IF @Result <> 'OK' --有錯誤
BEGIN
print ('外層事務回滾前,@@trancount計數爲:' + cast(@@trancount as varchar));
rollback transaction OuterTran;
print ('外層事務回滾後,@@trancount計數爲:' + cast(@@trancount as varchar));
print ('外層:執行出錯,已回滾!');
end
else
begin
print ('外層事務提交前,@@trancount計數爲:' + cast(@@trancount as varchar));
commit transaction OuterTran;
print ('外層事務提交後,@@trancount計數爲:' + cast(@@trancount as varchar));
print ('外層:執行成功,已提交!');
end
print (char(10) + @Result);
end
go
--開始測試(值大於200則不能插入表中。AgeValues表age字段約束爲不能大於200)
declare @k int = 300;
exec OuterProcedure @k;
go
這個是第二種方式的sql腳本詳細過程:
/***************************************************************************
作者:董輝
類型:新建
日期:2012-7-17 9:26
說明:用於解決嵌套事務的存儲過程的方式 2
outertran事務存儲過程在調用 innertran 事務存儲過程與調用 innertranDemo
事務存儲過程有區別,調用第一個時由於內層出錯回滾了當前事務,這時
@@trancount被回滾到了0,外層再回滾時就會出錯。而調用第二個事務存儲過程
時,由於內層事務存儲過程保存了事務點,出錯時回滾到事務點,這樣外層就不
會出數計數不匹配的錯誤。
BEGIN distributed transaction 指定一個由 Microsoft 分佈式事務處理協調器
(MS DTC) 管理的 Transact-SQL 分佈式事務的起始
***************************************************************************/
--內層事務存儲過程,演示如何處理才能在嵌套的事務存儲過程中正確處理事務
alter procedure innertranDemo
as
begin
begin tran tran1 --開始事務
save tran t --保存事務點
insert into t1 VALUES(1);
if @@error <> 0
begin
rollback tran t; --回滾保存點的事務
commit tran tran1; --提示當前事務
return -1;
end
insert INTO t1 VALUES(189);
IF @@error <> 0
begin
rollback tran t;
commit tran tran1;
return -1;
end
commit tran tran1;
return 1;
end
--外層事務存儲過程
alter procedure outertran
as
begin
-- with mark '開始事務' 指定在日誌中標記事務
begin tran tran2 with mark '開始事務' --開始事務,使@@trancount加1
insert INTO t1 VALUES(2);
declare @invikecode int;
exec @invikecode = innertranDemo;--執行innertran與執行innertranDeom會有不同的結果
IF @invikecode = -1
begin
rollback tran tran2;
print ('回滾,當前計數爲:' + cast(@@trancount as varchar));
end
else
begin
commit tran tran2;
print ('提交,當前計數爲:' + cast(@@trancount as varchar));
end
end
--測試
exec outertran;
總結下來,對於嵌套事務的存儲過程解決起來無非就是利用@@trancount來判斷事務的當前嵌套狀態,來決定內層的事務是回滾到事務點,還是直接全部回滾。內層回滾時是可以直接引用外層的事務名進行操作的。但不允許引用自己內層的事務名。如果說有這種情況,就是別人寫好的正常執行的單個帶事務操作的存儲過程,你去調用,但你自己的存儲過程也需要事務處理。這種情況再怎麼解決也是需要去修改它的存儲過程的。不是修改邏輯處理,而是按上面的方式進行能正常用於嵌套事務的處理方式來修改。畢竟單一事務與多重嵌套事務在處理上還是存在很大差別的。