什么是數(shù)據(jù)庫(kù)事務(wù)
數(shù)據(jù)庫(kù)事務(wù)是指作為單個(gè)邏輯工作單元執(zhí)行的一系列操作。
設(shè)想網(wǎng)上購(gòu)物的一次交易,其付款過(guò)程至少包括以下幾步數(shù)據(jù)庫(kù)操作:
· 更新客戶所購(gòu)商品的庫(kù)存信息
· 保存客戶付款信息--可能包括與銀行系統(tǒng)的交互
· 生成訂單并且保存到數(shù)據(jù)庫(kù)中
· 更新用戶相關(guān)信息,例如購(gòu)物數(shù)量等等
正常的情況下,這些操作將順利進(jìn)行,最終交易成功,與交易相關(guān)的所有數(shù)據(jù)庫(kù)信息也成功地更新。但是,如果在這一系列過(guò)程中任何一個(gè)環(huán)節(jié)出了差錯(cuò),例如在更新商品庫(kù)存信息時(shí)發(fā)生異常、該顧客銀行帳戶存款不足等,都將導(dǎo)致交易失敗。一旦交易失敗,數(shù)據(jù)庫(kù)中所有信息都必須保持交易前的狀態(tài)不變,比如最后一步更新用戶信息時(shí)失敗而導(dǎo)致交易失敗,那么必須保證這筆失敗的交易不影響數(shù)據(jù)庫(kù)的狀態(tài)--庫(kù)存信息沒(méi)有被更新、用戶也沒(méi)有付款,訂單也沒(méi)有生成。否則,數(shù)據(jù)庫(kù)的信息將會(huì)一片混亂而不可預(yù)測(cè)。
數(shù)據(jù)庫(kù)事務(wù)正是用來(lái)保證這種情況下交易的平穩(wěn)性和可預(yù)測(cè)性的技術(shù)。
數(shù)據(jù)庫(kù)事務(wù)的ACID屬性
事務(wù)處理可以確保除非事務(wù)性單元內(nèi)的所有操作都成功完成,否則不會(huì)永久更新面向數(shù)據(jù)的資源。通過(guò)將一組相關(guān)操作組合為一個(gè)要么全部成功要么全部失敗的單元,可以簡(jiǎn)化錯(cuò)誤恢復(fù)并使應(yīng)用程序更加可靠。一個(gè)邏輯工作單元要成為事務(wù),必須滿足所謂的ACID(原子性、一致性、隔離性和持久性)屬性:
·
原子性
事務(wù)必須是原子工作單元;對(duì)于其數(shù)據(jù)修改,要么全都執(zhí)行,要么全都不執(zhí)行。通常,與某個(gè)事務(wù)關(guān)聯(lián)的操作具有共同的目標(biāo),并且是相互依賴的。如果系統(tǒng)只執(zhí)行這些操作的一個(gè)子集,則可能會(huì)破壞事務(wù)的總體目標(biāo)。原子性消除了系統(tǒng)處理操作子集的可能性。
·
一致性
事務(wù)在完成時(shí),必須使所有的數(shù)據(jù)都保持一致狀態(tài)。在相關(guān)數(shù)據(jù)庫(kù)中,所有規(guī)則都必須應(yīng)用于事務(wù)的修改,以保持所有數(shù)據(jù)的完整性。事務(wù)結(jié)束時(shí),所有的內(nèi)部數(shù)據(jù)結(jié)構(gòu)(如 B 樹索引或雙向鏈表)都必須是正確的。某些維護(hù)一致性的責(zé)任由應(yīng)用程序開發(fā)人員承擔(dān),他們必須確保應(yīng)用程序已強(qiáng)制所有已知的完整性約束。例如,當(dāng)開發(fā)用于轉(zhuǎn)帳的應(yīng)用程序時(shí),應(yīng)避免在轉(zhuǎn)帳過(guò)程中任意移動(dòng)小數(shù)點(diǎn)。
·
隔離性
由并發(fā)事務(wù)所作的修改必須與任何其它并發(fā)事務(wù)所作的修改隔離。事務(wù)查看數(shù)據(jù)時(shí)數(shù)據(jù)所處的狀態(tài),要么是另一并發(fā)事務(wù)修改它之前的狀態(tài),要么是另一事務(wù)修改它之后的狀態(tài),事務(wù)不會(huì)查看中間狀態(tài)的數(shù)據(jù)。這稱為可串行性,因?yàn)樗軌蛑匦卵b載起始數(shù)據(jù),并且重播一系列事務(wù),以使數(shù)據(jù)結(jié)束時(shí)的狀態(tài)與原始事務(wù)執(zhí)行的狀態(tài)相同。當(dāng)事務(wù)可序列化時(shí)將獲得最高的隔離級(jí)別。在此級(jí)別上,從一組可并行執(zhí)行的事務(wù)獲得的結(jié)果與通過(guò)連續(xù)運(yùn)行每個(gè)事務(wù)所獲得的結(jié)果相同。由于高度隔離會(huì)限制可并行執(zhí)行的事務(wù)數(shù),所以一些應(yīng)用程序降低隔離級(jí)別以換取更大的吞吐量。
·
持久性
事務(wù)完成之后,它對(duì)于系統(tǒng)的影響是永久性的。該修改即使出現(xiàn)致命的系統(tǒng)故障也將一直保持。
DBMS的責(zé)任和我們的任務(wù)
企業(yè)級(jí)的數(shù)據(jù)庫(kù)管理系統(tǒng)(DBMS)都有責(zé)任提供一種保證事務(wù)的物理完整性的機(jī)制。就常用的SQL Server2000系統(tǒng)而言,它具備鎖定設(shè)備隔離事務(wù)、記錄設(shè)備保證事務(wù)持久性等機(jī)制。因此,我們不必關(guān)心數(shù)據(jù)庫(kù)事務(wù)的物理完整性,而應(yīng)該關(guān)注在什么情況下使用數(shù)據(jù)庫(kù)事務(wù)、事務(wù)對(duì)性能的影響,如何使用事務(wù)等等。
本文將涉及到在.net框架下使用C#語(yǔ)言操縱數(shù)據(jù)庫(kù)事務(wù)的各個(gè)方面。
體驗(yàn)SQL語(yǔ)言的事務(wù)機(jī)制
作為大型的企業(yè)級(jí)數(shù)據(jù)庫(kù),SQL Server2000對(duì)事務(wù)提供了很好的支持。我們可以使用SQL語(yǔ)句來(lái)定義、提交以及回滾一個(gè)事務(wù)。
如下所示的SQL代碼定義了一個(gè)事務(wù),并且命名為"MyTransaction"(限于篇幅,本文并不討論如何編寫SQL語(yǔ)言程序,請(qǐng)讀者自行參考相關(guān)書籍):
DECLARE @TranName VARCHAR(20)
SELECT @TranName = 'MyTransaction'
BEGIN TRANSACTION @TranNameGOUSE pubs
GO
UPDATE roysched
SET royalty = royalty * 1.10
WHERE title_id LIKE 'Pc%'
GO
COMMIT TRANSACTION MyTransaction
GO |
這里用到了SQL Server2000自帶的示例數(shù)據(jù)庫(kù)pubs,提交事務(wù)后,將為所有暢銷計(jì)算機(jī)書籍支付的版稅增加 10%。
打開SQL Server2000的查詢分析器,選擇pubs數(shù)據(jù)庫(kù),然后運(yùn)行這段程序,結(jié)果顯而易見(jiàn)。
可是如何在C#程序中運(yùn)行呢?我們記得在普通的SQL查詢中,一般需要把查詢語(yǔ)句賦值給SalCommand.CommandText屬性,這里也就像普通的SQL查詢語(yǔ)句一樣,將這些語(yǔ)句賦給SqlCommand.CommandText屬性即可。要注意的一點(diǎn)是,其中的"GO"語(yǔ)句標(biāo)志著SQL批處理的結(jié)束,編寫SQL腳本是需要的,但是在這里是不必要的。我們可以編寫如下的程序來(lái)驗(yàn)證這個(gè)想法:
//TranSql.csusing System;
using System.Data;
using System.Data.SqlClient;
namespace Aspcn
{
public class DbTranSql
{
file://將事務(wù)放到SQL Server中執(zhí)行
public void DoTran()
{
file://建立連接并打開
SqlConnection myConn=GetConn();myConn.Open();
SqlCommand myComm=new SqlCommand();
try
{
myComm.Connection=myConn;
myComm.CommandText="DECLARE @TranName VARCHAR(20) ";
myComm.CommandText+="SELECT @TranName = 'MyTransaction' ";
myComm.CommandText+="BEGIN TRANSACTION @TranName ";
myComm.CommandText+="USE pubs ";
myComm.CommandText+="UPDATE roysched SET royalty = royalty * 1.10 WHERE title_id LIKE 'Pc%' ";
myComm.CommandText+="COMMIT TRANSACTION MyTransaction ";
myComm.ExecuteNonQuery();
}
catch(Exception err)
{
throw new ApplicationException("事務(wù)操作出錯(cuò),系統(tǒng)信息:"+err.Message);
}
finally
{
myConn.Close();
}
}
file://獲取數(shù)據(jù)連接
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()
{
DbTranSql tranTest=new DbTranSql();
tranTest.DoTran();
Console.WriteLine("事務(wù)處理已經(jīng)成功完成。");
Console.ReadLine();
}
}
} |
注意到其中的SqlCommand對(duì)象myComm,它的CommandText屬性僅僅是前面SQL代碼字符串連接起來(lái)即可,當(dāng)然,其中的"GO"語(yǔ)句已經(jīng)全部去掉了。這個(gè)語(yǔ)句就像普通的查詢一樣,程序?qū)QL文本事實(shí)上提交給DBMS去處理了,然后接收返回的結(jié)果(如果有結(jié)果返回的話)。
很自然,我們最后看到了輸出"事務(wù)處理已經(jīng)成功完成",再用企業(yè)管理器查看pubs數(shù)據(jù)庫(kù)的roysched表,所有title_id字段以"PC"開頭的書籍的royalty字段的值都增加了0.1倍。
這里,我們并沒(méi)有使用ADO.net的事務(wù)處理機(jī)制,而是簡(jiǎn)單地將執(zhí)行事務(wù)的SQL語(yǔ)句當(dāng)作普通的查詢來(lái)執(zhí)行,因此,事實(shí)上該事務(wù)完全沒(méi)有用到.net的相關(guān)特性。
了解.net中的事務(wù)機(jī)制
如你所知,在.net框架中主要有兩個(gè)命名空間(namespace)用于應(yīng)用程序同數(shù)據(jù)庫(kù)系統(tǒng)的交互:System.Data.SqlClient和System.Data.OleDb。前者專門用于連接Microsoft公司自己的SQL Server數(shù)據(jù)庫(kù),而后者可以適應(yīng)多種不同的數(shù)據(jù)庫(kù)。這兩個(gè)命名空間中都包含有專門用于管理數(shù)據(jù)庫(kù)事務(wù)的類,分別是System.Data.SqlClient.SqlTranscation類和System.Data.OleDb.OleDbTranscation類。
就像它們的名字一樣,這兩個(gè)類大部分功能是一樣的,二者之間的主要差別在于它們的連接機(jī)制,前者提供一組直接調(diào)用 SQL Server 的對(duì)象,而后者使用本機(jī) OLE DB 啟用數(shù)據(jù)訪問(wèn)。 事實(shí)上,ADO.net 事務(wù)完全在數(shù)據(jù)庫(kù)的內(nèi)部處理,且不受 Microsoft 分布式事務(wù)處理協(xié)調(diào)器 (DTC) 或任何其他事務(wù)性機(jī)制的支持。本文將主要介紹System.Data.SqlClient.SqlTranscation類,下面的段落中,除了特別注明,都將使用System.Data.SqlClient.SqlTranscation類。
事務(wù)的開啟和提交
現(xiàn)在我們對(duì)事務(wù)的概念和原理都了然于心了,并且作為已經(jīng)有一些基礎(chǔ)的C#開發(fā)者,我們已經(jīng)熟知編寫數(shù)據(jù)庫(kù)交互程序的一些要點(diǎn),即使用SqlConnection類的對(duì)象的Open()方法建立與數(shù)據(jù)庫(kù)服務(wù)器的連接,然后將該連接賦給SqlCommand對(duì)象的Connection屬性,將欲執(zhí)行的SQL語(yǔ)句賦給它的CommandText屬性,于是就可以通過(guò)SqlCommand對(duì)象進(jìn)行數(shù)據(jù)庫(kù)操作了。對(duì)于我們將要編寫的事務(wù)處理程序,當(dāng)然還需要定義一個(gè)SqlTransaction類型的對(duì)象。并且看到SqlCommand對(duì)象的Transcation屬性,我們很容易想到新建的SqlTransaction對(duì)象應(yīng)該與它關(guān)聯(lián)起來(lái)。
基于以上認(rèn)識(shí),下面我們就開始動(dòng)手寫我們的第一個(gè)事務(wù)處理程序。我們可以很熟練地寫出下面這一段程序:
//DoTran.csusing System;
using System.Data;
using System.Data.SqlClient;
namespace Aspcn
{
public class DbTran
{
file://執(zhí)行事務(wù)處理
public void DoTran()
{
file://建立連接并打開
SqlConnection myConn=GetConn();
myConn.Open();
SqlCommand myComm=new SqlCommand();
SqlTransaction myTran=new SqlTransaction();
try
{
myComm.Connection=myConn;
myComm.Transaction=myTran;
file://定位到pubs數(shù)據(jù)庫(kù)
myComm.CommandText="USE pubs";
myComm.ExecuteNonQuery();
file://更新數(shù)據(jù)
file://將所有的計(jì)算機(jī)類圖書
myComm.CommandText="UPDATE roysched SET royalty = royalty * 1.10 WHERE title_id LIKE 'Pc%'";
myComm.ExecuteNonQuery();//提交事務(wù)
myTran.Commit();
}
catch(Exception err)
{
throw new ApplicationException("事務(wù)操作出錯(cuò),系統(tǒng)信息:"+err.Message);
}
finally
{
myConn.Close();
}
}
file://獲取數(shù)據(jù)連接
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("事務(wù)處理已經(jīng)成功完成。");
Console.ReadLine();
}
}
} |
顯然,這個(gè)程序非常簡(jiǎn)單,我們非常自信地編譯它,但是,出乎意料的結(jié)果使我們的成就感頓時(shí)煙消云散:
error CS1501: 重載"SqlTransaction"方法未獲取"0"參數(shù)
是什么原因呢?注意到我們初始化的代碼:
SqlTransaction myTran=new SqlTransaction(); |
顯然,問(wèn)題出在這里,事實(shí)上,SqlTransaction類并沒(méi)有公共的構(gòu)造函數(shù),我們不能這樣新建一個(gè)SqlTrancaction類型的變量。在事務(wù)處理之前確實(shí)需要有一個(gè)SqlTransaction類型的變量,將該變量關(guān)聯(lián)到SqlCommand類的Transcation屬性也是必要的,但是初始化方法卻比較特別一點(diǎn)。在初始化SqlTransaction類時(shí),你需要使用SqlConnection類的BeginTranscation()方法:
SqlTransaction myTran; myTran=myConn.BeginTransaction(); |
該方法返回一個(gè)SqlTransaction類型的變量。在調(diào)用BeginTransaction()方法以后,所有基于該數(shù)據(jù)連接對(duì)象的SQL語(yǔ)句執(zhí)行動(dòng)作都將被認(rèn)為是事務(wù)MyTran的一部分。同時(shí),你也可以在該方法的參數(shù)中指定事務(wù)隔離級(jí)別和事務(wù)名稱,如:
SqlTransaction myTran;
myTran=myConn.BeginTransaction(IsolationLevel.ReadCommitted,"SampleTransaction"); |
關(guān)于隔離級(jí)別的概念我們將在隨后的內(nèi)容中探討,在這里我們只需牢記一個(gè)事務(wù)是如何被啟動(dòng),并且關(guān)聯(lián)到特定的數(shù)據(jù)鏈接的。
先不要急著去搞懂我們的事務(wù)都干了些什么,看到這一行:
是的,這就是事務(wù)的提交方式。該語(yǔ)句執(zhí)行后,事務(wù)的所有數(shù)據(jù)庫(kù)操作將生效,并且為數(shù)據(jù)庫(kù)事務(wù)的持久性機(jī)制所保持--即使系統(tǒng)在這以后發(fā)生致命錯(cuò)誤,該事務(wù)對(duì)數(shù)據(jù)庫(kù)的影響也不會(huì)消失。
對(duì)上面的程序做了修改之后我們可以得到如下代碼(為了節(jié)約篇幅,重復(fù)之處已省略,請(qǐng)參照前文):
//DoTran.cs……}
file://執(zhí)行事務(wù)處理
public void DoTran()
{
file://建立連接并打開
SqlConnection myConn=GetConn();
myConn.Open();
SqlCommand myComm=new SqlCommand();
file://SqlTransaction myTran=new SqlTransaction();
file://注意,SqlTransaction類無(wú)公開的構(gòu)造函數(shù)
SqlTransaction myTran;
file://創(chuàng)建一個(gè)事務(wù)
myTran=myConn.BeginTransaction();
try
{
file://從此開始,基于該連接的數(shù)據(jù)操作都被認(rèn)為是事務(wù)的一部分
file://下面綁定連接和事務(wù)對(duì)象
myComm.Connection=myConn;
myComm.Transaction=myTran; file://定位到pubs數(shù)據(jù)庫(kù)
myComm.CommandText="USE pubs";
myComm.ExecuteNonQuery();//更新數(shù)據(jù)
file://將所有的計(jì)算機(jī)類圖書
myComm.CommandText="UPDATE roysched SET royalty = royalty * 1.10 WHERE title_id LIKE 'Pc%'";
myComm.ExecuteNonQuery();
file://提交事務(wù)
myTran.Commit();
}
catch(Exception err)
{
throw new ApplicationException("事務(wù)操作出錯(cuò),系統(tǒng)信息:"+err.Message);
}
finally
{
myConn.Close();
}
}
…… |
到此為止,我們僅僅掌握了如何開始和提交事務(wù)。下一步我們必須考慮的是在事務(wù)中可以干什么和不可以干什么。
另一個(gè)走向極端的錯(cuò)誤
滿懷信心的新手們可能為自己所掌握的部分知識(shí)陶醉不已,剛接觸數(shù)據(jù)庫(kù)庫(kù)事務(wù)處理的準(zhǔn)開發(fā)者們也一樣,躊躇滿志地準(zhǔn)備將事務(wù)機(jī)制應(yīng)用到他的數(shù)據(jù)處理程序的每一個(gè)模塊每一條語(yǔ)句中去。的確,事務(wù)機(jī)制看起來(lái)是如此的誘人——簡(jiǎn)潔、美妙而又實(shí)用,我當(dāng)然想用它來(lái)避免一切可能出現(xiàn)的錯(cuò)誤——我甚至想用事務(wù)把我的數(shù)據(jù)操作從頭到尾包裹起來(lái)。
看著吧,下面我要從創(chuàng)建一個(gè)數(shù)據(jù)庫(kù)開始:
using System;
using System.Data;
using System.Data.SqlClient;
namespace Aspcn
{
public class DbTran
{
file://執(zhí)行事務(wù)處理
public void DoTran()
{
file://建立連接并打開
SqlConnection myConn=GetConn();
myConn.Open();
SqlCommand myComm=new SqlCommand();
SqlTransaction myTran;
myTran=myConn.BeginTransaction();
file://下面綁定連接和事務(wù)對(duì)象
myComm.Connection=myConn;
myComm.Transaction=myTran;
file://試圖創(chuàng)建數(shù)據(jù)庫(kù)TestDB
myComm.CommandText="CREATE database TestDB";
myComm.ExecuteNonQuery();
file://提交事務(wù)
myTran.Commit();
}
file://獲取數(shù)據(jù)連接
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("事務(wù)處理已經(jīng)成功完成。");
Console.ReadLine();
}
}
}
//--------------- |
未處理的異常: System.Data.SqlClient.SqlException: 在多語(yǔ)句事務(wù)內(nèi)不允許使用 CREATE DATABASE 語(yǔ)句。
at System.Data.SqlClient.SqlCommand.ExecuteNonQuery()
at Aspcn.DbTran.DoTran()
at Aspcn.Test.Main() |
注意,如下的SQL語(yǔ)句不允許出現(xiàn)在事務(wù)中:
ALTER DATABASE |
修改數(shù)據(jù)庫(kù) |
BACKUP LOG |
備份日志 |
CREATE DATABASE |
創(chuàng)建數(shù)據(jù)庫(kù) |
DISK INIT |
創(chuàng)建數(shù)據(jù)庫(kù)或事務(wù)日志設(shè)備 |
DROP DATABASE |
刪除數(shù)據(jù)庫(kù) |
DUMP TRANSACTION |
轉(zhuǎn)儲(chǔ)事務(wù)日志 |
LOAD DATABASE |
裝載數(shù)據(jù)庫(kù)備份復(fù)本 |
LOAD TRANSACTION |
裝載事務(wù)日志備份復(fù)本 |
RECONFIGURE |
更新使用 sp_configure 系統(tǒng)存儲(chǔ)過(guò)程更改的配置選項(xiàng)的當(dāng)前配置(sp_configure 結(jié)果集中的 config_value 列)值。 |
RESTORE DATABASE |
還原使用BACKUP命令所作的數(shù)據(jù)庫(kù)備份 |
RESTORE LOG |
還原使用BACKUP命令所作的日志備份 |
UPDATE STATISTICS |
在指定的表或索引視圖中,對(duì)一個(gè)或多個(gè)統(tǒng)計(jì)組(集合)有關(guān)鍵值分發(fā)的信息進(jìn)行更新 |
除了這些語(yǔ)句以外,你可以在你的數(shù)據(jù)庫(kù)事務(wù)中使用任何合法的SQL語(yǔ)句。
事務(wù)回滾
事務(wù)的四個(gè)特性之一是原子性,其含義是指對(duì)于特定操作序列組成的事務(wù),要么全部完成,要么就一件也不做。如果在事務(wù)處理的過(guò)程中,發(fā)生未知的不可預(yù)料的錯(cuò)誤,如何保證事務(wù)的原子性呢?當(dāng)事務(wù)中止時(shí),必須執(zhí)行回滾操作,以便消除已經(jīng)執(zhí)行的操作對(duì)數(shù)據(jù)庫(kù)的影響。
一般的情況下,在異常處理中使用回滾動(dòng)作是比較好的想法。前面,我們已經(jīng)得到了一個(gè)更新數(shù)據(jù)庫(kù)的程序,并且驗(yàn)證了它的正確性,稍微修改一下,可以得到:
//RollBack.cs
using System;
using System.Data;
using System.Data.SqlClient;
namespace Aspcn
{
public class DbTran
{
file://執(zhí)行事務(wù)處理
public void DoTran()
{
file://建立連接并打開
SqlConnection myConn=GetConn();
myConn.Open();
SqlCommand myComm=new SqlCommand();
SqlTransaction myTran;
file://創(chuàng)建一個(gè)事務(wù)
myTran=myConn.BeginTransaction();
file://從此開始,基于該連接的數(shù)據(jù)操作都被認(rèn)為是事務(wù)的一部分
file://下面綁定連接和事務(wù)對(duì)象
myComm.Connection=myConn;
myComm.Transaction=myTran;
try
{
file://定位到pubs數(shù)據(jù)庫(kù)
myComm.CommandText="USE pubs";
myComm.ExecuteNonQuery();
myComm.CommandText="UPDATE roysched SET royalty = royalty * 1.10 WHERE title_id LIKE 'Pc%'";
myComm.ExecuteNonQuery();
file://下面使用創(chuàng)建數(shù)據(jù)庫(kù)的語(yǔ)句制造一個(gè)錯(cuò)誤
myComm.CommandText="Create database testdb";
myComm.ExecuteNonQuery();
myComm.CommandText="UPDATE roysched SET royalty = royalty * 1.20 WHERE title_id LIKE 'Ps%'";
myComm.ExecuteNonQuery();
file://提交事務(wù)
myTran.Commit();
}
catch(Exception err)
{
myTran.Rollback();
Console.Write("事務(wù)操作出錯(cuò),已回滾。系統(tǒng)信息:"+err.Message);
}
}
file://獲取數(shù)據(jù)連接
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("事務(wù)處理已經(jīng)成功完成。");
Console.ReadLine();
}
}
} |
首先,我們?cè)谥虚g人為地制造了一個(gè)錯(cuò)誤——使用前面講過(guò)的Create database語(yǔ)句。然后,在異常處理的catch塊中有如下語(yǔ)句:
myTran.Rollback();
當(dāng)異常發(fā)生時(shí),程序執(zhí)行流跳轉(zhuǎn)到catch塊中,首先執(zhí)行的就是這條語(yǔ)句,它將當(dāng)前事務(wù)回滾。在這段程序可以看出,在Create database之前,已經(jīng)有了一個(gè)更新數(shù)據(jù)庫(kù)的操作——將pubs數(shù)據(jù)庫(kù)的roysched表中的所有title_id字段以“PC”開頭的書籍的royalty字段的值都增加0.1倍。但是,由于異常發(fā)生而導(dǎo)致的回滾使得對(duì)于數(shù)據(jù)庫(kù)來(lái)說(shuō)什么都沒(méi)有發(fā)生。由此可見(jiàn),Rollback()方法維護(hù)了數(shù)據(jù)庫(kù)的一致性及事務(wù)的原子性。
使用存儲(chǔ)點(diǎn)
事務(wù)只是一種最壞情況下的保障措施,事實(shí)上,平時(shí)系統(tǒng)的運(yùn)行可靠性都是相當(dāng)高的,錯(cuò)誤很少發(fā)生,因此,在每次事務(wù)執(zhí)行之前都檢查其有效性顯得代價(jià)太高——絕大多數(shù)的情況下這種耗時(shí)的檢查是不必要的。我們不得不想另外一種辦法來(lái)提高效率。
事務(wù)存儲(chǔ)點(diǎn)提供了一種機(jī)制,用于回滾部分事務(wù)。因此,我們可以不必在更新之前檢查更新的有效性,而是預(yù)設(shè)一個(gè)存儲(chǔ)點(diǎn),在更新之后,如果沒(méi)有出現(xiàn)錯(cuò)誤,就繼續(xù)執(zhí)行,否則回滾到更新之前的存儲(chǔ)點(diǎn)。存儲(chǔ)點(diǎn)的作用就在于此。要注意的是,更新和回滾代價(jià)很大,只有在遇到錯(cuò)誤的可能性很小,而且預(yù)先檢查更新的有效性的代價(jià)相對(duì)很高的情況下,使用存儲(chǔ)點(diǎn)才會(huì)非常有效。
使用.net框架編程時(shí),你可以非常簡(jiǎn)單地定義事務(wù)存儲(chǔ)點(diǎn)和回滾到特定的存儲(chǔ)點(diǎn)。下面的語(yǔ)句定義了一個(gè)存儲(chǔ)點(diǎn)“NoUpdate”:
myTran.Save("NoUpdate");
當(dāng)你在程序中創(chuàng)建同名的存儲(chǔ)點(diǎn)時(shí),新創(chuàng)建的存儲(chǔ)點(diǎn)將替代原有的存儲(chǔ)點(diǎn)。
在回滾事務(wù)時(shí),只需使用Rollback()方法的一個(gè)重載函數(shù)即可:
myTran.Rollback("NoUpdate");
下面這段程序說(shuō)明了回滾到存儲(chǔ)點(diǎn)的方法和時(shí)機(jī):
using System;
using System.Data;
using System.Data.SqlClient;
namespace Aspcn
{
public class DbTran
{
file://執(zhí)行事務(wù)處理
public void DoTran()
{
file://建立連接并打開
SqlConnection myConn=GetConn();
myConn.Open();
SqlCommand myComm=new SqlCommand();
SqlTransaction myTran;
file://創(chuàng)建一個(gè)事務(wù)
myTran=myConn.BeginTransaction();
file://從此開始,基于該連接的數(shù)據(jù)操作都被認(rèn)為是事務(wù)的一部分
file://下面綁定連接和事務(wù)對(duì)象
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://提交事務(wù)
myTran.Commit();
}
catch(Exception err)
{
file://更新錯(cuò)誤,回滾到指定存儲(chǔ)點(diǎn)
myTran.Rollback("NoUpdate");
throw new ApplicationException("事務(wù)操作出錯(cuò),系統(tǒng)信息:"+err.Message);
}
}
file://獲取數(shù)據(jù)連接
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("事務(wù)處理已經(jīng)成功完成。");
Console.ReadLine();
}
}
}
|
很明顯,在這個(gè)程序中,更新無(wú)效的幾率是非常小的,而且在更新前驗(yàn)證其有效性的代價(jià)相當(dāng)高,因此我們無(wú)須在更新之前驗(yàn)證其有效性,而是結(jié)合事務(wù)的存儲(chǔ)點(diǎn)機(jī)制,提供了數(shù)據(jù)完整性的保證。
隔離級(jí)別的概念
企業(yè)級(jí)的數(shù)據(jù)庫(kù)每一秒鐘都可能應(yīng)付成千上萬(wàn)的并發(fā)訪問(wèn),因而帶來(lái)了并發(fā)控制的問(wèn)題。由數(shù)據(jù)庫(kù)理論可知,由于并發(fā)訪問(wèn),在不可預(yù)料的時(shí)刻可能引發(fā)如下幾個(gè)可以預(yù)料的問(wèn)題:
臟讀:包含未提交數(shù)據(jù)的讀取。例如,事務(wù)1 更改了某行。事務(wù)2 在事務(wù)1 提交更改之前讀取已更改的行。如果事務(wù)1 回滾更改,則事務(wù)2 便讀取了邏輯上從未存在過(guò)的行。
不可重復(fù)讀取:當(dāng)某個(gè)事務(wù)不止一次讀取同一行,并且一個(gè)單獨(dú)的事務(wù)在兩次(或多次)讀取之間修改該行時(shí),因?yàn)樵谕粋€(gè)事務(wù)內(nèi)的多次讀取之間修改了該行,所以每次讀取都生成不同值,從而引發(fā)不一致問(wèn)題。
幻象:通過(guò)一個(gè)任務(wù),在以前由另一個(gè)尚未提交其事務(wù)的任務(wù)讀取的行的范圍中插入新行或刪除現(xiàn)有行。帶有未提交事務(wù)的任務(wù)由于該范圍中行數(shù)的更改而無(wú)法重復(fù)其原始讀取。
如你所想,這些情況發(fā)生的根本原因都是因?yàn)樵诓l(fā)訪問(wèn)的時(shí)候,沒(méi)有一個(gè)機(jī)制避免交叉存取所造成的。而隔離級(jí)別的設(shè)置,正是為了避免這些情況的發(fā)生。事務(wù)準(zhǔn)備接受不一致數(shù)據(jù)的級(jí)別稱為隔離級(jí)別。隔離級(jí)別是一個(gè)事務(wù)必須與其它事務(wù)進(jìn)行隔離的程度。較低的隔離級(jí)別可以增加并發(fā),但代價(jià)是降低數(shù)據(jù)的正確性。相反,較高的隔離級(jí)別可以確保數(shù)據(jù)的正確性,但可能對(duì)并發(fā)產(chǎn)生負(fù)面影響。
根據(jù)隔離級(jí)別的不同,DBMS為并行訪問(wèn)提供不同的互斥保證。在SQL Server數(shù)據(jù)庫(kù)中,提供四種隔離級(jí)別:未提交讀、提交讀、可重復(fù)讀、可串行讀。這四種隔離級(jí)別可以不同程度地保證并發(fā)的數(shù)據(jù)完整性:
隔離級(jí)別 |
臟 讀 |
不可重復(fù)讀取 |
幻 像 |
未提交讀 |
是 |
是 |
是 |
提交讀 |
否 |
是 |
是 |
可重復(fù)讀 |
否 |
否 |
是 |
可串行讀 |
否 |
否 |
否 |
可以看出,“可串行讀”提供了最高級(jí)別的隔離,這時(shí)并發(fā)事務(wù)的執(zhí)行結(jié)果將與串行執(zhí)行的完全一致。如前所述,最高級(jí)別的隔離也就意味著最低程度的并發(fā),因此,在此隔離級(jí)別下,數(shù)據(jù)庫(kù)的服務(wù)效率事實(shí)上是比較低的。盡管可串行性對(duì)于事務(wù)確保數(shù)據(jù)庫(kù)中的數(shù)據(jù)在所有時(shí)間內(nèi)的正確性相當(dāng)重要,然而許多事務(wù)并不總是要求完全的隔離。例如,多個(gè)作者工作于同一本書的不同章節(jié)。新章節(jié)可以在任意時(shí)候提交到項(xiàng)目中。但是,對(duì)于已經(jīng)編輯過(guò)的章節(jié),沒(méi)有編輯人員的批準(zhǔn),作者不能對(duì)此章節(jié)進(jìn)行任何更改。這樣,盡管有未編輯的新章節(jié),但編輯人員仍可以確保在任意時(shí)間該書籍項(xiàng)目的正確性。編輯人員可以查看以前編輯的章節(jié)以及最近提交的章節(jié)。這樣,其它的幾種隔離級(jí)別也有其存在的意義。
在.net框架中,事務(wù)的隔離級(jí)別是由枚舉System.Data.IsolationLevel所定義的:
[Flags]
[Serializable]
public enum IsolationLevel |
其成員及相應(yīng)的含義如下:
成 員 |
含 義 |
Chaos |
無(wú)法改寫隔離級(jí)別更高的事務(wù)中的掛起的更改。 |
ReadCommitted |
在正在讀取數(shù)據(jù)時(shí)保持共享鎖,以避免臟讀,但是在事務(wù)結(jié)束之前可以更改數(shù)據(jù),從而導(dǎo)致不可重復(fù)的讀取或幻像數(shù)據(jù)。 |
ReadUncommitted |
可以進(jìn)行臟讀,意思是說(shuō),不發(fā)布共享鎖,也不接受獨(dú)占鎖。 |
RepeatableRead |
在查詢中使用的所有數(shù)據(jù)上放置鎖,以防止其他用戶更新這些數(shù)據(jù)。防止不可重復(fù)的讀取,但是仍可以有幻像行。 |
Serializable |
在DataSet上放置范圍鎖,以防止在事務(wù)完成之前由其他用戶更新行或向數(shù)據(jù)集中插入行。 |
Unspecified |
正在使用與指定隔離級(jí)別不同的隔離級(jí)別,但是無(wú)法確定該級(jí)別。
|
顯而意見(jiàn),數(shù)據(jù)庫(kù)的四個(gè)隔離級(jí)別在這里都有映射。
默認(rèn)的情況下,SQL Server使用ReadCommitted(提交讀)隔離級(jí)別。
關(guān)于隔離級(jí)別的最后一點(diǎn)就是如果你在事務(wù)執(zhí)行的過(guò)程中改變了隔離級(jí)別,那么后面的命名都在最新的隔離級(jí)別下執(zhí)行——隔離級(jí)別的改變是立即生效的。有了這一點(diǎn),你可以在你的事務(wù)中更靈活地使用隔離級(jí)別從而達(dá)到更高的效率和并發(fā)安全性。
忠告
無(wú)疑,引入事務(wù)處理是應(yīng)對(duì)可能出現(xiàn)的數(shù)據(jù)錯(cuò)誤的好方法,但是也應(yīng)該看到事務(wù)處理需要付出的巨大代價(jià)——用于存儲(chǔ)點(diǎn)、回滾和并發(fā)控制所需要的CPU時(shí)間和存儲(chǔ)空間。
摘自:http://hi.baidu.com/bolove/blog/item/b2397f1e477b3ef41ad5763f.html