本節書摘來自異步社群出版社《nosql權威指南》一書中的第1章,第1.4節,作者:【美】joe celko(喬•塞科) ,更多章節内容可以通路雲栖社群“異步社群”公衆号檢視。
悲觀并發控制假定沖突是預料之中的情況,必須警惕。在關系資料庫管理系統(relational database management system,rdbms)中最流行的模型是基于加鎖的。鎖是一種允許一個使用者會話對資源的通路同時保持或限制其他會話對同一資源的通路的裝置。每個會話可以針對資源獲得對應的鎖,對資源進行修改,然後在資料庫中<code>送出(commit)</code>或復原(<code>rollback</code>)相應的操作。<code>commit</code>語句将修改持久儲存,<code>rollback</code>語句将資料庫恢複到會話之前的狀态。如果修改遇到問題,系統也可以做一個rollback操作。這時,鎖會被釋放,其他會話可以通路對應的表或其他資源。
有各種各樣的“加鎖”,但sql的基本模型中有如下幾種事務之間可以互相影響的方式。
p0(髒寫)。事務t1修改一個資料項,此時另一個事務t2在事務t1執行commit或rollback之前也在修改同一資料項。如果t1或t2執行rollback,系統會不清楚正确的資料值應該是什麼。髒寫很不好,原因是這樣會違反資料庫的一緻性。假設在x和y之間有限制(如x = y),并且如果t1和t2單獨運作,它們會各自保持限制的一緻性。但是,當兩個事務以不同的順序寫入x和y時(這種情況隻有允許髒寫才會發生),限制就很容易被打破。
p1(髒讀)。事務t1修改了一行資料,然後事務t2在t1執行<code>commit</code>之前讀取該行資料。如果t1執行<code>rollback</code>,t2讀到的行是從未被“送出”的,是以可以認為是根本不存在的。
p2(不可重複讀)。事務t1讀取了一行資料,事務t2修改或删除了該行資料,并執行<code>commit</code>操作。那麼,如果t1嘗試重讀該行,則可能會收到修改後的值或者發現該行已被删除。
p3(幻讀)。事務t1讀取一組滿足某些搜尋條件的行n,然後事務t2執行了某些生成了滿足事務t1(使用的)所有搜尋條件的一行或多行。那麼,如果事務t1以同樣的搜尋條件重新讀,将會得到不同的行集合。
p4(丢失更新)。事務t1讀取了一個資料項後,事務t2更新相關的資料項(可能基于先前讀出的資料),然後事務t1(根據它較早讀取的值)來更新資料項,并執行<code>commit</code>,這時會發生異常的更新丢失。
這些現象并不總是壞事。如果資料庫僅用于查詢,或是在工作日期間不會進行任何修改,那麼就不會發生這些問題。如果資料庫系統不需要設法保護自己避免這些問題,那麼它将運作得更快。在某些情況下對資料進行更改也是可以接受的。
想象一下,有一張表,表中存儲着世界上所有的汽車。我想執行一個查詢,找到紅色跑車司機的平均年齡。這個查詢需要花一定的時間來運作,在此期間,汽車會報廢、買入或賣出,新車會被生産出來,等等。但是,我接受符合p1~p3這3個現象的情況,因為平均年齡從我開始查詢到完成查詢的時候不會改變太多。第二位小數的變化并不重要。
可以通過設定事務隔離級别防止這些現象發生,這就是系統使用鎖的方法。原始的ansi模型僅包括p1、p2和p3。其他定義最早出現在由hal berenson和他的同僚(1995年)撰寫的微軟研究院技術報告msr-tr-95-51“ansi sql隔離級别的批判”中。
1.4.1 隔離級别
在标準sql中,使用者可以在會話中設定事務的隔離級别。隔離級别可以避免剛才談到的一些現象,并給資料庫一些其他資訊。下面是<code>set transaction</code>語句的文法:
可選的<code><diagnostics size></code>子句告訴資料庫設定給定大小的錯誤資訊清單。這是标準sql功能之一,是以在個别的産品中可能沒有這個功能。其原因是,單條語句中可能存在多個錯誤,資料庫引擎應該發現這些錯誤,并支援在宿主程式中通過<code>get diagnostics</code>語句在診斷區報告這些錯誤。
<code><transaction access mode></code>子句是自解釋的。<code>readonly</code>選項表示這是一個查詢,讓sql引擎可以放松一些(不用考慮沖突等問題)。<code>read write</code>選項告知sql引擎,資料行可能會被修改,它必須注意上面提到的3個現象。
在目前大多數sql産品中都實作的重要子句是<code><isolation level></code>。事務的隔離級别定義了一個事務的操作允許受到并發事務影響的程度。事務的預設隔離級别是<code>serializable</code>,但使用者可以在<code>set transaction</code>語句中明确地設定隔離級别。
每個隔離級别都確定每個事務将完全執行或完全不執行,而且不會有更新操作會丢失。當sql引擎檢測到無法保證兩個或兩個以上并發事務的串行化時,或當它檢測到發生了不可恢複的錯誤時,可以自行啟動<code>rollback</code>語句。
來看一下表1-1。表1-1展示了隔離級别和3個現象。yes代表該現象在對應的隔離級别下是可能發生的。

