SQLITE3的鎖以及事務


以下內容摘自《SQLITE權威指南》,下載地址http://download.csdn.net/detail/cxjchen/5643391

 

SQLITE的鎖

在SQLite中,鎖和事務是緊密聯系的。為了有效地使用事務,需要了解一些關於如何加鎖的知識。
SQLite采用粗放型的鎖。當一個連接要寫數據庫,所有其它的連接被鎖住,直到寫連接結束了它的事務。SQLite有一個加鎖表,來幫助不同的寫數據庫都能夠在最后一刻再加鎖,
以保證最大的並發性。
SQLite使用鎖逐步上升機制,為了寫數據庫,連接需要逐級地獲得排它鎖。SQLite有5個不同的鎖狀態:未加鎖(UNLOCKED)、共享(SHARED)、保留(RESERVED)、未決(PENDING)和排它(EXCLUSIVE)。每個數據庫連接在同一時刻只能處於其中一個狀態。每種狀態(未加
鎖狀態除外)都有一種鎖與之對應。
最初的狀態是未加鎖狀態,在此狀態下,連接還沒有存取數據庫。當連接到了一個數據庫,甚至已經用BEGIN開始了一個事務時,連接都還處於未加鎖狀態。未加鎖狀態的下一個狀態是共享狀態。為了能夠從數據庫中讀(不寫)數據,連接必須首先進入共享狀態,也就是說首先要獲得一個共享鎖。多個連接可以同時獲得並保持共享鎖,也就是說多個連接可以同時從同一個數據庫中讀數據。但哪怕只有一個共享鎖還沒有釋放,也不允許任何連接寫數據庫。

如果一個連接想要寫數據庫,它必須首先獲得一個保留鎖。一個數據庫上同時只能有一個保 留鎖。保留鎖可以與共享鎖共存,保留鎖是寫數據庫的第1階段。保留鎖即不阻止其它擁有 共享鎖的連接繼續讀數據庫,也不阻止其它連接獲得新的共享鎖。 一旦一個連接獲得了保留鎖,它就可以開始處理數據庫修改操作了,盡管這些修改只能在
緩沖區中進行,而不是實際地寫到磁盤。對讀出內容所做的修改保存在內存緩沖區中。 當連接想要提交修改(或事務)時,需要將保留鎖提升為排它鎖。為了得到排它鎖,還必須首
先將保留鎖提升為未決鎖。獲得未決鎖之后,其它連接就不能再獲得新的共享鎖了,但已經擁有共享鎖的連接仍然可以繼續正常讀數據庫。此時,擁有未決鎖的連接等待其它擁有共享鎖的連接完成工作並釋放其共享鎖。
一旦所有其它共享鎖都被釋放,擁有未決鎖的連接就可以將其鎖提升至排它鎖,此時就可以自由地對數據庫進行修改了。所有以前對緩沖區所做的修改都會被寫到數據庫文件。

 

事務的種類

SQLite有三種不同的事務,使用不同的鎖狀態。事務可以開始於:DEFERRED、MMEDIATE或EXCLUSIVE。事務類型在BEGIN命令中指定:
BEGIN [ DEFERRED | IMMEDIATE | EXCLUSIVE ] TRANSACTION;
一個DEFERRED事務不獲取任何鎖(直到它需要鎖的時候),BEGIN語句本身也不會做什么事情——它開始於UNLOCK狀態。默認情況下就是這樣的,如果僅僅用BEGIN開始一個事
務,那么事務就是DEFERRED的,同時它不會獲取任何鎖;當對數據庫進行第一次讀操作時,它會獲取SHARED鎖;同樣,當進行第一次寫操作時,它會獲取RESERVED鎖。由BEGIN開始的IMMEDIATE 事務會嘗試獲取RESERVED鎖。如果成功,BEGIN IMMEDIATE保證沒有別的連接可以寫數據庫。但是,別的連接可以對數據庫進行讀操作;但是,RESERVED鎖會阻止其它連接的BEGIN IMMEDIATE或者BEGIN EXCLUSIVE命令,當其它連接執行上述命令時,會返回SQLITE_BUSY錯誤。這時你就可以對數據庫進行
修改操作了,但是你還不能提交,當你COMMIT時,會返回SQLITE_BUSY錯誤,這意味着還有其它的讀事務沒有完成,得等它們執行完后才能提交事務。

