第10章事務管理與併發控制程序員
Ø 事務(Transaction)是構成單一邏輯工做單元的數據庫操做序列。這些操做是一個統一的總體,要麼所有成功執行(執行結果寫到物理數據文件),要麼所有不執行(執行結果沒有寫到任何的物理數據文件)。也能夠這樣理解,事務是若干操做語句的序列,這些語句序列要麼所有成功執行,要麼所有都不執行。所有不執行的狀況是:在執行到這些語句序列中的某一條語句時,因爲某種緣由(如斷電、磁盤故障等)而致使該語句執行失敗,這時將撤銷在該語句以前已經執行的語句所產生的結果,使數據庫恢復到執行這些語句序列以前的狀態。數據庫
【例子】對於銀行轉賬問題,能夠表述爲:將賬戶A1上的金額x轉到賬戶A2。這個操做過程能夠用如圖10.1所示的流程表示。安全
• 若是轉賬程序在恰好執行完操做③的時刻出現硬件故障,並由此致使程序運行中斷,那麼數據庫就處於這樣的狀態:賬號A1中已經被扣除金額x(轉出部分),而賬號A2並無增長相應的金額x。也就是說,已經從賬號A1上轉出金額x,但賬號A2並無收到這批錢。顯然,這種狀況在實際應用決不容許出現。架構
• 若是將上述操做①至⑤定義爲一個事務,因爲事務中的操做要麼全都執行,要麼全都不執行,那麼就能夠避免出現上述錯誤的狀態。這就是事務的魅力。併發
Ø 做爲一種特殊的數據庫操做序列,事務的主要特性體現如下四個方面:post
(1)原子性(Atomicity)性能
事務是數據庫操做的邏輯工做單位。就操做而言,事務中的操做是一個總體,不能再被分割,要麼所有成功執行,要麼所有不成功執行。spa
(2)一致性(Consistency)3d
事務的一致性是指出事務執行先後都可以保持數據庫狀態的一致性,即事務的執行結果是將數據庫從一個一致狀態轉變爲另外一個一致狀態。日誌
Ø 實際上,事務的一致性和原子性是密切相關的。
Ø 對於前面轉賬的例子,當操做操做③被執行後,出於某種客觀緣由而致使操做④不能被執行時,若是操做③和④都是同一個事務中的操做,那麼因爲事務具備原子性,因此操做①、②和③執行的結果也自動被取消,這樣數據庫就回到執行操做①前的狀態,從而保持數據庫的一致性。
Ø 數據庫的一致性狀態除了取決於事務的一致性之外,還要求在事務開始執行時的數據庫狀態也必須一致的。不然就算事務具備一致性,但在執行該事務後並不必定可以保持數據庫狀態的一致性。
(3)隔離性(Isolation)
隔離性是指多個事務在執行時不相互干擾的一種特性。事務的隔離性意味着一個事務的內部操做及其使用的數據對其餘事務是不透明的,其餘事務感受不到這些操做和數據的存在,更不會干擾這些操做和數據。也就是說,事務的隔離性使系統中的每一個事務都感受到「只有本身在工做」,而感受不到系統中還有其餘事務在併發執行,
(4)持久性(Durability)
持久性或稱永久性(Permanence),是指一個事務一旦成功提交,其結果對數據庫的改變將是永久的,即便是出現系統故障等問題。
事務的這四個特性一般被稱爲事務的ACID特性。一個數據庫管理系統及其併發控制機制應該能確保這些特性不遭到破壞。
Ø 啓動事務方式有三種:顯式啓動、自動提交和隱式啓動。
1. 顯式啓動
顯式啓動是以BEGIN TRANSACTION命令開始的,即當執行到該語句的時SQL Server將認爲這是一個事務的起點。
BEGIN TRANSACTION的語法以下:
BEGIN { TRAN | TRANSACTION } [ { transaction_name | @tran_name_variable } [ WITH MARK [ 'description' ] ] ] [ ; ]
u 其參數意義以下:
Ø transaction_name | @tran_name_variable
指定事務的名稱,能夠用變量提供名稱。該項是可選項。若是是事務是嵌套的,則僅在最外面的BEGIN...COMMIT或BEGIN...ROLLBACK嵌套語句對中使用事務名。
Ø WITH MARK [ 'description' ]
指定在日誌中標記事務。description 是描述該標記的字符串。若是使用了WITH MARK,則必須指定事務名。WITH MARK容許將事務日誌還原到命名標記。
顯式啓動的事務一般稱爲顯式事務。本章介紹的主要是顯式事務。
2. 自動提交
Ø 自動提交是指用戶每發出一條SQL語句,SQL Server會自動啓動一個事務,語句執行完了之後SQL Server自動執行提交操做來提交該事務。也就是說,在自動提交方式下,每一條SQL語句就是一個事務,一般稱爲自動提交事務,這是SQL Server的默認模式。
Ø CREATE TABLE語句是一個事務,所以不可能出現這樣的狀況:在執行該語句時,有的字段被建立而有的沒有被建立。
3. 隱式啓動
Ø 當將SIMPLICIT_TRANSACTIONS設置爲ON時,表示將隱式事務模式設置爲打開,設置語句以下:
SET IMPLICIT_TRANSACTIONS ON;
Ø 在隱式事務模式下,任何DML語句(DELETE、UPDATE、INSERT)都自動啓動一個事務,直到遇到事務提交語句或事務回滾語句,該事務才結束。結束後,自動啓動新的事務,而無需用BEGIN TRANSACTION描述事務的開始。隱式啓動的事務一般稱爲隱性事務。在隱性事務模式生下,事務會造成連續的事務鏈。
Ø 若是已將IMPLICIT_TRANSACTIONS設置爲ON,建議隨時將之設置回OFF。另外,事務的結束是使用COMMIT或ROLLBACK語句來實現,這將在下一節介紹。
Ø 有啓動,就必有終止。
Ø 終止方法有兩種,一種是使用COMMIT命令(提交命令),另外一種是使用ROLLBACK命令(回滾命令)。這兩種方法有本質上的區別:當執行到COMMIT命令時,會將語句執行的結果保存到數據庫中(提交事務),並終止事務;當執行到ROLLBACK命令時,數據庫將返回到事務開始時的初始狀態,並終止事務。若是ROLLBACK命令是採用ROLLBACK TRANSACTION savepoint_name時,則數據庫將返回到savepoint_name標識的狀態。
1. 提交事務——COMMIT TRANSACTION
Ø 執行COMMIT TRANSACTION語句時,將終止隱式啓動或顯式啓動的事務。
ü 若是@@TRANCOUNT爲1,COMMIT TRANSACTION使得自從事務開始以來所執行的全部數據修改爲爲數據庫的永久部分,釋放事務所佔用的資源,並將@@TRANCOUNT減小到0。
ü 若是@@TRANCOUNT大於1,則COMMIT TRANSACTION使@@TRANCOUNT按1遞減而且事務將保持活動狀態。
Ø COMMIT TRANSACTION語句的語法以下:
COMMIT { TRAN | TRANSACTION } [ transaction_name | @tran_name_variable ] ] [ ; ]
其中,transaction_name | @tran_name_variable用於設置要結束的事務的名稱(該名稱是由BEGIN TRANSACTION語句指定),但SQL Server會忽略此參數,設置它的目的是給程序員看的,向程序員指明COMMIT TRANSACTION與哪些BEGIN TRANSACTION相關聯,以提升代碼的可讀性。
【例10.1】建立關於銀行轉賬的事務。
Ø 假設用UserTable表保存銀行客戶信息,該表的定義代碼以下:
CREATE TABLE UserTable ( UserId varchar(18) PRIMARY KEY, --身份證號 username varchar(20) NOT NULL, --用戶名 account varchar(20) NOT NULL UNIQUE, --賬號 balance float DEFAULT 0, --餘額 address varchar(100) --地址 );
Ø 用下面兩條語句分別添加兩條用戶記錄:
INSERT INTO UserTable VALUES('430302x1','王偉志','020000y1',10000,'中關村南路'); INSERT INTO UserTable VALUES('430302x2','張宇','020000y2',100,'火器營橋');
u 如今將帳戶020000y1上的2000元轉到帳戶430302x2上。爲了使得不出現前面所述的狀況(轉出賬號上已經被扣錢,但轉入賬號上的餘額並無增長),咱們把轉賬操做涉及的關鍵語句放到一個事務中,這樣就能夠避免出現上述錯誤狀況。下面代碼是對轉賬操做的一個簡化模擬:
BEGIN TRANSACTION virement -- 顯式啓動事務 DECLARE @balance float,@x float; -- ①設置轉賬金額 SET @x = 200; -- ②若是轉出賬號上的金額小於x,則取消轉賬操做 SELECT @balance = balance FROM UserTable WHERE account = '020000y1'; IF(@balance < @x) return; -- 不然執行下列操做 -- ③從轉出賬號上扣除金額x UPDATE UserTable SET balance = balance - @x WHERE account = '020000y1'; -- ④在轉入賬號上加上金額x UPDATE UserTable SET balance = balance + @x WHERE account = '020000y2'; -- ⑤轉賬操做結束 GO COMMIT TRANSACTION virement; -- 提交事務,事務終止
Ø 利用以上啓動的事務,操做③和操做④要麼都對數據庫產生影響,要麼對數據庫都不產生影響,從而避免了「轉出賬號上已經被扣錢,但轉入賬號上的餘額並無增長」的狀況。實際上,只是須要將操做③和操做④對應的語句放在BEGIN TRANSACTION …COMMIT TRANSACTION便可。
Ø 有時候DML語句執行失敗並不必定是由硬件故障等外部因素形成的,也有多是由內部運行錯誤(如違反約束等)形成的,從而致使相應的DML語句執行失敗。
Ø 若是在一個事務中,既有成功執行的DML語句,也有因內部錯誤而致使失敗執行的DML語句,那麼該事務會自動回滾嗎?
通常來講,執行SQL語句產生運行時錯誤時,SQL Server只回滾產生錯誤的SQL語句,而不會回滾整個事務。若是但願當遇到某一個SQL 語句產生運行時錯誤時,事務可以自動回滾整個事務,則SET XACT_ABORT選項設置爲ON(默認值爲OFF):SET XACT_ABORT ON
ü 即當SET XACT_ABORT爲ON時,若是執行SQL語句產生運行時錯誤,則整個事務將終止並回滾;
ü 當SET XACT_ABORT爲OFF時,有時只回滾產生錯誤的SQL語句,而事務將繼續進行處理。
ü 若是錯誤很嚴重,那麼即便SET XACT_ABORT爲OFF,也可能回滾整個事務。OFF 是默認設置。
Ø 注意,編譯錯誤(如語法錯誤)不受SET XACT_ABORT的影響。
【例10.2】回滾包含運行時錯誤的事務。
Ø 先觀察下列代碼:
USE MyDatabase; GO CREATE TABLE TestTransTable1(c1 char(3) NOT NULL, c2 char(3)); GO BEGIN TRAN INSERT INTO TestTransTable1 VALUES('aa1','aa2'); INSERT INTO TestTransTable1 VALUES(NULL,'bb2'); -- 違反非空約束 INSERT INTO TestTransTable1 VALUES('cc1','cc2'); COMMIT TRAN;
Ø 上述代碼的做用是:
(1)先建立表TestTransTable1,其中字段c1有非空約束;
(2)建立了一個事務,其中包含三條INSERT語句,用於向表TestTransTable1插入數據。
Ø 第二條INSER語句違反了非空約束。根據事務的概念,因而許多讀者可能會獲得這樣的結論:因爲第二條INSERT語句違反非空約束,所以該語句執行失敗,從而致使整個事務被回滾,使得全部的INSERT語句都不被執行,數據庫回到事務開始時的狀態——表TestTransTable1仍然爲空。
Ø 但實際狀況並非這樣。咱們使用SELECT語句查看錶TestTransTable1:
SELECT * FROM TestTransTable1;
Ø 結果如圖10.2所示。
Ø 圖10.2代表,只有第二條記錄沒有被插入,第一和第三條都被成功插入了,可見事務並無產生回滾。但若是將XACT_ABORT設置爲ON,當出現違反非空約束而致使語句執行失敗時,整個事務將被回滾。
【例子】執行下列代碼:
USE MyDatabase; GO SET XACT_ABORT ON; -- 將XACT_ABORT設置爲ON xact_abort GO DROP TABLE TestTransTable1; GO CREATE TABLE TestTransTable1(c1 char(3) NOT NULL, c2 char(3)); GO BEGIN TRAN INSERT INTO TestTransTable1 VALUES('aa1','aa2'); INSERT INTO TestTransTable1 VALUES(NULL,'bb2'); -- 違反非空約束 INSERT INTO TestTransTable1 VALUES('cc1','cc2'); COMMIT TRAN; SET XACT_ABORT OFF; -- 將XACT_ABORT改回默認設置OFF GO
Ø而後用SELECT語句查詢表TestTransTable1,結果發現,表TestTransTable1中並無數據。這說明,上述事務已經被回滾。
Ø相似地,例10.1也有一樣的問題。好比,若是用CHECK將字段balance設置在必定的範圍內,那麼餘額超出這個範圍時會違反這個CHECK約束。但定義的事務virement在出現違反約束狀況下卻沒法保證數據的一致性。顯然,經過將XACT_ABORT設置爲ON,這個問題就能夠獲得解決。
2. 回滾事務——ROLLBACK TRANSACTION
回滾事務是利用ROLLBACK TRANSACTION語句來實現,它能夠將顯式事務或隱性事務回滾到事務的起點或事務內的某個保存點(savepoint)。該語句的語法以下:
ROLLBACK { TRAN | TRANSACTION } [ transaction_name | @tran_name_variable | savepoint_name | @savepoint_variable ] [ ; ]
Ø transaction_name | @tran_name_variable
該參數用於指定由BEGIN TRANSACTION語句分配的事務的名稱。嵌套事務時,transaction_name 必須是最外面的BEGIN TRANSACTION語句中的名稱。
Ø savepoint_name | @savepoint_variable
該參數爲SAVE TRANSACTION語句中指定的保存點。指定了該參數,則回滾時數據庫將恢復到該保存點時的狀態(而不是事務開始時的狀態)。不帶savepoint_name和transaction_name的ROLLBACK TRANSACTION語句將使事務回滾到起點。
Ø 根據在ROLLBACK TRANSACTION語句中是否使用保存點,能夠將回滾分爲所有回滾和部分回滾。
(1)所有回滾
【例10.3】所有回滾事務。
下面代碼先定義表TestTransTable2,而後在事務myTrans1中執行三條插入語句,事務結束時用ROLLBACK TRANSACTION語句所有回滾事務,以後又執行兩條插入語句,以觀察所有回滾事務的效果。代碼以下:
USE MyDatabase; GO CREATE TABLE TestTransTable2(c1 char(3), c2 char(3)); GO DECLARE @TransactionName varchar(20) = 'myTrans1'; BEGIN TRAN @TransactionName INSERT INTO TestTransTable2 VALUES('aa1','aa2’); INSERT INTO TestTransTable2 VALUES('bb1','bb2’); INSERT INTO TestTransTable2 VALUES('cc1','cc2'); ROLLBACK TRAN @TransactionName -- 回滾事務 INSERT INTO TestTransTable2 VALUES('dd1','dd2'); INSERT INTO TestTransTable2 VALUES('ee1','ee2'); SELECT * FROM TestTransTable2
執行上述代碼,結果如圖10.3所示
Ø 以上能夠看到,事務myTrans1中包含的三條插入語句並無實現將相應的三條數據記錄插入到表TestTransTable2中,
緣由:在於ROLLBACK TRAN語句對整個事務進行所有回滾,使得數據庫回到執行這三條插入語句以前的狀態。事務myTrans1以後又執行了兩條插入語句,這時是處於事務自動提交模式(每一條SQL語句就是一個事務,而且這種事務結束後會自動提交,而沒有回滾)下,所以這兩條插入語句成功地將兩條數據記錄插入到數據庫中。
Ø 根據ROLLBACK的語法,在本例中,BEGIN TRAN及其ROLLBACK TRAN後面的@TransactionName能夠省略,其效果是同樣的。
(2)部分回滾
Ø 若是在事務中設置了保存點(即ROLLBACK TRANSACTION語句帶參數savepoint_name | @savepoint_variable)時,ROLLBACK TRANSACTION語句將回滾到由savepoint_name或@savepoint_variable指定的保存點上。
Ø 在事務內設置保存點是使用SAVE TRANSACTION語句來實現,其語法以下:
SAVE { TRAN | TRANSACTION } { savepoint_name | @savepoint_variable } [ ; ]
savepoint_name | @savepoint_variable是保存點的名稱,必須指定。
【例10.4】部分回滾事務。
在例10.3所定義的事務中利用SAVE TRANSACTION語句增長一個保存點save1,同時修改ROLLBACK語句,其餘代碼相同。全部代碼以下:
USE MyDatabase; GO DROP TABLE TestTransTable2; CREATE TABLE TestTransTable2(c1 char(3), c2 char(3)); GO DECLARE @TransactionName varchar(20) = 'myTrans1'; BEGIN TRAN @TransactionName INSERT INTO TestTransTable2 VALUES('aa1','aa2'); INSERT INTO TestTransTable2 VALUES('bb1','bb2'); SAVE TRANSACTION save1; -- 設置保存點 INSERT INTO TestTransTable2 VALUES('cc1','cc2'); ROLLBACK TRAN save1; INSERT INTO TestTransTable2 VALUES('dd1','dd2'); INSERT INTO TestTransTable2 VALUES('ee1','ee2'); SELECT * FROM TestTransTable2
執行結果如圖10.4所示。
此結果代表,只有第三條插入語句的執行結果被撤銷了。其緣由在於,事務myTrans1結束時ROLLBACK TRAN語句回滾保存點save1處,即回滾到第三條插入語句執行以前,故第三條插入語句的執行結果被撤銷,其餘插入語句的執行結果是有效的。
Ø 事務是容許嵌套的,即一個事務內能夠包含另一個事務。當事務嵌套時,就存在多個事務同時處於活動狀態。
Ø 系統全局變量@@TRANCOUNT可返回當前鏈接的活動事務的個數。對@@TRANCOUNT返回值有影響的是BEGIN TRANSACTION、ROLLBACK TRANSACTION和COMMIT語句。具體影響方式以下:
ü 每執行一次BEGIN TRANSACTION命令就會使@@TRANCOUNT的值增長1;
ü 每執行一次COMMIT命令時,@@TRANCOUNT的值就減1;
ü 一旦執行到ROLLBACK TRANSACTION命令(所有回滾)時,@@TRANCOUNT的值將變爲0;
ü 但ROLLBACK TRANSACTION savepoint_name(部分回滾)不影響@@TRANCOUNT的值。
【例10.5】嵌套事務。
本例中,先建立表TestTransTable3,而後在有三個嵌套層的嵌套事務中向該表插入數據,並在每次啓動或提交一個事務時都打印@@TRANCOUNT的值。代碼以下:
USE MyDatabase; GO CREATE TABLE TestTransTable3(c1 char(3), c2 char(3)); GO if(@@TRANCOUNT!=0) ROLLBACK TRAN; -- 先終止全部事務 BEGIN TRAN Trans1 PRINT '啓動事務Trans1後@@TRANCOUNT的值:'+CAST(@@TRANCOUNT AS VARCHAR(10)); INSERT INTO TestTransTable3 VALUES('aa1','aa2’); BEGIN TRAN Trans2 PRINT '啓動事務Trans2後@@TRANCOUNT的值:'+CAST(@@TRANCOUNT AS VARCHAR(10)); INSERT INTO TestTransTable3 VALUES('bb1','bb2'); BEGIN TRAN Trans3 PRINT '啓動事務Trans3後@@TRANCOUNT的值:'+CAST(@@TRANCOUNT AS VARCHAR(10)); INSERT INTO TestTransTable3 VALUES('cc1','cc2'); SAVE TRANSACTION save1; -- 設置保存點 PRINT '設置保存點save1後@@TRANCOUNT的值:'+CAST(@@TRANCOUNT AS VARCHAR(10)); INSERT INTO TestTransTable3 VALUES('dd1','dd2'); ROLLBACK TRAN save1; PRINT '回滾到保存點save1後@@TRANCOUNT的值:'+CAST(@@TRANCOUNT AS VARCHAR(10)); INSERT INTO TestTransTable3 VALUES('ee1','ee2'); COMMIT TRAN Trans3 PRINT '提交Trans3後@@TRANCOUNT的值:'+CAST(@@TRANCOUNT AS VARCHAR(10)); INSERT INTO TestTransTable3 VALUES('ff1','ff2’); COMMIT TRAN Trans2 PRINT '提交Trans2後@@TRANCOUNT的值:'+CAST(@@TRANCOUNT AS VARCHAR(10)); COMMIT TRAN Trans1 PRINT '提交Trans1後@@TRANCOUNT的值:'+CAST(@@TRANCOUNT AS VARCHAR(10));
Ø 執行上述代碼,結果如圖13.5所示。
Ø 從圖10.5中也能夠能夠看出,每執行一次BEGIN TRANSACTION命令就會使@@TRANCOUNT的值增長1,每執行一次COMMIT命令時,@@TRANCOUNT的值就減1,但ROLLBACK TRANSACTION savepoint_name不影響@@TRANCOUNT的值。
Ø 若是遇到ROLLBACK TRANSACTION命令,無論該命令以後是否還有其餘的COMMIT命令,系統中全部的事務都被終止(不提交),@@TRANCOUNT的值爲0。
Ø 執行上述嵌套事務後,表TestTransTable3中的數據如圖10.6所示。
Ø 若是將上述代碼中的語句COMMIT TRAN Trans1(倒數第二條)改成ROLLBACK TRAN(不帶參數),則表TestTransTable3中將沒有任何數據。這說明,對於嵌套事務,無論內層是否使用COMMIT命令來提交事務,只要外層事務中使用ROLLBACK TRAN來回滾,那麼整個嵌套事務都被回滾,數據庫將回到嵌套事務開始時的狀態。
Ø 數據共享是數據庫的基本功能之一。一個數據庫可能同時擁有多個用戶,這意味着在同一時刻系統中可能同時運行上百上千個事務。而每一個事務又是由若干個數據庫操做構成的操做序列,如何有效地控制這些操做的執行對提升系統的安全性和運行效率有着十分重要的意義。
Ø 在單CPU系統中,事務的運行有兩種方式,一種是串行執行,一種是併發執行。串行執行是指每一個時刻系統中只有一個事務在運行,其餘事務必須等到該事務中全部的操做執行完了之後才能運行。這種執行方式的優勢是方便控制,但其缺點倒是十分突出,那就是整個系統的運行效率很低。由於在串行方式中,不一樣的操做須要不一樣的資源,但一個操做通常不會使用全部的資源且使用時間長短不一,因此串行執行的事務會使許多系統資源處於空閒狀態。
Ø 若是可以充分利用這些空閒的資源,無疑能夠有效提升系統的運行效率,這是考慮事務併發控制的主要緣由之一。另外,併發控制能夠更好保證數據的一致性,從而實現數據的安全性。
Ø 在併發執行方式中,系統容許同一個時刻有多個事務在並行執行。這種並行執行其實是經過事務操做的輪流交叉執行來實現的。雖然在同一時刻只有某一個事務的某一個操做在佔用CPU資源,但其餘事務中的操做可使用該操做沒有佔用的有關資源,這樣能夠在整體上提升系統的運行效率。
Ø 對於併發運行的事務,若是沒有有效地控制其操做,就可能致使對資源的不合理使用,對數據庫而言就可能致使數據的不一致性和不完整性等問題。所以,DBMS必須提供一種容許多個用戶同時對數據進行存取訪問的併發控制機制,以確保數據庫的一致性和完整性。
Ø 簡而言之,併發控制就是針對併發執行的事務,如何有效地控制和調度其交叉執行的數據庫操做,使各事務的執行不相互干擾,以免出現數據庫的不一致性和不完整性等問題。
當多個用戶同時訪問數據庫時,若是沒有必要的訪問控制措施,可能會引起數據不一致等併發問題,這是誘發併發控制的主要緣由。爲進行有效的併發控制,首先要明確併發問題的類型,分析不一致問題產生的根源。
1. 丟失修改(Lost Update)
下面看一個經典的關於民航訂票系統的例子。它能夠說明多個事務對數據庫的併發操做帶來的不一致性問題。
【例子】假設某個民航訂票系統有兩個售票點,分別爲售票點A和售票點B。假設系統把一次訂票業務定義爲一個事務,其包含的數據庫操做序列以下:
T:Begin Transaction
讀取機票餘數x;
售出機票y張,機票餘數x ← x – y;
把x寫回數據庫,修改數據庫中機票的餘數;
Commit;
Ø 假設當前機票餘數爲10張,售票點A和售票點B同時進行一次訂票業務,分別有用戶訂4張和3張機票。因而在系統中同時造成兩個事務,分別記爲TA和TB。若是事務TA和TB中的操做交叉執行,執行過程如圖10.7所示。
事務TA和TB執行完了之後,因爲B_op3是最後的操做,因此數據庫中機票的餘數6。而實際狀況是,售票點A售出4張,售票點B售出3張,因此實際剩下10-(4+3) = 3張機票。這就形成了數據庫反映的信息與實際狀況不符,從而產生了數據的不一致性。這種不一致性是由操做B_op3的(對數據庫的)修改結果將操做A_op3的修改結果覆蓋掉而產生的,即A_op3的修改結果「丟了」,因此稱爲丟失修改。
2. 讀「髒」數據(Dirty Read)
Ø 事務TC對某一數據處理了之後將結果寫回到數據區,而後事務TD從數據區中讀取該數據。但事務TC出於某種緣由進行回滾操做,撤消已作出的操做,這時TD剛讀取的數據又被恢復到原值(事務TC開始執行時的值),這樣TD讀到的數據就與數據庫中的實際數據不一致了,而TD讀取的數據就是所謂的「髒」數據(不正確的數據)。「髒」數據是指那些被某事務更改、但尚未被提交的數據。
【例子】 在訂票系統中,事務TC在讀出機票餘數10並售出4張票後,將機票餘數10-4=6寫到數據區(還沒來得及提交),恰在此時事務TD讀取機票餘數6,而TC出於某種緣由(如斷電等)進行回滾操做,機票餘數恢復到了原來的值10並撤銷這次售票操做,但這時事務TD仍然使用着讀到的機票餘數6,這與數據庫中實際的機票餘數不一致,這個「機票餘數6」就是所謂的「髒」數據,如圖10.8所示。
3. 不可重複讀(Non-Repeatable Read)
Ø 事務TE按照必定條件讀取數據庫中某數據x,隨後事務TF又修改了數據x,這樣當事務TE操做完了之後又按照相同條件讀取數據x,但這時因爲數據x已經被修改,因此此次讀取值與上一次不一致,從而在進行一樣的操做後卻獲得不同的結果。因爲另外一個事務對數據的修改而致使當前事務兩次讀到的數據不一致,這種狀況就是不可重複讀。這與讀「髒」數據有類似之處。
【例子】 在圖10.9中c表明機票的價格,n表明機票的張數。機票查詢事務TE讀取機票價格c = 800和機票張數n = 7,接着計算這7張票的總價錢5600(可能有人想查詢7張機票總共須要多少錢);剛好在計算總價錢完後,管理事務TF(相關航空公司執行)讀取c = 800並進行六五折降價處理後將c = 520寫回數據庫;這時機票查詢事務TE重讀c(可能爲驗證總價錢的正確性),結果獲得c=520,這與第一次讀取值不一致。顯然,這種不一致性會致使系統給出錯誤的信息,這是不容許的。
4. 幻影讀(Phantom Row)
Ø 假設事務TG按照必定條件兩次讀取表中的某些數據記錄,在第一次讀取數據記錄後事務TH在該表中刪除(或添加)某些記錄。這樣在事務TG第二次按照一樣條件讀取數據記錄時會發現有些記錄「幻影」般地消失(或增多)了,這稱爲幻影(Phantom Row)讀。
Ø 致使以上四種不一致性產生的緣由是併發操做的隨機調度,這使事務的隔離性遭到破壞。爲此,須要採起相應措施,對全部數據庫操做的執行次序進行合理而有效的安排,使得各個事務都可以獨立地運行、彼此不相互干擾,保證事務的ACID特性,避免出現數據不一致性等併發問題。
Ø 保證事務的隔離性能夠有效防止數據不一致等併發問題。事務的隔離性有程度之別,這就是事務隔離級別。在SQL Server中,事務的隔離級別用於表徵一個事務與其餘事務進行隔離的程度。隔離級別越高,就能夠更好地保證數據的正確性,但併發程度和效率就越低;相反,隔離級別越低,出現數據不一致性的可能性就越大,但其併發程度和效率就越高。經過設定不一樣事務隔離級別能夠實現不一樣層次的訪問控制需求。
Ø 在SQL Server中,事務隔離級別分爲四種:READ UNCOMMITTED、READ COMMITTED、REPEATABLE READ、SERIALIZABLE,它們對數據訪問的限制程度依次從低到高。設置隔離級別是經過SET TRANSACTION ISOLATION LEVEL語句來實現,其語法以下:
ET TRANSACTION ISOLATION LEVEL { READ UNCOMMITTED | READ COMMITTED | REPEATABLE READ | SERIALIZABLE } [ ; ]
1. 使用READ UNCOMMITTED
Ø 該隔離級別容許讀取已經被其餘事務修改過但還沒有提交的數據,實際上該隔離級別根本就沒有提供事務間的隔離。這種隔離級別是四種隔離級別中限制最少的一種,級別最低。
Ø 其做用可簡記爲:容許讀取未提交數據。
【例10.6】使用READ UNCOMMITTED隔離級別,容許丟失修改。
當事務的隔離級別設置爲READ UNCOMMITTED時,SQL Server容許用戶讀取未提交的數據,所以會形成丟失修改。爲觀察這種效果,按序完成下列步驟:
(1)建立表TestTransTable4並插入兩條數據:
CREATE TABLE TestTransTable4(flight char(4), price float, number int); INSERT INTO TestTransTable4 VALUES('A111',800,10); INSERT INTO TestTransTable4 VALUES('A222',1200,20);
其中,flight、price、number分別表明航班號、機票價格、剩餘票數。
(2)編寫事務TA和TB的代碼:
-- 事務TA的代碼 SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED; -- 設置事務隔離級別 BEGIN TRAN TA DECLARE @n int; SELECT @n = number FROM TestTransTable4 WHERE flight = 'A111’; WAITFOR DELAY '00:00:10' -- 等待事務TB讀數據 SET @n = @n - 4; UPDATE TestTransTable4 SET number = @n WHERE flight = 'A111'; COMMIT TRAN TA -- 事務TB的代碼 SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED; BEGIN TRAN TB DECLARE @n int; SELECT @n = number FROM TestTransTable4 WHERE flight = 'A111’; WAITFOR DELAY '00:00:15' -- 等待,以讓事務TA先提交數據 SET @n = @n - 3; UPDATE TestTransTable4 SET number = @n WHERE flight = 'A111'; COMMIT TRAN TB
(3)打開兩個查詢窗口,分別在兩個窗口中前後執行事務TA和TB(執行TA後應該在10秒之內執行TB,不然看不到預設的結果),分別如圖10.10和圖10.11所示。
(4)查詢表中的數據:
SELECT * FROM TestTransTable4;
結果如圖10.12所示。
由代碼可知,事務TA和TB分別售出了4張和3張票,所以應該剩下10-(4+3) = 3張票。但由圖10.12能夠看到,系統還剩下7張票。這就是丟失修改的結果。當隔離級別爲READ UNCOMMITTED時,事務不能防止丟失修改。
實際上,對於前面介紹的四種數據不一致狀況,READ UNCOMMITTED隔離級別都不能防止它們。這是READ UNCOMMITTED隔離級別的缺點。其優勢是可避免併發控制所需增長的系統開銷,通常用於單用戶系統(不適用於併發場合)或者系統中兩個事務同時訪問同一資源的可能性爲零或幾乎爲零。
2. 使用READ COMMITTED
Ø 在使用該隔離級別時,當一個事務已經對一個數據塊進行了修改(UPDATE)但還沒有提交或回滾時,其餘事務不容許讀取該數據塊,即該隔離級別不容許讀取未提交的數據。它的隔離級別比READ UNCOMMITTED高一層,能夠防止讀「髒」,但不能防止丟失修改,也不能防止不可重複讀和「幻影」讀。
Ø 其做用可簡記爲:不容許讀取已修改但未提交數據。
Ø READ COMMITTED是SQL Server默認的事務隔離級別。
【例10.7】使用READ COMMITTED隔離級別,防止讀「髒」數據。
先恢復表TestTransTable4中的數據:
DELETE FROM TestTransTable4;
INSERT INTO TestTransTable4 VALUES('A111',800,10);
INSERT INTO TestTransTable4 VALUES('A222',1200,20);
Ø 爲觀察讀「髒」數據,先將事務的隔離級別設置爲READ UNCOMMITTED,分別在兩個查詢窗口中前後執行事務TC和TD:
-- 事務TC的代碼 SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED; -- 設置事務隔離級別 BEGIN TRAN TC DECLARE @n int; SELECT @n = number FROM TestTransTable4 WHERE flight = 'A111’; SET @n = @n - 4; UPDATE TestTransTable4 SET number = @n WHERE flight = 'A111’; WAITFOR DELAY '00:00:10' -- 等待事務TD讀「髒」數據 ROLLBACK TRAN TC -- 回滾事務
-- 事務TD的代碼
SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED; BEGIN TRAN TD DECLARE @n int; SELECT @n = number FROM TestTransTable4 WHERE flight = 'A111'; -- 讀「髒」數據 PRINT '剩餘機票數:'+CONVERT(varchar(10),@n); COMMIT TRAN TD
Ø 結果事務TD輸出以下的結果: 剩餘機票數:6
Ø 在等待事務TC執行完了之後,利用SELECT語句查詢表TestTransTable4,結果發現剩餘機票數爲10。6就是事務TD讀到的「髒」數據。
Ø 爲了不讀到這個「髒」數據,只需將上述的隔離級別由READ UNCOMMITTED改成READ COMMITTED便可(其餘代碼不變)。但在將隔離級別更改了之後,咱們發現事務TD要等事務TC回滾了之後(ROLLBACK)才執行讀操做。READ COMMITTED雖然能夠比READ UNCOMMITTED具備更好解決併發問題的能力,可是其效率較後者低。
3. 使用REPEATABLE READ
Ø 在該隔離級別下,若是一個數據塊已經被一個事務讀取但還沒有做提交操做,則任何其餘事務都不能修改(UPDATE)該數據塊(但能夠執行INSERT和DELETE),直到該事務提交或回滾後才能修改。該隔離級別的層次又在READ COMMITTED之上,即比READ COMMITTED有更多的限制,
Ø 它能夠防止讀「髒」數據和不可重複讀。但因爲一個事務讀取數據塊後另外一個事務能夠執行INSERT和DELETE操做,因此它不能防止「幻影」讀。另外,該隔離級別容易形成死鎖。例如,將它用於解決例10.6中的丟失修改問題時,就形成死鎖。
Ø 其做用可簡記爲:不容許讀取未提交數據,不容許修改已讀數據。
【例10.8】使用REPEATABLE READ隔離級別,防止不可重複讀。
Ø 先看看存在不可重複讀的事務TE: -- 事務TE的代碼 SET TRANSACTION ISOLATION LEVEL READ COMMITTED; -- 設置事務隔離級別 BEGIN TRAN TE DECLARE @n int, @c int; -- 顧客先查詢張機票的價格 SELECT @c = price FROM TestTransTable4 WHERE flight = 'A111'; -- 第一次讀 SET @n = 7; PRINT CONVERT(varchar(10),@n)+'張機票的價格:'+CONVERT(varchar(10),@n*@c)+'元’; WAITFOR DELAY '00:00:10' -- 爲觀察效果,讓該事務等待10秒 -- 接着購買張機票 SELECT @c = price FROM TestTransTable4 WHERE flight = 'A111'; -- 第二次讀 SET @n = 7; PRINT '總共'+CONVERT(varchar(10),@n)+'張機票,應付款:'+CONVERT(varchar(10),@n*@c)+'元'; COMMIT TRAN TE -- 提交事務
Ø 另外一事務TF的代碼以下: -- 事務TF的代碼 SET TRANSACTION ISOLATION LEVEL READ COMMITTED; -- 設置事務隔離級別 BEGIN TRAN TF UPDATE TestTransTable4 SET price = price*0.65 WHERE flight = 'A111'; -- 折價65折 COMMIT TRAN TF
Ø 分別在兩個查詢窗口中前後運行事務TE和事務TF(時間間隔要小於10秒),事務TE輸出的結果如圖10.13所示。
Ø 該結果說了事務TE出現了不可重複讀:在相同條件下,利用兩次讀取的信息來計算的機票價格卻不同。緣由在於,當事務TE處於10秒等待期時,事務TF對機票價格(price)進行六五折處理,結果致使了在同一事務中的兩次讀取操做得到不一樣的結果。
Ø 若是將事務隔離級別由原來的READ COMMITTED改成REPEATABLE READ(其餘代碼不變),則就能夠防止上述的不可重複讀,如圖10.14所示。這是由於REPEATABLE READ隔離級別不容許對事務TE已經讀取的數據(價格)進行任何的更新操做,這樣事務TF只能等待事務TE結束後才能對價格進行五六折處理,從而避免不可重複讀問題。顯然,因爲出現事務TF等待事務TE的狀況,所以使用REPEATABLE READ隔離級別時要比使用READ COMMITTED的效率低。
4. 使用SERIALIZABLE
Ø SERIALIZABLE是SQL Server最高的隔離級別。在該隔離級別下,一個數據塊一旦被一個事務讀取或修改,則不容許別的事務對這些數據進行更新操做(包括UPDATE, INSERT, DELETE),直到該事務提交或回滾。也就是說,一旦一個數據塊被一個事務鎖定,則其餘事務若是須要修改此數據塊,它們只能排隊等待。SERIALIZABLE隔離級別的這些性質決定了它可以解決「幻影」讀問題。
Ø 其做用可簡記爲:事務必須串行執行。
【例10.9】使用SERIALIZABLE隔離級別,防止「幻影」讀。
Ø 先看看存在「幻影」讀的事務TG:
-- 事務TG的代碼 SET TRANSACTION ISOLATION LEVEL REPEATABLE READ -- 設置事務隔離級別 BEGIN TRAN TG SELECT * FROM TestTransTable4 WHERE price <= 1200; -- 第一次讀 WAITFOR DELAY '00:00:10' -- 事務等待10秒 SELECT * FROM TestTransTable4 WHERE price <= 1200; -- 第二次讀 COMMIT TRAN TG -- 提交事務
Ø 構造另外一事務TH:
-- 事務TH的代碼 SET TRANSACTION ISOLATION LEVEL REPEATABLE READ; -- 設置事務隔離級別 BEGIN TRAN TH INSERT INTO TestTransTable4 VALUES('A333',1000,20); COMMIT TRAN TH
Ø 分別在兩個查詢窗口中前後運行事務TG和事務TH(時間間隔要小於10秒,且先恢復表TestTransTable4中的數據),事務TG中的兩條SELECT語句輸出的結果分別如圖10.15和圖10.16所示:
Ø 在事務TG中徹底相同的兩個查詢語句在兩次執行後獲得的結果不同,其中在第二次查詢結果中「幻影」般地增長了一個票價爲1000元的航班信息。可見,REPEATABLE READ隔離級別雖然比前兩者均高,但仍是不能防止「幻影」讀。
Ø 若是將事務隔離級別由原來的REPEATABLE READ改成SERIALIZABLE(其餘代碼不變),按照上述一樣方法執行這兩個事務後,事務TG中的兩次查詢獲得的結果均如圖10.15所示。這代表「幻影」讀已經不復存在了,隔離級別SERIALIZABLE能夠防止上述的「幻影」讀。若是這時進一步查詢表TestTransTable4中的數據,能夠看到其結果與圖10.16所示的結果同樣。這是由於,在SERIALIZABLE隔離級別下,事務TG執行完了之後再執行事務TH,即串行事務TG和TH,所以事務TH中的語句不會影響到事務TG,從而避免「幻影」讀。
Ø 須要說明的是,REPEATABLE READ和SERIALIZABLE隔離級別對系統性能的影響都很大,特別是SERIALIZABLE隔離級別,不是非不得以,最好不要使用。
Ø 根據以上分析,四種隔離級對事務「讀」和「寫」操做的處理關係說明如表10.1所示。
表10.1中,「讀」、「寫」、「插」和「刪」分別指SELECT、UPDATE、INSERT和DELETE操做。
「讀了,可再讀」表述的意思是,執行了SELECT後,在事務尚未提交或回滾以前,還能夠繼續執行SELECT;
「讀了,不可再寫」是指,執行了SELECT後,在事務尚未提交或回滾以前,是不容許執行UPDATE操做的。其餘項的意思能夠照此類推。
Ø 根據表10.1,咱們可進一步總結四種隔離級別對支持解決併發問題的狀況,結果如表10.2所示。
注:√表示「防止」,×表示「不必定防止」
嚴格說,REPEATABLE READ和SERIALIZABLE是不支持解決丟失修改問題的,由於它們用於此類問題時,容易形成死鎖。
【例子】對於例10.6中的事務TA和TB,若是將其中的UNCOMMITTED替換成REPEATABLE READ或SERIALIZABLE,而後按照例10.6中的方法執行這兩個事務,結果雖然沒有形成數據的不一致,但出現了死鎖(死鎖最後是由SQL Server自動終止一個事務來解除)。隔離級別的方法並不能徹底解決涉及的併發問題。
Ø 鎖定是指對數據塊的鎖定,是SQL Server數據庫引擎用來同步多個用戶同時對同一個數據塊進行訪問的一種控制機制。這種機制的實現是利用鎖(LOCK)來完成的。一個用戶(事務)能夠申請對一個資源加鎖,若是申請成功的話,則在該事務訪問此資源的時候其餘用戶對此資源的訪問受到諸多的限制,以保證數據的完整性和一致性。
Ø SQL Server提供了多種不一樣類型的鎖。有的鎖類型是兼容的,有的是不兼容的。不一樣類型的鎖決定了事務對數據塊的訪問模式。SQL Serve經常使用的鎖類型主要包括:
(1)共享鎖(S):容許多個事務併發讀取同一數據塊,但不容許其餘事務修改當前事務加鎖的數據塊。一個事務對一個數據塊加上一個共享鎖後,其餘事務也能夠繼續對該數據塊加上共享鎖。這就是說,當一個數據塊被多個事務同時加上共享鎖的時候,全部的事務都不能對這個數據塊進行修改,直到數據讀取完成,共享鎖釋放。
(2)排它鎖(X):也稱獨佔鎖、寫鎖,當一個事務對一個數據塊加上排它鎖後,它能夠對該數據塊進行UPDATE、DELETE、INSERT等操做,而其餘事務不能對該數據塊加上任何鎖,於是也不能執行任何的更新操做(包括UPDATE、DELETE和INSERT)。通常用於對數據塊進行更新操做時的併發控制,它能夠保證同一數據塊不會被多個事務同時進行更新操做,避免由此引起的數據不一致。
(3)更新鎖:更新鎖介於共享鎖和排它鎖之間,主要用於數據更新,能夠較好地防止死鎖。一個數據塊的更新鎖一次只能分配給一個事務,在讀數據的時候該更新鎖是共享鎖,一旦更新數據時它就變成排他鎖,更新完後又變爲共享鎖。但在變換過程當中,可能出現鎖等待等問題,且變換自己也須要時間,所以使用這種鎖時,效率並不十分理想。
(4)意向鎖:表示SQL Server須要在層次結構中的某些底層資源上(如行,列)獲取共享鎖、排它鎖或更新鎖。
【例子】表級放置了意向共享鎖,就表示事務要對錶的頁或行上使用共享鎖;在表的某一行上上放置意向鎖,能夠防止其它事務獲取其它不兼容的鎖。意向鎖的優勢是能夠提升性能,由於數據引擎不須要檢測資源的每一列每一行,就能判斷是否能夠獲取到該資源的兼容鎖。它包括三種類型:意向共享鎖,意向排他鎖,意向排他共享鎖。
(5)架構鎖:架構鎖用於在修改表結構時,阻止其餘事務對錶的併發訪問。
(6)鍵範圍鎖:用於鎖定表中記錄之間的範圍的鎖,以防止記錄集中的「幻影」插入或刪除,確保事務的串行執行。
(7)大容量更新鎖:容許多個進程將大容量數據併發的複製到同一個表中,在複製加載的同時,不容許其它非複製進程訪問該表。
在這些鎖當中,共享鎖(S鎖)和排他鎖(X鎖)尤其重要,它們之間的相容關係描述以下:
Ø 若是事務T對數據塊D成功加上共享鎖,則其餘事務只能對D再加共享鎖,不能加排他鎖,且此時事務T只能讀數據塊D,不能修改它(除非其餘事務沒有對該數據塊加共享鎖)。
Ø 若是事務T對數據塊D成功加上排他鎖,則其餘事務不能再對D加上任何類型的鎖,也對D進行讀操做和寫操做,而此時事務T既能讀數據塊D,也又能修改該數據塊。
Ø 下面主要是結合SQL Server提供的表提示(table_hint),介紹共享鎖和排他鎖在併發控制中的使用方法。加鎖狀況的動態信息能夠經過查詢系統表sys.dm_tran_locks得到。
Ø 經過在SELECT、INSERT、UPDATE及DELETE語句中爲單個表引用指定表提示,能夠實現對數據塊的加鎖功能,實現事務對數據訪問的併發控制。
Ø 爲數據表指定表提示的簡化語法以下:
{SELECT … | INSERT … | UPDATE … | DELECT … | MERGE …} [ WITH ( <table_hint> ) ] <table_hint> ::= [ NOEXPAND ] { INDEX ( index_value [ ,...n ] ) | INDEX = ( index_value ) | FASTFIRSTROW | FORCESEEK | HOLDLOCK | NOLOCK | NOWAIT | PAGLOCK | READCOMMITTED | READCOMMITTEDLOCK | READPAST | READUNCOMMITTED | REPEATABLEREAD | ROWLOCK | SERIALIZABLE | TABLOCK | TABLOCKX | UPDLOCK | XLOCK }
u 表提示語法中有不少選項,下面主要介紹與表級鎖有密切相關的幾個選項:
Ø HOLDLOCK
表示使用共享鎖,使用共享鎖更具備限制性,保持共享鎖直到事務完成。而不是不管事務是否完成,都在再也不須要所需表或數據頁時當即釋放共享鎖。HOLDLOCK不能被用於包含FOR BROWSE選項的SELECT語句。它同等於SERIALIZABLE隔離級別。
Ø NOLOCK
表示不發佈共享鎖來阻止其餘事務修改當前事務在讀的數據,容許讀「髒」數據。它同等於等同於READ UNCOMMITTED隔離級別。
Ø PAGLOCK
表示使用頁鎖,一般使用在行或鍵採用單個鎖的地方,或者採用單個表鎖的地方。
Ø READPAST
指定數據庫引擎跳過(不讀取)由其餘事務鎖定的行。在大多數狀況下,這一樣適用於頁。數據庫引擎跳過這些行或頁,而不是在釋放鎖以前阻塞當前事務。它僅適用於READ COMMITTED或REPEATABLE READ隔離級別的事務中。
Ø ROWLOCK
表示使用行鎖,一般在採用頁鎖或表鎖時使用。
Ø TABLOCK
指定對錶採用共享鎖並讓其一直持有,直至語句結束。若是同時指定了HOLDLOCK,則會一直持有共享表鎖,直至事務結束。
Ø TABLOCKX
指定對錶採用排他鎖(獨佔表級鎖)。若是同時指定了HOLDLOCK,則會一直持有該鎖,直至事務完成。在整個事務期間,其餘事務不能訪問該數據表。
Ø UPDLOCK
指定要使用更新鎖(而不是共享鎖),並保持到事務完成。
注意:若是設置了事務隔離級別,同時指定了鎖提示,則鎖提示將覆蓋會話的當前事務隔離級別。
【例10.10】使用表級共享鎖。
Ø 對於數據表TestTransTable4,事務T1對其加上表級共享鎖,使得在事務期內其餘事務不能更新此數據表。事務T1的代碼以下:
BEGIN TRAN T1
DECLARE @s varchar(10);
-- 下面一條語句的惟一做用是對錶加共享鎖
SELECT @s = flight FROM TestTransTable4 WITH(HOLDLOCK,TABLOCK) WHERE 1=2;
PRINT '加鎖時間:'+CONVERT(varchar(30), GETDATE(), 20);
WAITFOR DELAY '00:00:10' -- 事務等待10秒
PRINT '解鎖時間:'+CONVERT(varchar(30), GETDATE(), 20);
COMMIT TRAN T1
Ø 爲觀察共享鎖的效果,進一步定義事務T2:
BEGIN TRAN T2
UPDATE TestTransTable4 SET price = price*0.65 WHERE flight = 'A111’;
PRINT '數據更新時間:'+CONVERT(varchar(30), GETDATE(), 20);
COMMIT TRAN T2
Ø 而後分別在兩個查詢窗口中前後運行事務T1和事務T2(時間間隔要小於10秒),事務T1和T2輸出的結果分別如圖10.17和圖10.18所示。
Ø 對比圖10.17和圖10.18,事務T1對錶TestTransTable4的更新操做(包括刪除和添加)必須等到事務T2解除共享鎖之後才能進行(但在事務T1期內,事務T2可使用SELECT語句查詢表TestTransTable4)。
Ø 使用HOLDLOCK和TABLOCK能夠避免在事務期內被鎖定對象受到更新(包括刪除和添加),於是能夠避免「幻影」讀;但因爲T1在進行UPDATE操做後,T2可以繼續SELECT數據,所以這種控制策略不能防止讀「髒」數據;共享鎖也不能防止丟失修改。
Ø 若是同時在T1和T2中添加讀操做和寫操做,則容易形成死鎖。
【例子】若是在例10.6的兩個事務TA和TB中改用共享鎖進行併發控制,一樣會出現死鎖的現象。但更新鎖可以自動實如今共享鎖和排他鎖之間的切換,完成對數據的讀取和更新,且在防止死鎖方面有優點。若是在例10.6的兩個事務TA和TB中改用更新鎖,結果是能夠對這兩個事務成功進行併發控制的。
【例10.11】利用更新鎖解決丟失修改問題。
對於例10.6的兩個事務TA和TB,用事務隔離級別的方法難以解決丟失修改問題,但用更新鎖則能夠較好地解決這個問題。更新鎖是用UPDLOCK選項來定義,修改後事務TA和TB的代碼以下:
-- 事務TA的代碼 BEGIN TRAN TA DECLARE @n int; SELECT @n = number FROM TestTransTable4 WITH(UPDLOCK,TABLOCK) WHERE flight = 'A111'; WAITFOR DELAY '00:00:10' -- 等待10秒,以讓事務TB讀數據 SET @n = @n - 4; UPDATE TestTransTable4 SET number = @n WHERE flight = 'A111'; COMMIT TRAN TA -- 事務TB的代碼 BEGIN TRAN TB DECLARE @n int; SELECT @n = number FROM TestTransTable4 WITH(UPDLOCK,TABLOCK) WHERE flight = 'A111'; WAITFOR DELAY '00:00:15' SET @n = @n - 3; UPDATE TestTransTable4 SET number = @n WHERE flight = 'A111'; COMMIT TRAN TB
【例10.12】 利用排他鎖來實現事務執行的串行化。
下面代碼是爲表TestTransTable4加上表級排他鎖(TABLOCKX),並將其做用範圍設置爲整個事務期:
BEGIN TRAN T3 DECLARE @s varchar(10); -- 下面一條語句的惟一做用是對錶加排他鎖 SELECT @s = flight FROM TestTransTable4 WITH(HOLDLOCK,TABLOCKX) WHERE 1=2; PRINT '加鎖時間:'+CONVERT(varchar(30), GETDATE(), 20); WAITFOR DELAY '00:00:10' -- 事務等待10秒 PRINT '解鎖時間:'+CONVERT(varchar(30), GETDATE(), 20); COMMIT TRAN T3 進一步定義事務T4: BEGIN TRAN T4 DECLARE @s varchar(10); SELECT @s = flight FROM TestTransTable4; PRINT '數據查詢時間:'+CONVERT(varchar(30), GETDATE(), 20); COMMIT TRAN T4
Ø 與例13.10相似,分別在兩個查詢窗口中前後運行事務T3和事務T4(時間間隔要小於10秒),事務T3和T4輸出的結果分別如圖10.19和圖10.20所示。
Ø 事務T3經過利用TABLOCKX選項對錶TestTransTable4加上排他鎖之後,事務T4對該表的查詢操做只能在事務T3結束以後才能進行,其餘更新操做(如INSERT、UPDATE、DELETE)更是如此。所以,利用排他鎖能夠實現事務執行的串行化控制。