在表1-1中:
<code>serializable</code>隔離級别是保證那些不得不并發執行的事務與它們在串行順序執行情況下産生同樣的結果。事務串行執行是指一個事務執行完成後才開始下一個事務執行。通路資料庫的使用者就像是在排隊等候獲得對資料庫的完整通路。
<code>repeatableread</code>隔離級别是保證在使用者會話期間為使用者維護資料庫的相同鏡像。
<code>readcommitted</code>隔離級别允許目前會話中的事務能夠讀到會話運作期間其他事務已經送出的行。
<code>readuncommitted</code>隔離級别允許目前會話中的事務能夠讀到會話運作期間其他事務建立但不一定送出的資料。
不管事務隔離級别是哪種,在執行語句、檢查完整性限制、執行與引用限制相關的引用操作等隐含讀取模式(schema)定義期間,現象p1、p2和p3都是不應該出現的。我們不希望模式自己針對不同使用者發生變化。
我們已經讨論了ansi/iso模型,但廠商往往會實作一些專有的隔離級别。我們需要知道它們的工作原理,以便在工作中使用這些産品。ansi/iso在會話級别為整個模式設定隔離級别。專有模型可能會允許程式員用額外的文法配置設定表級鎖。微軟公司使用的文法有這樣的提示清單:
<code>select.. from <base table> with (<hint list>)</code>
該模型可以采用行級鎖或表級鎖。如果它們采用表級鎖,可以得到與ansi/iso一緻的特性。例如,with (holdlock)相當于<code>serializable</code>,但它僅适用于指定的表和視圖,且隻适用于由正在使用的語句定義的事務的運作期間。
用“讀者”(reader)和“寫者”(writer)的概念是解釋各種模式的最簡單的方法。讀者和寫者這兩個名字無需解釋。
在oracle中,寫者是彼此阻塞的,資料将保持被鎖定狀态,直到<code>commit</code>、<code>rollback</code>或不儲存資料停止會話為止。如果兩個使用者試圖同時編輯同一資料,當第一個使用者完成操作資料後,資料便被鎖定。鎖繼續被保持,即使這個使用者正在處理其他資料。
讀者不會阻塞寫者:讀取資料庫的使用者不會在任何隔離級别阻止其他使用者修改同一資料。但db2和informix有所不同。例如,在oracle中,寫者會阻塞寫者,但在db2和informix,寫者在<code>uncommitted read</code>以上的任何隔離級别都會禁止其他使用者讀同一資料。在較高的隔離級别,鎖定資料直到儲存或復原被編輯的資料,可能會導緻并發問題。如果你正處在一個編輯會話中,其他任何會話都不能讀你已鎖定在編輯的資料。
讀者阻塞寫者:在db2和informix中,在<code>uncommitted read</code>以上的任何隔離級别讀者都會禁止其他使用者修改同一資料。應用程式在dbms中打開遊标,每次讀取一行,一邊周遊結果集一邊處理資料,隻有在這樣的應用程式中讀者才能真正阻塞寫者。在這種情況下,db2和informix開始擷取鎖,并在處理結果集的時候持有鎖。
在postgresql中,直到更改該行的第一個事務被送出給資料庫或復原的時候,行才會被更新。當兩個使用者試圖同時編輯同一資料時,第一個使用者會阻塞其他使用者更新該行。直到該使用者儲存了修改,送出了對資料庫的更改,或在不儲存的情況下停止該編輯會話(復原所有在該編輯會話中進行的編輯操作),其他使用者才能編輯該行。如果使用postgresql的多版本并發控制(mvcc)(這是預設和推薦的方式),寫入資料庫的事務操作不會阻塞讀者查詢該資料庫。不論使用預設的<code>read committed</code>隔離級别還是設定隔離級别為<code>serializable</code>,這都是成立的。讀者不會阻塞寫者:無論在資料庫中設定哪個隔離級别,讀者都不鎖定資料。