本文主要介紹sqlite的事務模型,以及基於事務模型的一些性能優化tips,包括事務封裝、WAL+讀寫分離、分庫分表、page size優化等。並基於手淘sqlite的使用現狀總結了部分常見問題及誤區,主要集中在多線程的設置、多線程下性能優化的誤區等。本文先提出如下幾個問題(做者在進行統一存儲的關係存儲框架優化過程當中一直困惑的問題,同時也是客戶端開發者常常搞錯的問題)並在正文中進行解答:html
在深刻了解sqlite以前,最好先對sqlite的主要數據結構有個概要的理解,sqlite是一個很是完備的關係數據庫系統,由不少部分組成(parser,tokenize,virtual machine等等),同時sqlite的事務模型相對簡化,是入門學習關係數據庫方法論的一個不錯的選擇;下文對事務模型的分析也基於這些核心數據結構。下面這張圖比較準確的描述了sqlite的幾個核心數據結構:linux
connection經過sqlite3_open函數打開,表明一個獨立的事務環境(這裏及下文提到的事務,包括顯式聲明的事務,也包括隱式的事務,即每條獨立的sql語句)sql
B-Tree負責請求pager從disk讀取數據,而後把頁面(page)加載到頁面緩衝區(page cache)數據庫
Pager負責讀寫數據庫,管理內存緩存和頁面(即下文提到的page caches),以及管理事務,鎖和崩潰恢復windows
關於建議鎖(advisory lock)和強制鎖(mandatory lock)緩存
典型的建議鎖安全
sqlite的文件鎖在linux/posix上基於記錄鎖實現,也就是說sqlite在文件鎖上會有如下幾個特色:性能優化
sqlite對每一個鏈接設計了五鍾鎖的狀態(UNLOCKED, PENDING, SHARED, RESERVED, EXCLUSIVE), sqlite的事務模型中經過鎖的狀態保證讀寫事務(包括顯式的事務和隱式的事務)的一致性和讀寫安全。sqlite官方提供的事務生命週期以下圖所示,我在這裏稍微加了一些我的的理解:數據結構
這裏有幾點須要注意:多線程
按照官方文檔,WAL的原理以下:
對數據庫修改是是寫入到WAL文件裏的,這些寫是能夠併發的(WAL文件鎖)。因此並不會阻塞其語句讀原始的數據庫文件。當WAL文件到達必定的量級時(CheckPoint),自動把WAL文件的內容寫入到數據庫文件中。當一個鏈接嘗試讀數據庫的時候,首先記錄下來當前WAL文件的末尾 end mark,而後,先嚐試在WAL文件裏查找對應的Page,經過WAL-Index來對查找加速(放在共享內存裏,.shm文件),若是找不到再查找數據庫文件。
這裏結合源碼,有下面幾個理解:
// 多線程的設置的實現:設置bCoreMutex和bFullMutex #if defined(SQLITE_THREADSAFE) && SQLITE_THREADSAFE>0 /* IMP: R-54466-46756 */ case SQLITE_CONFIG_SINGLETHREAD: { /* EVIDENCE-OF: R-02748-19096 This option sets the threading mode to ** Single-thread. */ sqlite3GlobalConfig.bCoreMutex = 0; /* Disable mutex on core */ sqlite3GlobalConfig.bFullMutex = 0; /* Disable mutex on connections */ break; } #endif #if defined(SQLITE_THREADSAFE) && SQLITE_THREADSAFE>0 /* IMP: R-20520-54086 */ case SQLITE_CONFIG_MULTITHREAD: { /* EVIDENCE-OF: R-14374-42468 This option sets the threading mode to ** Multi-thread. */ sqlite3GlobalConfig.bCoreMutex = 1; /* Enable mutex on core */ sqlite3GlobalConfig.bFullMutex = 0; /* Disable mutex on connections */ break; } #endif #if defined(SQLITE_THREADSAFE) && SQLITE_THREADSAFE>0 /* IMP: R-59593-21810 */ case SQLITE_CONFIG_SERIALIZED: { /* EVIDENCE-OF: R-41220-51800 This option sets the threading mode to ** Serialized. */ sqlite3GlobalConfig.bCoreMutex = 1; /* Enable mutex on core */ sqlite3GlobalConfig.bFullMutex = 1; /* Enable mutex on connections */ break; } #endif
if( isThreadsafe ){ // bFullMutex = 1 db->mutex = sqlite3MutexAlloc(SQLITE_MUTEX_RECURSIVE); // 每一個數據庫鏈接會初始化一個成員鎖 if( db->mutex==0 ){ sqlite3_free(db); db = 0; goto opendb_out; } }
/* If the xMutexAlloc method has not been set, then the user did not ** install a mutex implementation via sqlite3_config() prior to ** sqlite3_initialize() being called. This block copies pointers to ** the default implementation into the sqlite3GlobalConfig structure. */ sqlite3_mutex_methods const *pFrom; sqlite3_mutex_methods *pTo = &sqlite3GlobalConfig.mutex; if( sqlite3GlobalConfig.bCoreMutex ){ pFrom = sqlite3DefaultMutex(); }else{ pFrom = sqlite3NoopMutex(); } pTo->xMutexInit = pFrom->xMutexInit; pTo->xMutexEnd = pFrom->xMutexEnd; pTo->xMutexFree = pFrom->xMutexFree; pTo->xMutexEnter = pFrom->xMutexEnter; pTo->xMutexTry = pFrom->xMutexTry; pTo->xMutexLeave = pFrom->xMutexLeave; pTo->xMutexHeld = pFrom->xMutexHeld; pTo->xMutexNotheld = pFrom->xMutexNotheld; sqlite3MemoryBarrier(); pTo->xMutexAlloc = pFrom->xMutexAlloc;
sqlite3_mutex_methods const *sqlite3NoopMutex(void){ static const sqlite3_mutex_methods sMutex = { noopMutexInit, noopMutexEnd, noopMutexAlloc, noopMutexFree, noopMutexEnter, noopMutexTry, noopMutexLeave, 0, 0, }; return &sMutex; } // CoreMutext未打開的話,對應使用的鎖函數均爲空實現 static int noopMutexInit(void){ return SQLITE_OK; } static int noopMutexEnd(void){ return SQLITE_OK; } static sqlite3_mutex *noopMutexAlloc(int id){ UNUSED_PARAMETER(id); return (sqlite3_mutex*)8; } static void noopMutexFree(sqlite3_mutex *p){ UNUSED_PARAMETER(p); return; } static void noopMutexEnter(sqlite3_mutex *p){ UNUSED_PARAMETER(p); return; } static int noopMutexTry(sqlite3_mutex *p){ UNUSED_PARAMETER(p); return SQLITE_OK; } static void noopMutexLeave(sqlite3_mutex *p){ UNUSED_PARAMETER(p); return; }
粗略看了一下,經過db->mutex(sqlite3_mutex_enter(db->mutex);)保護的邏輯塊和函數主要以下列表:
sqlite3_db_status、sqlite3_finalize、sqlite3_reset、sqlite3_step、sqlite3_exec、 sqlite3_preppare_v二、column_name、blob操做、sqlite3Close、sqlite3_errmsg...
基本覆蓋了全部的讀、寫、DDL、DML,也包括prepared statement操做;也就是說,在未打開FullMutex的狀況下,在一個鏈接上的全部DB操做必須嚴格串行執行,包括只讀操做。
sqlite3中的mutex操做函數,除了用於操做db->mutex這個成員以外,還主要用於如下邏輯塊(主要是影響數據庫全部鏈接的邏輯):
shm操做(index for wal)、內存池操做、內存緩存操做等
由#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 transaction auto 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(); }
啓用WAL以後,數據庫大部分寫操做變成了串行寫(對WAL文件的串行操做),對寫入性能提高有很是大的幫助;同時讀寫操做能夠互相徹底不阻塞(如#2.3所述)。上述兩點比較好的解釋了啓用WAL帶來的提高;同時推薦一個寫鏈接 + 多個讀鏈接的模型,以下圖所示:
全部的寫操做、顯式事務操做都使用同一個鏈接,且全部的寫操做、顯式事務操做都串行執行
// two transactions: void Transaction_1() { connection_->Exec("BEGIN"); connection_->Exec("insert into table(value) values('xxxx')"); connection_->Exec("COMMIT"); } void Transaction_2() { connection_->Exec("BEGIN"); connection_->Exec("insert into table(value) values('xxxx')"); connection_->Exec("COMMIT"); } // code fragment 1: concurrent transaction thread1.RunBlock([]() -> void { for (int i=0; i< 100000; i++) { Transaction_1(); } }); thread2.RunBlock([]() -> void { for (int i=0; i< 100000; i++) { Transaction_2(); } }); thread1.Join(); thread2.join(); // code fragment 2: serial transaction for (int i=0; i< 100000; i++) { Transaction_1(); } for (int i=0; i< 100000; i++) { Transaction_2(); }
如#2.3提到,過大的WAL文件,會讓查找操做從B-Tree查找退化成線性查找(WAL中page連續存儲);但大的WAL文件對寫操做較友好。對於大記錄的寫入操做,較大的wal size會有效提升寫入效率,同時不會影響查詢效率
分庫分表能夠有效提升數據操做的併發度;但同時過多的表會影響數據庫文件的加載速度。如今數據庫方向的不少研究包括Auto sharding, paxos consensus, 存儲和計算的分離等;Auto
application-awared optimization,Auto hardware-awared optimization,machine
learning based optimization也是不錯的方向。
包括WAL checkpoint策略、WAL size優化、page size優化等,均須要根據具體的業務場景設置。
按照sqlite文檔,sqlite線程安全模式有如下三種:
SQLITE_CONFIG_SINGLETHREAD(單線程模式)
SQLITE_CONFIG_MULTITHREAD(多線程模式)
SQLITE_CONFIG_SERIALIZED(串行模式)
產生這個誤區主的主要緣由是官方文檔裏的最後一句話:
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操做,都沒有線程安全的保護。也即在多線程模式下,對單個鏈接的操做,仍須要在業務層進行鎖保護。
關於這一點,#2.4給出了具體的解釋;多線程模式下(SQLITE_CONFIG_MULTITHREAD)對prepared statement、connection的操做都不是線程安全的
這個問題比較籠統;即便在串行模式下,全部的數據庫操做仍需遵循事務模型;而事務模型已經將數據庫操做的鎖進行了很是細粒度的分離,串行模式的鎖也是在上層保證了事務模型的完整性
多線程模式下,仍須要業務上層進行鎖保護,串行模式則是在sqlite內部進行了鎖保護;認爲多線程模式性能好的兄弟哪來的自信認爲業務層的鎖實現比sqlite內部鎖實現性能更高?
本文爲雲棲社區原創內容,未經容許不得轉載。