滿懷信心的新手們可能為自己所掌握的部分知識陶醉不已,剛接觸資料庫庫交易處理的准開發人員們也一樣,躊躇滿志地準備將事務機制應用到他的資料處理程式的每一個模組每一條語句中去。的確,事務機制看起來是如此的誘人——簡潔、美妙而又實用,我當然想用它來避免一切可能出現的錯誤——我甚至想用事務把我的資料操作從頭到尾包裹起來。
看著吧,下面我要從建立一個資料庫開始:
using System; using System.Data; using System.Data.SqlClient; namespace Aspcn { public class DbTran { file://執行交易處理 public void DoTran() { file://建立串連並開啟 SqlConnection myConn=GetConn(); myConn.Open(); SqlCommand myComm=new SqlCommand(); SqlTransaction myTran; myTran=myConn.BeginTransaction(); file://下面綁定串連和事務對象 myComm.Connection=myConn; myComm.Transaction=myTran; file://試圖建立資料庫TestDB myComm.CommandText="CREATE database TestDB"; myComm.ExecuteNonQuery(); file://提交事務 myTran.Commit(); } file://擷取資料連線 private SqlConnection GetConn() { string strSql="Data Source=localhost;Integrated Security=SSPI;user id=sa;password="; SqlConnection myConn=new SqlConnection(strSql); return myConn; } } public class Test { public static void Main() { DbTran tranTest=new DbTran(); tranTest.DoTran(); Console.WriteLine("交易處理已經成功完成。"); Console.ReadLine(); } } } //--------------- |
未處理的異常: System.Data.SqlClient.SqlException: 在多語句事務內不允許使用 CREATE DATABASE 語句。
at System.Data.SqlClient.SqlCommand.ExecuteNonQuery() at Aspcn.DbTran.DoTran() at Aspcn.Test.Main() |
注意,如下的SQL語句不允許出現在事務中:
ALTER DATABASE |
修改資料庫 |
BACKUP LOG |
備份日誌 |
CREATE DATABASE |
建立資料庫 |
DISK INIT |
建立資料庫或交易記錄裝置 |
DROP DATABASE |
刪除資料庫 |
DUMP TRANSACTION |
轉儲交易記錄 |
LOAD DATABASE |
裝載Database Backup複本 |
LOAD TRANSACTION |
裝載交易記錄備份複本 |
RECONFIGURE |
更新使用 sp_configure 系統預存程序更改的配置選項的當前配置(sp_configure 結果集中的 config_value 列)值。 |
RESTORE DATABASE |
還原使用BACKUP命令所作的Database Backup |
RESTORE LOG |
還原使用BACKUP命令所作的記錄備份 |
UPDATE STATISTICS |
在指定的表或索引檢視表中,對一個或多個統計組(集合)有關索引值分發的資訊進行更新 |
除了這些語句以外,你可以在你的資料庫事務中使用任何合法的SQL語句。
交易回復事務的四個特性之一是原子性,其含義是指對於特定操作序列組成的事務,要麼全部完成,要麼就一件也不做。如果在交易處理的過程中,發生未知的不可預料的錯誤,如何保證事務的原子性呢?當事務中止時,必須執行復原操作,以便消除已經執行的操作對資料庫的影響。
一般的情況下,在異常處理中使用復原動作是比較好的想法。前面,我們已經得到了一個更新資料庫的程式,並且驗證了它的正確性,稍微修改一下,可以得到:
//RollBack.cs using System; using System.Data; using System.Data.SqlClient; namespace Aspcn { public class DbTran { file://執行交易處理 public void DoTran() { file://建立串連並開啟 SqlConnection myConn=GetConn(); myConn.Open(); SqlCommand myComm=new SqlCommand(); SqlTransaction myTran; file://建立一個事務 myTran=myConn.BeginTransaction(); file://從此開始,基於該串連的資料操作都被認為是事務的一部分 file://下面綁定串連和事務對象 myComm.Connection=myConn; myComm.Transaction=myTran; try { file://定位到pubs資料庫 myComm.CommandText="USE pubs"; myComm.ExecuteNonQuery(); myComm.CommandText="UPDATE roysched SET royalty = royalty * 1.10 WHERE title_id LIKE 'Pc%'"; myComm.ExecuteNonQuery(); file://下面使用建立資料庫的語句製造一個錯誤 myComm.CommandText="Create database testdb"; myComm.ExecuteNonQuery(); myComm.CommandText="UPDATE roysched SET royalty = royalty * 1.20 WHERE title_id LIKE 'Ps%'"; myComm.ExecuteNonQuery(); file://提交事務 myTran.Commit(); } catch(Exception err) { myTran.Rollback(); Console.Write("事務操作出錯,已復原。系統資訊:"+err.Message); } } file://擷取資料連線 private SqlConnection GetConn() { string strSql="Data Source=localhost;Integrated Security=SSPI;user id=sa;password="; SqlConnection myConn=new SqlConnection(strSql); return myConn; } } public class Test { public static void Main() { DbTran tranTest=new DbTran(); tranTest.DoTran(); Console.WriteLine("交易處理已經成功完成。"); Console.ReadLine(); } } } |
首先,我們在中間人為地製造了一個錯誤——使用前面講過的Create database語句。然後,在異常處理的catch塊中有如下語句:
myTran.Rollback();
當異常發生時,程式執行流跳轉到catch塊中,首先執行的就是這條語句,它將當前交易回復。在這段程式可以看出,在Create database之前,已經有了一個更新資料庫的操作——將pubs資料庫的roysched表中的所有title_id欄位以“PC”開頭的書籍的royalty欄位的值都增加0.1倍。但是,由於異常發生而導致的復原使得對於資料庫來說什麼都沒有發生。由此可見,Rollback()方法維護了資料庫的一致性及事務的原子性。
使用儲存點事務只是一種最壞情況下的保障措施,事實上,平時系統的運行可靠性都是相當高的,錯誤很少發生,因此,在每次事務執行之前都檢查其有效性顯得代價太高——絕大多數的情況下這種耗時的檢查是不必要的。我們不得不想另外一種辦法來提高效率。
事務儲存點提供了一種機制,用於復原部分事務。因此,我們可以不必在更新之前檢查更新的有效性,而是預設一個儲存點,在更新之後,如果沒有出現錯誤,就繼續執行,否則復原到更新之前的儲存點。儲存點的作用就在於此。要注意的是,更新和復原代價很大,只有在遇到錯誤的可能性很小,而且預先檢查更新的有效性的代價相對很高的情況下,使用儲存點才會非常有效。
使用.net架構編程時,你可以非常簡單地定義事務儲存點和復原到特定的儲存點。下面的語句定義了一個儲存點“NoUpdate”:
myTran.Save("NoUpdate");
當你在程式中建立同名的儲存點時,新建立的儲存點將替代原有的儲存點。
在復原事務時,只需使用Rollback()方法的一個重載函數即可:
myTran.Rollback("NoUpdate");
下面這段程式說明了復原到儲存點的方法和時機:
using System; using System.Data; using System.Data.SqlClient; namespace Aspcn { public class DbTran { file://執行交易處理 public void DoTran() { file://建立串連並開啟 SqlConnection myConn=GetConn(); myConn.Open(); SqlCommand myComm=new SqlCommand(); SqlTransaction myTran; file://建立一個事務 myTran=myConn.BeginTransaction(); file://從此開始,基於該串連的資料操作都被認為是事務的一部分 file://下面綁定串連和事務對象 myComm.Connection=myConn; myComm.Transaction=myTran; try { myComm.CommandText="use pubs"; myComm.ExecuteNonQuery(); myTran.Save("NoUpdate"); myComm.CommandText="UPDATE roysched SET royalty = royalty * 1.10 WHERE title_id LIKE 'Pc%'"; myComm.ExecuteNonQuery(); file://提交事務 myTran.Commit(); } catch(Exception err) { file://更新錯誤,復原到指定儲存點 myTran.Rollback("NoUpdate"); throw new ApplicationException("事務操作出錯,系統資訊:"+err.Message); } } file://擷取資料連線 private SqlConnection GetConn() { string strSql="Data Source=localhost;Integrated Security=SSPI;user id=sa;password="; SqlConnection myConn=new SqlConnection(strSql); return myConn; } } public class Test { public static void Main() { DbTran tranTest=new DbTran(); tranTest.DoTran(); Console.WriteLine("交易處理已經成功完成。"); Console.ReadLine(); } } } |
很明顯,在這個程式中,更新無效的幾率是非常小的,而且在更新前驗證其有效性的代價相當高,因此我們無須在更新之前驗證其有效性,而是結合事務的儲存點機制,提供了資料完整性的保證。
隔離等級的概念企業級的資料庫每一秒鐘都可能應付成千上萬的並發訪問,因而帶來了並發控制的問題。由資料庫理論可知,由於並發訪問,在不可預料的時刻可能引發如下幾個可以預料的問題:
髒讀:包含未提交資料的讀取。例如,事務1 更改了某行。事務2 在事務1 提交更改之前讀取已更改的行。如果事務1 復原更改,則事務2 便讀取了邏輯上從未存在過的行。
不可重複讀取:當某個事務不止一次讀取同一行,並且一個單獨的事務在兩次(或多次)讀取之間修改該行時,因為在同一個事務內的多次讀取之間修改了該行,所以每次讀取都產生不同值,從而引發不一致問題。
幻象:通過一個任務,在以前由另一個尚未提交其事務的任務讀取的行的範圍中插入新行或刪除現有行。帶有未提交事務的任務由於該範圍中行數的更改而無法重複其原始讀取。
如你所想,這些情況發生的根本原因都是因為在並發訪問的時候,沒有一個機制避免交叉存取所造成的。而隔離等級的設定,正是為了避免這些情況的發生。事務準備接受不一致資料的層級稱為隔離等級。隔離等級是一個事務必須與其它事務進行隔離的程度。較低的隔離等級可以增加並發,但代價是降低資料的正確性。相反,較高的隔離等級可以確保資料的正確性,但可能對並發產生負面影響。
根據隔離等級的不同,DBMS為並行訪問提供不同的互斥保證。在SQL Server資料庫中,提供四種隔離等級:未提交讀、提交讀、可重複讀、可串列讀。這四種隔離等級可以不同程度地保證並發的資料完整性:
隔離等級 |
髒 讀 |
不可重複讀取 |
幻 像 |
未提交讀 |
是 |
是 |
是 |
提交讀 |
否 |
是 |
是 |
可重複讀 |
否 |
否 |
是 |
可串列讀 |
否 |
否 |
否 |
可以看出,“可串列讀”提供了最進階別的隔離,這時並發事務的執行結果將與串列執行的完全一致。如前所述,最進階別的隔離也就意味著最低程度的並發,因此,在此隔離等級下,資料庫的服務效率事實上是比較低的。儘管可串列性對於事務確保資料庫中的資料在所有時間內的正確性相當重要,然而許多事務並不總是要求完全的隔離。例如,多個作者工作於同一本書的不同章節。新章節可以在任意時候提交到項目中。但是,對於已經編輯過的章節,沒有編輯人員的批准,作者不能對此章節進行任何更改。這樣,儘管有未編輯的新章節,但編輯人員仍可以確保在任意時間該書籍項目的正確性。編輯人員可以查看以前編輯的章節以及最近提交的章節。這樣,其它的幾種隔離等級也有其存在的意義。
在.net架構中,事務的隔離等級是由枚舉System.Data.IsolationLevel所定義的:
[Flags] [Serializable] public enum IsolationLevel |
其成員及相應的含義如下:
成 員含 義Chaos無法改寫隔離等級更高的事務中的暫止的變更。在正在讀取資料時保持共用鎖定,以避免髒讀,但是在事務結束之前可以更改資料,從而導致不可重複的讀取或幻像資料。可以進行髒讀,意思是說,不發布共用鎖定,也不接受獨佔鎖。在查詢中使用的所有資料上放置鎖,以防止其他使用者更新這些資料。防止不可重複的讀取,但是仍可以有幻像行。在DataSet上放置範圍鎖,以防止在事務完成之前由其他使用者更新行或向資料集中插入行。正在使用與指定隔離等級不同的隔離等級,但是無法確定該層級。
顯而意見,資料庫的四個隔離等級在這裡都有映射。
預設的情況下,SQL Server使用ReadCommitted(提交讀)隔離等級。
關於隔離等級的最後一點就是如果你在事務執行的過程中改變了隔離等級,那麼後面的命名都在最新的隔離等級下執行——隔離等級的改變是立即生效的。有了這一點,你可以在你的事務中更靈活地使用隔離等級從而達到更高的效率和並發安全性。
最後的忠告 無疑,引入交易處理是應對可能出現的資料錯誤的好方法,但是也應該看到交易處理需要付出的巨大代價——用於儲存點、復原和並發控制所需要的CPU時間和儲存空間。
本文的內容只是針對Microsoft SQL Server資料庫的,對應於.net架構中的System.Data.SqlClient命名空間,對於使用OleDb的情形,具體的實現稍有不同,但這不是本文的內容,有興趣的讀者可以到.net中華網(www.aspcn.com)的論壇裡找到答案。