EXCLUSIVE事務會試着獲取對數據庫的EXCLUSIVE鎖。這與IMMEDIATE類似,但是一旦成功,EXCLUSIVE事務保證沒有其它的連接,所以就可對數據庫進行讀寫操作了。
上節那個例子的問題在於兩個連接最終都想寫數據庫,但是它們都沒有放棄各自原來的鎖最終,SHARED鎖導致了問題的出現。如果兩個連接都以BEGIN IMMEDIATE開始事務,
那么死鎖就不會發生。在這種情況下,在同一時刻只能有一個連接進入BEGIN IMMEDIATE,其它的連接就得等待。BEGIN IMMEDIATE和BEGIN EXCLUSIVE通常被寫
事務使用。就像同步機制一樣,它防止了死鎖的產生。基本的准則是:如果你正在使用的數據庫沒有其它的連接,用BEGIN就足夠了。但是,如果你使用的數據庫有其它的連接也會對數據庫進行寫操作,就得使用BEGIN IMMEDIATE或BEGIN EXCLUSIVE開始你的事務。

 

事務的生命周期

有一些關於代碼和事務的問題需要關注。首先需要知道哪個對象運行在哪個事務之下。另一個問題是持續時間——一個事務何時開始,何時結束,從哪一點開始影響其它連接?第一
個問題與API直接關聯,第二個與SQL的一般概念及SQLite的特殊實現關聯。
一個連接(connection)可以包含多個語句(statement),而且每個連接有一個與數據庫關聯的B-tree和一個pager。Pager在連接中起着很重要的作用,因為它管理事務、鎖、內存緩沖以及負責崩潰恢復(crash recovery)。當你進行數據庫寫操作時,記住最重要的一件事:在任何時候,只在一個事務下執行一個連接。這回答了第一個問題。
關於第二個問題,一般來說,一個事務的生命周期和語句差不多,你也可以手動結束它。默認情況下,事務自動提交,當然你也可以通過BEGIN..COMMIT手動提交。接下來的問題
是事務如何與鎖關聯。

 

鎖的狀態

大多數情況下,鎖的生命周期在事務的生命周期之中。它們不一定同時開始,但總時同時結束。當你結束一個事務時,也會釋放它相關的鎖。或者說,鎖直到事務結束或系統崩潰時才會釋放。如果系統在事務沒有結束的情況下崩潰,那么下一個訪問數據庫的連接會處理這種情況。

讀事務

我們先來看看SELECT語句執行時鎖的狀態變化過程,非常簡單:一個連接執行SELECT語句,觸發一個事務,從UNLOCKED到SHARED,當事務COMMIT時,又回到UNLOCKED,就這么簡單。
那么,當你運行兩個語句時會發生什么呢?這時如何加鎖呢?這依賴於你是否運行在自動提交狀態。考慮下面的例子(為了簡單,這里用了偽碼):
db = open('foods.db')
db.exec('BEGIN')
db.exec('SELECT * FROM episodes')
db.exec('SELECT * FROM episodes')
db.exec('COMMIT')
db.close()
由於顯式地使用了BEGIN和COMMIT,兩個SELECT命令在一個事務下執行。第一個exec()執行時,連接處於SHARED,然后第二個exec()執行。當事務提交時,連接又從
SHARED回到UNLOCKED狀態,狀態變化如下:

