古老的ASP時代,要做Distributed Transaction(分散式交易,指跨越異質資料庫的交易,例如: 將SQL Server跟Oracle的更新動作包成一個Transaction),有個偷懶的方法。在ASP最前端宣告一下<%@ Transaction=Required%>,則整個ASP中的所有資料庫操作,不管Oracle、SQL、Sybase,通通會自動包成Transaction,不用多寫半行Code。

不過,這種寧可錯殺一百,不可錯放一個的做法效能有點鳥(連沒必要的SELECT動作也被包入Transaction)。會寫VB COM的人多半會寫顆Support Transaction的COM+元件,用來執行特定的資料庫的更新。而多顆異質資料庫的Transactional元件可以再包出一個大Transaction。不過,這得另外寫COM,註冊到COM+ Application中,多了些額外手續。

ASP.NET 1.x誕生後,針對分散式交易,提供了一個四不像的做法:
寫一顆繼承自System.EnterpriseService.ServicedComponent的元件,內含更新資料庫的程式邏輯,再設定TransactionAttribute,然後要Strong-Named/Signed,包上COM+的皮,註冊放入COM+ Application中。

呃... 好像比以前寫COM+還麻煩,堂堂的.NET還是得回頭靠COM+才能實踐分散式交易,會不會有點...

終於.NET 2.0中,針對分散式交易做出了改良。System.Transactions.TransactionScope讓大家有機會重回ASP時代的美妙時光,可以將任意一段資料庫操作包成一個Transaction,不需要額外的手工。例如以下的範例: (要Oracle支援Transaction,記得安裝Oracle Service For Microsoft Transaction Server,不然會百忙一場。)

   21 TransactionOptions options = new TransactionOptions();

   22 options.IsolationLevel = System.Transactions.IsolationLevel.ReadCommitted;

   23 options.Timeout = new TimeSpan(0, 2, 0);

   24 using (TransactionScope scope = new TransactionScope(TransactionScopeOption.Required, options))

   25 {

   26     try

   27     {

   28         using (SqlConnection sqlCn = new SqlConnection("Data Source=(local); User Id=scott; Password=tigger; Initial Catalog=Lab"))

   29         {

   30             SqlCommand cmd = new SqlCommand("INSERT INTO tblAccount (Account, Password, Username) VALUES (@acct, @pwd, @name)", sqlCn);

   31             cmd.Parameters.Add("@acct", SqlDbType.NVarChar).Value = "EMP" + DateTime.Now.ToString("HHmmss");

   32             cmd.Parameters.Add("@pwd", SqlDbType.NVarChar).Value = "PWD";

   33             cmd.Parameters.Add("@name", SqlDbType.NVarChar).Value = "NAME" + DateTime.Now.ToString("HHmmss");

   34             sqlCn.Open();

   35             cmd.ExecuteNonQuery();

   36         }

   37         //** 實測結果,ODP.NET 9207不Support TransactionScope,必須用.NET 2.0的System.Data.OracleClient

   38         using (OracleConnection oraCn = new OracleConnection("Data Source=MYORA; User Id=scott; Password=tigger;"))

   39         {

   40             OracleCommand cmd = new OracleCommand("INSERT INTO MYTABLE.tblAccount (Account, Password, Username) VALUES (:acct, :pwd, :name)", oraCn);

   41             cmd.Parameters.Add(":acct", OracleType.VarChar).Value = "EMP" + DateTime.Now.ToString("HHmmss");

   42             cmd.Parameters.Add(":pwd", OracleType.VarChar).Value = "PWD";

   43             cmd.Parameters.Add(":name", OracleType.VarChar).Value = "NAME" + DateTime.Now.ToString("HHmmss");

   44             oraCn.Open();

   45             cmd.ExecuteNonQuery();

   46         }

   47         //在scope.Complete();後才算Commit!

   48         scope.Complete();

   49     }

   50     catch (Exception ex)

   51     {

   52         //只要沒有scope.Complete(),先前的動作都會Rollback

   53         Response.Write(ex.Message);

   54     }

   55 }

很簡短吧? (有寫過ServicedComponent的人才能感受出它的好呀!!) 為了證實Transaction效果,我在48行設Breakpoint,則中斷未scope.Complete前,SQL的tblAccount會被鎖定無法SELECT,而ORACLE中則SELECT不到新增的資料(可見SQL用的是Lock大法,ORACLE專攻Snapshot);scope.Complete後,二者的新資料就都出現了。

