概述
SQLite雖然是一個輕量的嵌入式數據庫,但這並不影響它支持事務。所謂支持事務,即需要在並發環境下,保持事務的ACID特性。事務的原子性,隔離性都需要通過並發控制來保證。那么Sqlite的並發控制是怎樣的,如何實現,在這里跟大家分享下我的理解。
SQLite是一個文件數據庫,所有的數據都在一個db文件中,對於wal模式,還包含wal索引文件和wal日志文件。SQlite支持庫級並發,即允許多個讀事務同時運行,同一時刻最多只有一個寫事務,讀寫沖突,相對於傳統的DBMS支持表級,行級甚至MVCC,SQLite的庫級並發確實顯得比較寒磣。但是鎖粒度越細,意味着維護鎖的成本越高,系統也會越復雜,因此SQLite的封鎖機制要簡單很多,對資源的消耗也非常少。SQLite 3.7版本后,對並發控制做了優化,推出了WAL日志模式,可以實現讀寫並發,但同一個時刻仍然只能有一個寫事務。由於SQLite的實現方式,SQLite只支持兩種隔離級別,串行化和讀未提交。讀未提交,就是讀全程不上鎖;串行化在事務開啟時上讀鎖,上鎖和釋放鎖同樣遵守兩階段鎖協議,在事務提交或回滾時才釋放鎖。
文件鎖
要說清楚SQLite鎖實現機制,首先要了解文件鎖,因為SQLite所有鎖實現都是基於文件鎖。對於Linux系統,文件鎖主要包含兩類,協同鎖和強制鎖,協同鎖類似於互斥量,需要參與者都遵守游戲規則,在操作文件前,都先上鎖,而強制鎖由OS內核強制實行。協同鎖根據鎖粒度分為文件級別和范圍級別。鎖文件是最簡單的對文件加鎖的方法,每個需要加鎖的數據文件都有一個鎖文件(lock file)。當鎖文件存在時,就認為該數據文件已經被加鎖,別的進程不應該訪問。當鎖不存在,進程就可以創建一個鎖文件,然后訪問相應的數據文件。只要創建鎖的過程是原子的,就能保證某一時刻只有一個進程擁有該鎖,這種方法保證某一時刻只有一個進程訪問文件。文件鎖的弊端顯而易見,並發粒度太低。范圍鎖相對於文件鎖,可以鎖文件的一部分內容,並且有讀鎖和寫鎖。對於同一部分內容,讀鎖可以共存,讀鎖和寫鎖互斥。POSIX標准提供接口fcntl()來實現。
鎖類型
SQLite中的鎖正是利用了范圍鎖來實現並發控制的目的。SQLite中主要包含了4種鎖:共享鎖(SHARED_LOCK)、保留鎖(RESERVED_LOCK)、未決鎖(PENDING_LOCK)和排它鎖(EXCLUSIVE_LOCK),這4種鎖定義了3個區域,其中共享鎖和排它鎖占用文件相同的區域。具體而言,SQLite定義了文件的以下區域為鎖文件區域,由於fcntl可以對不存在的文件區域加鎖,因此 PENDING_BYTE定位在區域1G的地方,即使DB文件沒這么大也不影響。三種類型的鎖,分別在1G,1G+1,1G+2的偏移處,之所以SHARED_SIZE長度是510,原因在於windows環境下,LockFile()加鎖區域不能重疊(Linux沒有這種問題),對於同一個字節上鎖會影響並發,因此設置了一個范圍,對SHARED_FIRST—SHARED_FIRST+ SHARED_SIZE范圍內的隨機數進行加鎖,這樣可以減少沖突,保證高效的讀取文件。具體鎖類別和說明參見表1
鎖類別 |
字節范圍 |
說明 |
PENDING_BYTE |
0x40000000 |
一種過渡鎖,讀事務獲取讀鎖,寫事務獲取寫鎖前,都需要獲取該鎖。 |
RESERVED_BYTE |
0x40000001 |
表示線程要開始寫操作,某一時刻只能有一個RESERVED Lock,但是RESERVED鎖和SHARED鎖可以共存,而且可以對數據庫加新的SHARED鎖。 |
SHARED_LOCK |
0x40000002-0x40000200 |
共享鎖,開啟事務時,都需要獲取該鎖 |
EXCLUSIVE_LOCK |
0x40000002-0x40000200 |
排它鎖 |
表1
從各個鎖的作用來看,不免會疑問,為啥要加上RESERVED_LOCK和PENDING_LOCK兩種類型,直接通過共享鎖和排它鎖不就可以達到讀讀共享,讀寫互斥的目的了嗎。這里引入這Reserved鎖的目的是為了提高並發。由於SQLite只有庫級排斥鎖(EXCLUSIVE LOCK),如果寫事務一開始就上EXCLUSIVE鎖,然后再進行實際的數據更新,寫磁盤操作,這會使得並發性大大降低。而SQLite一旦得到數據庫的RESERVED鎖,就可以對緩存中的數據進行修改,而與此同時,其它進程可以繼續進行讀操作。直到真正需要寫磁盤時才對數據庫加EXCLUSIVE鎖。Pending鎖的作用主要是為了防止寫餓死的情況,寫事務獲取Pending鎖后,新的讀事務無法再進來,然后再加EXCLUSIVE鎖,這樣寫事務獲取鎖的幾率大大提高,讀寫事務的流程如下表2,狀態變遷圖如圖1。
類型 |
操作 |
鎖信息 |
說明 |
讀事務 |
begin |
|
不持有鎖 |
select c1 from user where id=1 |
Lock: Pending(Read) Lock:Shared(Read) Unlock:Pending |
獲取Shared讀鎖前,需要先獲取Pending共享鎖, 通過這種方式與寫事務互斥。 |
|
commit |
UnLock:Shared |
|
|
寫事務 |
begin |
|
|
Update c1=c1+1 where id=1 |
Lock: Pending(Read) Lock:Shared Unlock:Pending Lock:Reserved(Write) |
先獲取Shared讀鎖,然后獲取Reserved的排它鎖,阻止其它寫事務 |
|
commit |
Lock:Pending(Write) Lock:Exclusive(Write) Unlock: Pending Unlock: Exclusive(Write) |
獲取Pending的排它鎖,阻止新的讀事務,最后上排它鎖,阻止所有讀事務,讀寫不能並發 Pending鎖方式好處是,減少寫餓死的幾率。 |
表2
圖1
Wal鎖類型
引入WAL機制后,SQLite開始支持讀寫並發,並且引入了WAL日志文件鎖。WAL日志鎖實質是鎖wal-index文件的區域,根據不同的鎖類型,將wal-index文件的不同區域划定義成不同的鎖,主要有讀鎖,寫鎖,檢查點鎖,具體如表3,4。WAL模式下,最新的數據位於日志文件中,無論是讀事務還是寫事務都需要持有WAL_READ_LOCK的讀鎖,因為它們都需要獲取最新的事務點。因此,做檢查點時,可以通過對WAL_READ_LOCK位置(124-127)上鎖,來確定檢查點需要等待還是停止推進。同時我們也可以看到,對於DB文件,讀寫事務都只需要對DB文件上讀鎖,對於WAL日志文件,WAL_READ_LOCK和WAL_WRITE_LOCK位於不同的位置,讀寫相互不影響,所以讀寫不互斥。
鎖類別 |
字節范圍 |
說明 |
|||
讀事務(WAL) |
begin |
|
|
||
select c1 from user where id=1 |
DB文件: Lock: Pending(Read) Lock:Shared Unlock:Pending WAL文件: Lock:WAL_READ_LOCK(Read) |
除了獲取DB文件鎖,還需要獲取WAL鎖,得到最新提交事務的位點。 若有事務再作檢查點,需要重試多次。 |
|||
commit |
Unlock:WAL_READ_LOCK Unlock:Shared |
|
|||
寫事務(WAL) |
begin |
|
|
||
Update c1=c1+1 where id=1 |
DB文件: Lock: Pending-Read Lock:Shared(Read) Unlock:Pending WAL文件: Lock:WAL_READ_LOCK(Read) Lock:WAL_WRITE_LOCK(Write) |
通過 EXCLUSIVE-WRITE-LOCK控制寫寫並發 由於不操作DB文件,因此不存在讀寫沖突,讀寫可以並發。 |
|||
commit |
WAL文件: Lock:SHARED-READ-LOCK Unlock:WAL_READ_LOCK(Read) Unlock: WAL_WRITE_LOCK(Write)
DB文件: Unlock:Shared |
獲取SHARED-READ-LOCK目的是為了獲取最新提交日志的位點 |
|||
檢查點 操作 (WAL)
|
|
WAL文件: Lock:WAL_CKPT_LOCK(write) Lock:WAL_READ_LOCK(write) UnLock:WAL_READ_LOCK UnLock:WAL_CKPT_LOCK |
EXCLUSIVE-CKPT-LOCK WAL_READ_LOCK阻止讀寫事務。 |
表3
鎖類別 |
字節范圍 |
說明 |
WAL_WRITE_LOCK |
120 |
寫鎖位置 |
WAL_CKPT_LOCK |
121 |
檢查點鎖位置 |
WAL_RECOVER_LOCK |
122 |
故障恢復鎖位置 |
WAL_READ_LOCK |
123 |
讀鎖(表示不需要wal文件) |
|
124-127 |
讀鎖(每個位置,對應一個鎖) 做檢查點時,逐一對每個位置上寫鎖,若上鎖失敗表示對應位置上的讀事務沒有結束,根據檢查點策略確定是等待(FULL),還是停止推進(PASSIVE)。 |
表4
調試
SQLite通過幾個宏定義可以打印語句執行的鎖信息,方便大家了解語句執行中加了哪些鎖,什么時候加的,什么時候釋放的,以及如何處理鎖沖突。具體的宏包括SQLITE_LOCK_TRACE,SQLITE_FORCE_OS_TRACE,和SQLITE_DEBUG,具體可以在代碼中查看宏定義的注釋。
gcc sqlite3.c -g -lpthread -ldl -fPIC -shared -DSQLITE_TEST -DSQLITE_DEBUG -DSQLITE_LOCK_TRACE -DSQLITE_FORCE_OS_TRACE -o libsqlite3.so
參考文檔
http://my.oschina.net/u/587236/blog/129022
http://www.cnblogs.com/hustcat/archive/2009/03/01/1400757.html