UNLOCKED→PENDING→SHARED→UNLOCKED
如果沒有BEGIN和COMMIT兩行,兩個SELECT都運行於自動提交狀態,狀態變化如下:
UNLOCKED→PENDING→SHARED→UNLOCKED→PENDING→SHARED→UNLOCKED
僅僅是讀數據,但在自動提交模式下,卻會經歷兩個加解鎖的循環,太麻煩。而且,一個寫進程可能插到兩個SELECT中間對數據庫進行修改,這樣,你就不能保證第二次能夠讀到
同樣的數據了,而使用BEGIN..COMMIT就可以有此保證。

 

寫事務

下面我們來考慮寫數據庫,比如UPDATE。和讀事務一樣,它也會經歷UNLOCKED→PENDING→SHARED的變化過程,但接下來就會看到PENDING鎖是如何起到關口作用的了。

保留(RESERVED)狀態

當一個連接(connection)要向數據庫寫數據時,從SHARED狀態變為RESERVED狀態。如果它得到RESERVED鎖,也就意味着它已經准備好進行寫操作了。即使它沒有把修改寫入數據庫,也可以把修改保存到位於pager的緩沖區中(page cache)了。
當一個連接進入RESERVED狀態,pager就開始初始化回卷日志(rollback journal)。回卷日志是一個文件,用於回卷和崩潰恢復,見圖5-1。在RESERVED狀態下,pager管理着三種頁:
(1)已修改的頁:包含被B-tree修改的記錄,位於page cache中。
(2)未修改的頁:包含沒有被B-tree修改的記錄。
(3)日志頁:這是修改頁以前的版本,日志頁並不存儲在page cache中,而是在B-tree修改頁之前寫入日志。
Page cache非常重要,正是因為它的存在,一個處於RESERVED狀態的連接可以真正的開始工作,而不會干擾其它的(讀)連接。所以,SQLite可以高效地處理在同一時刻的多個讀連接和一個寫連接。

未決(UNPENDING)狀態

當一個連接完成修改,需要真正開始提交事務時,執行該過程的pager進入EXCLUSIVE狀態。從RESERVED狀態開始,pager試着獲取PENDING鎖,一旦得到,就獨占它,不允
許任何其它連接獲得PENDING鎖。既然寫操作持有PENDING鎖,其它任何連接都不能從UNLOCKED狀態進入SHARED狀態,即不會再有新的讀進程,也不會再有新的寫進程。
只有那些已經處於SHARED狀態的連接可以繼續工作。而處於PENDING狀態的寫進程會一直等到所有這些連接釋放它們的鎖,然后對數據庫加EXCUSIVE鎖,進入EXCLUSIVE
狀態,獨占數據庫。

排它(EXCLUSIVE)狀態

在EXCLUSIVE狀態下,主要的工作是把修改的頁從page cache寫入數據庫文件,這是真正進行寫操作的地方。在pager將修改頁寫到文件之前,還必須先處理日志。它檢查是否所有的日志都寫入了磁盤,因為它們可能還位於操作系統的緩沖區中。所以pager得告訴OS把所有的文件寫入磁盤,這與synchronous pragma所做的工作相同,如第4章所述。
日志是數據庫進行恢復的惟一方法,所以日志對於DBMS非常重要。如果日志頁沒有完全寫入磁盤而發生崩潰,數據庫就不能恢復到它原來的狀態,此時數據庫就處於不一致狀態。
日志寫盤完成后,pager就把所有的修改頁寫入數據庫文件。接下來做什么取決於事務提交的模式,如果是自動提交,那么pager清理日志、page cache,然后由EXCLUSIVE進入
UNLOCKED。如果是手動提交,那么pager繼續持有EXCLUSIVE鎖和回卷日志,直至遇到COMMIT或者ROLLBACK。
總之,出於性能方面的考慮,進程占有排它鎖的時間應該盡可能的短,所以DBMS通常都是在真正寫文件時才會占有排它鎖,這樣能大大提高並發性能。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM