淺嘗數據庫併發控制

        數據庫是一個共享資源,可以供多個用戶使用。然而,對於大多數程序員來說,單處理機系統是我們接觸最多的系統,運行在其上的數據庫事務也並非真正意義上的並行,實際上它是這些並行事務的並行操作輪流交叉運行,這種並行執行方式稱爲交叉併發方式(Interleaved Concurrency),這樣可以減少處理機的空閒時間,提高系統的效率。而在多處理機系統中,每個處理機可以運行一個事務,多個處理機可以同時運行多個事務,實現真正意義上的並行運行,而這種並行執行方式,我們稱之爲同時並行方式(Simultaneous Concurrency)。在這篇文中,我將自己在開發中遇到的數據庫併發控制的程序設計經驗介紹給大家(當然了,這裏我用的是單處理機,並且拋棄具體的編程語言,只從SQL上進行詳細講解),希望能夠拋磚引玉。

       我們都知道,事務是併發控制的基本單位,保證事務的ACID(Atomicity,Consistency,Isolation,Durability)特性是事務處理的重要任務。而程序員在具體的程序設計中,又是如何做到的呢?市面上目前流行的數據庫書籍或者是針對某種編程語言的項目實踐指南,都只是從理論上對我們進行一番說教,可真正到了實戰中,往往三言兩語,整的我們大家往往丈二和尚摸不着頭腦。天花亂墜猛砍一頓,可是實際應用中又是如何呢,在這,我將帶領大家一起分享我的經驗,一起進入神祕的併發控制之旅吧!(對了,在這,我用的數據庫主要是MySQL,並配之以MS SQL Server,Oracle,PostgreSQL比較講解!)

       仔細分析併發操作給數據庫帶來的不一致,我將其分爲三類(注意,大家也許見過不同的分類,但萬變不離其宗,在這,也是爲了後面文章敘述方便,按照自己的理解將其歸類):第一類,丟失修改;第二類,不可重複讀;第三類,髒讀。下面,我將結合實例,帶大家一起看看具體的數據庫操作中,這些不一致現象是如何產生的,再將應對措施介紹給大家。


        如上圖,我們簡單地模擬一下火車站售票系統的數據庫,表seat只包含三列,分別代表座位編號,座位位置,座位訂票人。

        現在我們來看看什麼是丟失修改(Lost Update),假設訂票員X查看哪個座位空閒,這時,她開啓一個事務操作,如下所示:


       在此期間,訂票員Y處理了另一個事務,他發出了相同的查詢,並得到了相同的響應,馬上,他決定訂下座位1:


      訂票員X動作只比Y略微慢了一點,幾秒鐘之後,她開始訂票:

     

        怎麼樣,從圖中,我們很明顯看到訂票員Y做的訂票操作丟失了,這就是所謂的丟失修改。當然,這是一個嚴重的編程錯誤。那麼我們怎麼解決這種數據庫操作帶來的不一致呢?就目前,主流的解決方案有兩種,一種就是將數據庫事務隔離級別設置到Serializable,當然,這種方式我是不推薦使用的,原因很簡單,隔離級別指明瞭在事務中如何獲得鎖(恩,什麼是鎖?稍安勿躁,等會我會詳細告知),隔離級別越高,帶來的性能損失越大,這個在大數據集,高併發應用中,將會體現的淋漓盡致。第二種就是所謂的悲觀鎖機制,在Oracle,MySQL和PostgreSQL中,你可以使用如下語句:



         在MS SQL Server中,語句略有不同,如下:

        在這種情況下,訂票員X先運行select…for update ,鎖住了所有可用座位,這時,訂票員Y運行select… for update時,被強制等待,直到X運行提交爲止。當然,如果你想控制的更爲精細,即訂票員X在訂前排位置,而訂票員Y在訂後排位置時,訂票員Y能同時處理訂票,可以使用如下語句:

    select chaired from seat where booked is null and location=’front’ for update

        一個良好的算法或許是訂票員X訂中心線左邊的座位,Y訂中心線右邊的座位,以這種方式行事,只有在座位快滿員時纔可能發生阻塞。
        好了,來看看,數據庫不一致的第二種情況(Non-repeatable Read),不可重複讀包括三種情況:分別對應着DML的Insert,Delete,Update三種操作,其中前兩種,在有些書籍上又稱之爲幻影讀(Phantom read),爲了演示這個特效,需要對MySQL的默認設置進行修改(set transaction isolation level read committed),爲了節省時間,我在這就不貼效果圖了。如果我們使用Oracle或者是MS SQL Server的話,在不修改任何配置的情況下,就可以看到效果。原因在於其默認的事務隔離級別是read committed。
        同理,要想看到髒讀,也需要進行設置(set transaction isolation level read uncommitted),如下圖所示:



        首先,訂票員Z發現chairId的座位沒有別人預定,於是她發出如下語句:

           這時,訂票員X開始發出如下語句,

             這時,訂票員Z回滾操作,原因是有人退票了。那麼訂票員X讀到的數據就屬於髒數據(因爲booked列還是Z,與實際不符)。

            下面使用樂觀鎖進行一些實驗:



         在這裏,引入新的一列(這裏是updateid),作爲版本控制。現在我們來看看如何現實生活中的例子吧。比方說訂票員X要求保留兩個座位,訂票員則要求三個座位。爲了確保三個座位可用,我們可以使用如下方式:



          這個結果表明,三個在一起的座位,可以從座位號爲1訂起,分別爲1,2,3。或者是從4開始訂起(記住我們一共有6個座位),即4,5,6。
          好了,在訂票員X和Y的兩次訂票過程中,updateid作爲版本控制字段被返回,並且在任何後繼的select和update操作中使用。這樣,如果訂票員X需要預訂兩個座位,那麼她將做出如下操作:

        如上所示,訂票員X操作後2 rows affected,這是正確的座位個數。因此,通過提交完成該事務。如果訂票員Y此刻(注意:前一時刻 – 也就是在x還未提交更新數據前,Y也開啓了一個事務,並發現連續的三個位置可以從1開始),準備預訂3個座位,那麼他的操作將如上圖上所示,他應該能更新三條數據,但是受影響的只有一條,此刻,回滾事務,並且重新啓動一個事務處理過程。如下圖所示:

       如此,我們執行預訂三個座位的操作,如下:

        至此,樂觀鎖的實戰我們也看完了。在實際應用中,可以通過有效地算法,劃分不同區域,使得搜索從不同空間開始,這樣可以避免上述併發帶來的一系列問題。


