另一個走向極端的錯誤
??滿懷信心的新手們可能為自己所掌握的部分知識陶醉不已,剛接觸數據庫庫事務處理的準開發者們也一樣,躊躇滿志地準備將事務機制應用到他的數據處理程序的每一個模塊每一條語句中去。的確,事務機制看起來是如此的誘人——簡潔、美妙而又實用,我當然想用它來避免一切可能出現的錯誤——我甚至想用事務把我的數據操作從頭到尾包裹起來。
??看著吧,下面我要從創建一個數據庫開始:
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 | 裝載數據庫備份復本 |
LOAD TRANSACTION | 裝載事務日志備份復本 |
RECONFIGURE | 更新使用 sp_configure 系統存儲過程更改的配置選項的當前配置(sp_configure 結果集中的 config_value 列)值。 |
RESTORE 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 | 無法改寫隔離級別更高的事務中的掛起的更改。 |
ReadCommitted | 在正在讀取數據時保持共享鎖,以避免臟讀,但是在事務結束之前可以更改數據,從而導致不可重復的讀取或幻像數據。 |
ReadUncommitted | 可以進行臟讀,意思是說,不發布共享鎖,也不接受獨占鎖。 |
RepeatableRead | 在查詢中使用的所有數據上放置鎖,以防止其他用戶更新這些數據。防止不可重復的讀取,但是仍可以有幻像行。 |
Serializable | 在DataSet上放置范圍鎖,以防止在事務完成之前由其他用戶更新行或向數據集中插入行。 |
Unspecified | 正在使用與指定隔離級別不同的隔離級別,但是無法確定該級別。 |
??顯而意見,數據庫的四個隔離級別在這里都有映射。
??默認的情況下,SQL Server使用ReadCommitted(提交讀)隔離級別。
??關于隔離級別的最后一點就是如果你在事務執行的過程中改變了隔離級別,那么后面的命名都在最新的隔離級別下執行——隔離級別的改變是立即生效的。有了這一點,你可以在你的事務中更靈活地使用隔離級別從而達到更高的效率和并發安全性。
最后的忠告
??無疑,引入事務處理是應對可能出現的數據錯誤的好方法,但是也應該看到事務處理需要付出的巨大代價——用于存儲點、回滾和并發控制所需要的CPU時間和存儲空間。
??本文的內容只是針對Microsoft SQL Server數據庫的,對應于.net框架中的System.Data.SqlClient命名空間,對于使用OleDb的情形,具體的實現稍有不同,但這不是本文的內容,有興趣的讀者可以到.net中華網(www.aspcn.com)的論壇里找到答案。