sqlite事务模型、性能优化tips、常见误区
0,前言
本文主要介紹sqlite的事務模型,以及基于事務模型的一些性能優化tips,包括事務封裝、WAL+讀寫分離、分庫分表、page size優化等。并基于手淘sqlite的使用現狀總結了部分常見問題及誤區,主要集中在多線程的設置、多線程下性能優化的誤區等。本文先提出以下幾個問題(作者在進行統一存儲的關系存儲框架優化過程中一直困惑的問題,同時也是客戶端開發者經常搞錯的問題)并在正文中進行解答:
- 1,sqlite的多進程安全是怎么實現的?性能如何?
- 2,sqlite的數據庫連接是什么?
- 3,言sqlite必提的讀寫分離,具體指什么?能不能提升數據讀寫的性能?為什么
- 4,sqlite提供的WAL特性解決了什么問題?
- 5,sqlite的多線程設置是為了解決什么問題?與讀寫分離有什么關系?
- 6,什么情況下數據庫會發生死鎖?
- 7,有哪些性能優化的方案?
1,sqlite主要數據結構
在深入了解sqlite之前,最好先對sqlite的主要數據結構有個概要的理解,sqlite是一個非常完備的關系數據庫系統,由很多部分組成(parser,tokenize,virtual machine等等),同時sqlite的事務模型相對簡化,是入門學習關系數據庫方法論的一個不錯的選擇;下文對事務模型的分析也基于這些核心數據結構。下面這張圖比較準確的描述了sqlite的幾個核心數據結構:
1.1 Connection
connection通過sqlite3_open函數打開,代表一個獨立的事務環境(這里及下文提到的事務,包括顯式聲明的事務,也包括隱式的事務,即每條獨立的sql語句)
1.2 B-Tree
B-Tree負責請求pager從disk讀取數據,然后把頁面(page)加載到頁面緩沖區(page cache)
1.3 Pager
Pager負責讀寫數據庫,管理內存緩存和頁面(即下文提到的page caches),以及管理事務,鎖和崩潰恢復
2,sqlite事務模型及鎖
2.1 sqlite多進程安全及Linux & windows文件鎖
-
關于建議鎖(advisory lock)和強制鎖(mandatory lock)
- 建議鎖并不由內核強制實行,如果有進程不檢查目標文件是否已經由別的進程加了鎖就往其中寫入數據,內核也不會加以阻攔。因此,建議鎖并不能阻止進程對文件的訪問,而是需要進程事先對鎖的狀態做一個約定,并根據鎖的當前狀態和相互關系來確定其他進程是否能對文件執行指定的操作
- 強制鎖是由內核強制采用的文件鎖——由于內核對每個read()和write()操作都會檢查相應的鎖,會降低系統性能
-
典型的建議鎖
- 鎖文件;鎖文件是最簡單的對文件加鎖的方法,每個需要加鎖的數據文件都有一個鎖文件(lock file)。但這種方式存在比較大的問題是無法強制保護需要加鎖的文件,并且當加鎖進程非正常退出之后,會造成其他進程的死鎖
- 記錄鎖;System V和BSD4.3引入了記錄鎖,相應的系統調用為lockf()和flock()。而POSIX對于記錄鎖提供了另外一種機制,其系統調用為fcntl()。記錄鎖和鎖文件有兩個很重要的區別:1)記錄鎖可以對文件的任何一部分加鎖,這對DBMS有極大的幫助,2)記錄鎖的另一個優點就是它由進程持有,而不是文件系統持有,當進程結束時,所有的鎖也隨之釋放。對于一個進程本身而言,多個鎖絕不會沖突。(Windows中的鎖都是強制鎖,具體不是很熟,只知道在由于windows上文鎖的限制,sqlite多進程下的并發性會受影響)
2.1.1 結論
sqlite的文件鎖在linux/posix上基于記錄鎖實現,也就是說sqlite在文件鎖上會有以下幾個特點:
- 多進程使用安全,且不會因為進程異常退出引發死鎖
- 單進程使用性能幾乎不會受損,多進程使用的性能損耗會受一定的影響
2.2 事務模型(Without WAL)
sqlite對每個連接設計了五鐘鎖的狀態(UNLOCKED, PENDING, SHARED, RESERVED, EXCLUSIVE), sqlite的事務模型中通過鎖的狀態保證讀寫事務(包括顯式的事務和隱式的事務)的一致性和讀寫安全。sqlite官方提供的事務生命周期如下圖所示,我在這里稍微加了一些個人的理解:
這里有幾點需要注意:
- UNLOCKED、PENDING、SHARED、RESERVED狀態是非獨占的,也就是說同一個連接中多個線程并發只讀不會被阻塞。
- 寫操作的數據修改會先寫入page cache,內容包括journal日志、b-tree的修改等;正是由于page cache的存在,很多耗時的“重”操作都可以不干擾其他連接和當前連接的讀操作,真正意義上保證了sqlite可以同時處理一個寫連接和多個讀連接。
- 連接由RESERVED狀態進入EXCLUSIVE狀態,需要等待讀線程釋放SHARED鎖,也即寫操作會被讀操作阻塞
- 連接由RESERVED狀態進入EXCLUSIVE狀態后(顯式或隱式的調用commit),數據庫進入獨占狀態,其他任何連接都無法由UNLOCK狀態進入SHARED狀態;也即寫操作會阻塞所有連接的讀操作(不包括已經進入SHARED狀態的操作),直到page caches寫入數據庫文件(成功或失敗)。
- 數據庫獨占狀態越久,其他操作的等待時間越久,即SQLITE_BUSY產生的一個原因
2.2.1 結論
- 對于常規的事務模型(without WAL),讀寫(連接)分離,不同連接或同一個連接上的讀和寫操作仍互相阻塞,對性能提升沒有明顯幫助
- 寫事務在拿到reserve鎖之前在page cache里的操作不會影響其他連接的讀寫,所以使用事務進行批量數據的更新操作有非常大的性能優勢
- 事務模型存在死鎖的場景,如下圖所示:
2.3 WAL對事務模型的影響
按照官方文檔,WAL的原理如下:
對數據庫修改是是寫入到WAL文件里的,這些寫是可以并發的(WAL文件鎖)。所以并不會阻塞其語句讀原始的數據庫文件。當WAL文件到達一定的量級時(CheckPoint),自動把WAL文件的內容寫入到數據庫文件中。當一個連接嘗試讀數據庫的時候,首先記錄下來當前WAL文件的末尾 end mark,然后,先嘗試在WAL文件里查找對應的Page,通過WAL-Index來對查找加速(放在共享內存里,.shm文件),如果找不到再查找數據庫文件。
這里結合源碼,有下面幾個理解:
- 數據的寫操作寫入WAL的過程不再需要SHARED鎖、EXCLUSIVE鎖,而是需要WAL文件鎖
- 數據的寫操作不會被讀操作阻塞(寫操作不再需要SHARED鎖)
- 數據的讀操作不會被寫操作阻塞(寫操作不再需要獨占數據庫)
- WAL文件寫入數據庫文件的過程,依然會被讀操作阻塞,也會阻塞讀操作
- WAL文件的大小設置很關鍵,過大的WAL文件,會讓查找操作從B-Tree查找退化成線性查找(WAL中page連續存儲);但大的WAL文件對寫操作較友好。
2.3.1 結論
- 只有開了WAL,再使用讀寫(連接)分離才能有較大的性能提升
- WAL本質上是將部分隨機寫操作(數據庫文件和journal日志)變成了串行寫WAL文件,并進行了鎖分離
- WAL文件的大小設置很關鍵,過大的WAL文件,會讓查找操作從B-Tree查找退化成線性查找(WAL中page連續存儲);但大的WAL文件對寫操作較友好
2.4 多線程設置
- 多線程是sqlite使用過程中比較容易誤解的一個概念,帶來的問題要么是產生各種線程安全問題,要么是無法充分發掘sqlite的性能,這里結合代碼我們簡單分析一下并給出幾個重要結論。
- 線程安全設置主要在設置bCoreMutex和bFullMutex,啟用bFullMutex之后數據庫連接和prepared statement都已加鎖(社區各種文檔都到此為止);但還是感覺不夠清晰:這兩個鎖會對我們使用sqlite有怎樣的影響?best practice又是什么?
- 如果FullMutex打開,則每個數據庫連接會初始化一個互斥量成員(db->mutex),也就是社區各種文檔上所說的“bFullMutex是對連接的線程保護”
- 如果CoreMutex打開,則會設置全局的鎖控制函數
- 而CoreMutext未打開的話,sqlite3NoopMutex()的實現如下(CoreMutext未打開的話,對應使用的鎖函數均為空實現):
- FullMutex保護了什么?
粗略看了一下,通過db->mutex(sqlite3_mutex_enter(db->mutex);)保護的邏輯塊和函數主要如下列表:
sqlite3_db_status、sqlite3_finalize、sqlite3_reset、sqlite3_step、sqlite3_exec、 sqlite3_preppare_v2、column_name、blob操作、sqlite3Close、sqlite3_errmsg...基本覆蓋了所有的讀、寫、DDL、DML,也包括prepared statement操作;也就是說,在未打開FullMutex的情況下,在一個連接上的所有DB操作必須嚴格串行執行,包括只讀操作。
- CoreMutex保護了什么?
sqlite3中的mutex操作函數,除了用于操作db->mutex這個成員之外,還主要用于以下邏輯塊(主要是影響數據庫所有連接的邏輯):
shm操作(index for wal)、內存池操作、內存緩存操作等2.4.1 結論
- 多線程設置是決定DDL、DML、WAL(包括SHM)操作是否線程安全的設置
- 多線程設置與讀寫(連接)分離沒有任何關系,并不是實現讀寫(連接)分離的必要條件(很多人對這一點有誤解)
3,性能優化tips
3.1 合理使用事務
由#2.2的分析可知,寫操作會在RESERVED狀態下將數據更改、b-tree的更改、日志等寫入page cache,并最終flush到數據庫文件中;使用事務的話,只需要一次對DB文件的flush操作,同時也不會對其他連接的讀寫操作阻塞;對比以下兩種數據寫入方式(這里以統一存儲提供的API為例),實測耗時有十幾倍的差距(當然對于頻繁的讀操作,使用事務可以減事務狀態的切換,也會有一點點性能提升):
// batch insert in transaction with 1000000 records // AliDBExecResult* execResult = NULL; _database->InTransaction([&]() -> bool { // in transactionauto statement = _database->PrepareStatement("INSERT INTO table VALUES(?, ?)");for (auto record : records) { // bind 1000000 records// bind record......statement->AddBatch();}auto result = statement->ExecuteUpdate();return result->is_success_; });// batch insert with 1000000 records, no transaction // auto statement = _database->PrepareStatement("INSERT INTO table VALUES(?, ?)"); for (auto record : records) { // bind 1000000 records// bind record......statement->ExecuteUpdate(); }3.2 啟用WAL + 讀寫(連接)分離
啟用WAL之后,數據庫大部分寫操作變成了串行寫(對WAL文件的串行操作),對寫入性能提升有非常大的幫助;同時讀寫操作可以互相完全不阻塞(如#2.3所述)。上述兩點比較好的解釋了啟用WAL帶來的提升;同時推薦一個寫連接 + 多個讀連接的模型,如下圖所示:
3.2.1 讀寫連接分離的細節
- 讀操作使用不同的連接并發執行,可以完全避免由于顯式事務、寫操作之間的鎖競爭帶來的死鎖
-
所有的寫操作、顯式事務操作都使用同一個連接,且所有的寫操作、顯式事務操作都串行執行
- 可以完全避免由于顯式事務、寫操作之間的鎖競爭帶來的死鎖,如#2.2.1提到的死鎖的例子
- 并發寫并不能有效的提高寫入效率,參考如下偽代碼,哪段執行更快?
3.3 針對具體業務場景,設置合適的WAL SIZE
如#2.3提到,過大的WAL文件,會讓查找操作從B-Tree查找退化成線性查找(WAL中page連續存儲);但大的WAL文件對寫操作較友好。對于大記錄的寫入操作,較大的wal size會有效提高寫入效率,同時不會影響查詢效率
3.4 針對業務場景分庫分表
分庫分表可以有效提高數據操作的并發度;但同時過多的表會影響數據庫文件的加載速度。現在數據庫方向的很多研究包括Auto sharding,? paxos consensus, 存儲和計算的分離等;Auto
application-awared optimization,Auto hardware-awared optimization,machine
learning based optimization也是不錯的方向。
3.5 其他
包括WAL checkpoint策略、WAL size優化、page size優化等,均需要根據具體的業務場景設置。
4,常見問題 & 誤區
4.1 線程安全設置及誤區
- sqlites configuration options:?https://sqlite.org/c3ref/c_config_getmalloc.html
-
按照sqlite文檔,sqlite線程安全模式有以下三種:
-
SQLITE_CONFIG_SINGLETHREAD(單線程模式)
- This option sets the?threading mode?to Single-thread. In other words, it disables all mutexing and puts SQLite into a mode where it can only be used by a single thread.
-
SQLITE_CONFIG_MULTITHREAD(多線程模式)
- This option sets the?threading mode?to Multi-thread. In other words, it disables mutexing on?database connection?and?prepared statement?objects. The application is responsible for serializing access to?database connections?and?prepared statements. But other mutexes are enabled so that SQLite will be safe to use in a multi-threaded environment as long as no two threads attempt to use the same?database connection?at the same time.
-
SQLITE_CONFIG_SERIALIZED(串行模式)
- This option sets the?threading mode?to Serialized. In other words, this option enables all mutexes including the recursive mutexes on?database connection?and?prepared statement?objects. In this mode (which is the default when SQLite is compiled with?SQLITE_THREADSAFE=1) the SQLite library will itself serialize access to?database connections?and?prepared statements?so that the application is free to use the same?database connection?or the same?prepared statement?in different threads at the same time.
-
4.1.1 誤區一:多線程模式是線程安全的
產生這個誤區主的主要原因是官方文檔里的最后一句話:
SQLite will be safe to use in a multi-threaded environment as long as no two threads attempt to use the same?database connection?at the same time.
但大家往往忽略了前面的一句話:
it disables mutexing on?database connection?and?prepared statement?objects
即對于單個連接的讀、寫操作,包括創建出來的prepared statement操作,都沒有線程安全的保護。也即在多線程模式下,對單個連接的操作,仍需要在業務層進行鎖保護。
4.1.2 誤區二:多線程模式下,并發讀操作是安全的
關于這一點,#2.4給出了具體的解釋;多線程模式下(SQLITE_CONFIG_MULTITHREAD)對prepared statement、connection的操作都不是線程安全的
4.1.3 誤區三:串行模式下,所有數據庫操作都是串行執行
這個問題比較籠統;即使在串行模式下,所有的數據庫操作仍需遵循事務模型;而事務模型已經將數據庫操作的鎖進行了非常細粒度的分離,串行模式的鎖也是在上層保證了事務模型的完整性
4.1.4 誤區四:多線程模式性能最好,串行模式性能差
多線程模式下,仍需要業務上層進行鎖保護,串行模式則是在sqlite內部進行了鎖保護;認為多線程模式性能好的兄弟哪來的自信認為業務層的鎖實現比sqlite內部鎖實現性能更高?
原文鏈接
本文為云棲社區原創內容,未經允許不得轉載。
總結
以上是生活随笔為你收集整理的sqlite事务模型、性能优化tips、常见误区的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 【物联网开发实战】- 设备上云方案详解?
- 下一篇: 性能压测工具选型对比