304441事務管理與併發控制

Zander_Zhao發表於2019-06-22

第10章事務管理與併發控制

•    10.1 事務的基本概念

 

10.1.1 事務

Ø 事務(Transaction)是構成單一邏輯工作單元的資料庫操作序列。這些操作是一個統一的整體,要麼全部成功執行(執行結果寫到物理資料檔案),要麼全部不執行(執行結果沒有寫到任何的物理資料檔案)。也可以這樣理解,事務是若干操作語句的序列,這些語句序列要麼全部成功執行,要麼全部都不執行。全部不執行的情況是:在執行到這些語句序列中的某一條語句時,由於某種原因(如斷電、磁碟故障等)而導致該語句執行失敗,這時將撤銷在該語句之前已經執行的語句所產生的結果,使資料庫恢復到執行這些語句序列之前的狀態。

【例子】對於銀行轉帳問題,可以表述為:將帳戶A1上的金額x轉到帳戶A2。這個操作過程可以用如圖10.1所示的流程表示。

 

•         如果轉帳程式在剛好執行完操作③的時刻出現硬體故障,並由此導致程式執行中斷,那麼資料庫就處於這樣的狀態:帳號A1中已經被扣除金額x(轉出部分),而帳號A2並沒有增加相應的金額x。也就是說,已經從帳號A1上轉出金額x,但帳號A2並沒有收到這批錢。顯然,這種情況在實際應用決不允許出現。

•         如果將上述操作①至⑤定義為一個事務,由於事務中的操作要麼全都執行,要麼全都不執行,那麼就可以避免出現上述錯誤的狀態。這就是事務的魅力。

 

 

10.1.2 事務的ACID特性

Ø 作為一種特殊的資料庫操作序列,事務的主要特性體現以下四個方面:

(1)原子性(Atomicity)

        事務是資料庫操作的邏輯工作單位。就操作而言,事務中的操作是一個整體,不能再被分割,要麼全部成功執行,要麼全部不成功執行。

(2)一致性(Consistency)

       事務的一致性是指出事務執行前後都能夠保持資料庫狀態的一致性,即事務的執行結果是將資料庫從一個一致狀態轉變為另一個一致狀態。

Ø  實際上,事務的一致性和原子性是密切相關的。

Ø  對於前面轉帳的例子,當操作操作③被執行後,出於某種客觀原因而導致操作④不能被執行時,如果操作③和④都是同一個事務中的操作,那麼由於事務具有原子性,所以操作①、②和③執行的結果也自動被取消,這樣資料庫就回到執行操作①前的狀態,從而保持資料庫的一致性。

Ø  資料庫的一致性狀態除了取決於事務的一致性以外,還要求在事務開始執行時的資料庫狀態也必須一致的。否則就算事務具有一致性,但在執行該事務後並不一定能夠保持資料庫狀態的一致性。

(3)隔離性(Isolation)

        隔離性是指多個事務在執行時不相互干擾的一種特性。事務的隔離性意味著一個事務的內部操作及其使用的資料對其他事務是不透明的,其他事務感覺不到這些操作和資料的存在,更不會干擾這些操作和資料。也就是說,事務的隔離性使系統中的每個事務都感覺到“只有自己在工作”,而感覺不到系統中還有其他事務在併發執行,

(4)永續性(Durability)

        永續性或稱永久性(Permanence),是指一個事務一旦成功提交,其結果對資料庫的改變將是永久的,即使是出現系統故障等問題。

事務的這四個特性通常被稱為事務的ACID特性。一個資料庫管理系統及其併發控制機制應該能確保這些特性不遭到破壞。

 

 

•    10.2 事務的管理

 

 

10.2.1 啟動事務

Ø 啟動事務方式有三種:顯式啟動、自動提交和隱式啟動。

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語句來實現,這將在下一節介紹。

 

 

10.2.2 終止事務

Ø 有啟動,就必有終止。

Ø 終止方法有兩種,一種是使用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處,即回滾到第三條插入語句執行之前,故第三條插入語句的執行結果被撤銷,其他插入語句的執行結果是有效的。

 

10.2.3 巢狀事務

Ø 事務是允許巢狀的,即一個事務內可以包含另外一個事務。當事務巢狀時,就存在多個事務同時處於活動狀態。

 

Ø 系統全域性變數@@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來回滾,那麼整個巢狀事務都被回滾,資料庫將回到巢狀事務開始時的狀態。

 

•    10.3 併發控制

 

10.3.1 併發控制的概念

Ø 資料共享是資料庫的基本功能之一。一個資料庫可能同時擁有多個使用者,這意味著在同一時刻系統中可能同時執行上百上千個事務。而每個事務又是由若干個資料庫操作構成的操作序列,如何有效地控制這些操作的執行對提高系統的安全性和執行效率有著十分重要的意義。

Ø 在單CPU系統中,事務的執行有兩種方式,一種是序列執行,一種是併發執行。序列執行是指每個時刻系統中只有一個事務在執行,其他事務必須等到該事務中所有的操作執行完了以後才能執行。這種執行方式的優點是方便控制,但其缺點卻是十分突出,那就是整個系統的執行效率很低。因為在序列方式中,不同的操作需要不同的資源,但一個操作一般不會使用所有的資源且使用時間長短不一,所以序列執行的事務會使許多系統資源處於空閒狀態。

Ø  如果能夠充分利用這些空閒的資源,無疑可以有效提高系統的執行效率,這是考慮事務併發控制的主要原因之一。另外,併發控制可以更好保證資料的一致性,從而實現資料的安全性。

Ø  在併發執行方式中,系統允許同一個時刻有多個事務在並行執行。這種並行執行實際上是通過事務操作的輪流交叉執行來實現的。雖然在同一時刻只有某一個事務的某一個操作在佔用CPU資源,但其他事務中的操作可以使用該操作沒有佔用的有關資源,這樣可以在總體上提高系統的執行效率。

Ø  對於併發執行的事務,如果沒有有效地控制其操作,就可能導致對資源的不合理使用,對資料庫而言就可能導致資料的不一致性和不完整性等問題。因此,DBMS必須提供一種允許多個使用者同時對資料進行存取訪問的併發控制機制,以確保資料庫的一致性和完整性。

Ø  簡而言之,併發控制就是針對併發執行的事務,如何有效地控制和排程其交叉執行的資料庫操作,使各事務的執行不相互干擾,以避免出現資料庫的不一致性和不完整性等問題。

 

 

10.3.2 幾種併發問題

        當多個使用者同時訪問資料庫時,如果沒有必要的訪問控制措施,可能會引發資料不一致等併發問題,這是誘發併發控制的主要原因。為進行有效的併發控制,首先要明確併發問題的型別,分析不一致問題產生的根源。

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特性,避免出現資料不一致性等併發問題。

 

 

10.3.3 基於事務隔離級別的併發控制

Ø 保證事務的隔離性可以有效防止資料不一致等併發問題。事務的隔離性有程度之別,這就是事務隔離級別。在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的程式碼:

-- 事務他的程式碼

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自動終止一個事務來解除)。隔離級別的方法並不能完全解決涉及的併發問題。

 

 

 

10.3.4 基於鎖的併發控制

Ø 鎖定是指對資料塊的鎖定,是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的程式碼如下:

-- 事務他的程式碼

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)更是如此。因此,利用排他鎖可以實現事務執行的序列化控制。

 

相關文章