備註:
(1) Oracle中關閉自動提交功能的命令如下:set autocommit off。
(2) MySQL中,MyISAM數據庫引擎不支持事務,所以我們可以在create table語句中指定engine=InnoDB來確保在指定表上應用事務。在MySQL中,我們可以這樣開始一個事務:start transaction;
(3) 在PostgreSQL中,autocommit也稱作鏈接模式(Chained Model),這是默認模式,需要使用begin trasaction開啓事務。
(4) MS SQL Server也使用與PostgreSQL相同的語句開啓事務。

(5) 幻讀(Phantom Read):當兩個一致的查詢在一個事務內執行,第二個查詢返回的行集不同於第一個查詢返回的行集時,就發生了幻讀。(主要與delete和insert操作有關)。
(6) 不可重複讀(Nonrepeatable Read):當一個事務中的一個查詢從數據庫讀取數據,但隨後同一個事務的第二個操作欲查詢相同數據時,卻發現數據被另一個已經提交的事務修改,就發生了不可重複讀。(和update操作有關)。
(7) 髒讀(Dirty Read):除了可能讀到第二個事務中未提交的數據外,髒讀幾乎與不可重複讀完全一樣。
(8) 如何查詢當前數據庫事務隔離級別:mysql中我們需要使用下面的語句:select @@tx_isolation。postgresql中則需要輸入:show transaction isolation level。ms sql server中則需要輸入:set transaction isolation level serializable。Oracle 中則需要使用v$session和v$transaction的組合得到隔離級別。

發佈了69 篇原創文章 · 獲贊 127 · 訪問量 13萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章