同時,我還試了故意ORACLE新增動作失敗或不做scope.Complete(),則SQL的tblAccount的新增資料就不會出現。由此,可以驗證以上的程式的確實踐了分散式交易!! 而TransactionScope的確比.NET 1.x的ServicedComponent方便多了,大幅減少異質資料庫包成交易的複雜度。

我曾試著用ODP.NET 9207取代System.Data.OracleClient,測試結果顯示ODP.NET 9207無法參與TrasactionScope物件所建立的交易。後來找到Oracle的ODP.NET FAQ,提到了從ODP.NET 10.2.0.2.20起才支援.NET 2.0的System.Transactions,想用ODP.NET的人要留意。

最後還有一點要注意,如果你的SQL在遠端主機上,用的又是Windows 2003平台,則還有好幾個關節要打通。包含了MSDTC在Windows 2003 SP1上有些選項要調整。還有,你可能會連線失敗,並得到以下這類訊息:
Communication with the underlying transaction manager has failed.
The transaction has already been implicitly or explicitly committed or aborted.

經驗中多半是防火牆的傑作,我的私房解法是在Windows Firewall上開放MSDTC.EXE程式的所有對外連線,問題就可解決。微軟有篇專題文章,介紹MSDTC與Firewall間的愛恨情仇,有興趣的人也可以去挖挖寶。


Comments

# by Confusing

dear 版主, 可否請教您一個問題,目前使用TransactionScope來作交易,但是將資料的新修刪時,會出現「ora-24761交易被倒回」的錯誤,請問大概是什麼情況會出現這樣的錯誤呢? 謝謝!

# by Jeffrey

to Confusing, ora-24761是指ORACLE在Server端就Rollback了,而TransactionScope還試圖要Commit。 爬了一下文,有人是在大量Insert時遇到這種情況,而我想到的是你的資料庫操作中是否有用到Procedure? 是否在Procedure中有邏輯將Transaction給Rollback了?

# by 迷途羔羊

Dear大大您好: 可否請教一個問題,現在寫一支windows form的程式使用了TransactionScope來做交易,在windows form中去呼叫wcf1的service進行insert tableA的動作,和呼叫wcf2的service進行insertB的動作然後用TransactionScope包起來,insert tableA成功,insert tableB失敗,沒有完成Complete的動作,但是查看資料,結果tableA的資料已經寫進去並沒有rollback,請問一下問題可能會是甚麼? Try Using Scope As New TransactionScope Using client1 As new ServiceReference.ServiceClient() client1 .insertA(Aobj) End Using Using client2 As new AServiceReference.AServiceClient() client2 .insertB(BObj) End Using Scope.Complete() End Using MessageBox.Show("OK!") Catch ex As Exception MessageBox.Show(ex.Message) End Try

# by Jeffrey

to 迷途羔羊, WCF我沒玩過,看了你的Code胡亂說說,你參考看看: TransacationScope是透過同一個Process中對Transaction Context有感知能力的元件(如SqlClient.*),可以發現被包在TransactionScope中時,改用支援Transaction方式執行,所以第一是你用的DB連線物件要支援(例如ODP.NET 9.2就不行),第二是要在同一個Process內,去Call Web Service或遠端的服務當然也不行。這是我看到你的Code所聯想到的,不然就要請你再提供詳細點的資訊,請大家幫你看看。

# by Sulufei

WCF...我在WebService的說明有看到一段 WebService是Stateless,它本身只能是交易的根,意即它不能被其他交易包起來。

# by sand

可以請教你一下嗎? 我在.net 使用transactionscope時,有時候會發生transaction in doubt 的exception,而db兩邊的資料都成功的update了,並沒有rollback,請問如果用using transactionscope時,可以如何讓transaction rollback嗎? 我嘗試在exception發生時另外寫一段code把table資料update回去,但有時又會update失敗

# by Jeffrey

to sand, In Doubt發生的情境是在分散式交易中DTC已通知全部DB進行Commit,但未能收到所有DB執行成功的回應,有可能DB掛掉或是網路瞬斷造成(你的情況比較像後者)。 由TransactionScope的觀點,這個交易已經完成但DB沒有給回應,狀況生死未卜;有可能是DB Commit了沒給回應、也有可能是Rollback了沒給回應,兩種狀況都可能發生,因此需要人為介入處理,且需自行釐清資料狀態再決定處置方式。這裡有一篇不錯的討論: http://hectorcorrea.com/blog/transactionscope-complete-maybe

Post a comment