sqlite事務模型、性能優化tips、常見誤區

0,前言

本文主要介紹sqlite的事務模型,以及基於事務模型的一些性能優化tips,包括事務封裝、WAL+讀寫分離、分庫分表、page size優化等。並基於手淘sqlite的使用現狀總結了部分常見問題及誤區,主要集中在多線程的設置、多線程下性能優化的誤區等。本文先提出如下幾個問題(做者在進行統一存儲的關係存儲框架優化過程當中一直困惑的問題,同時也是客戶端開發者常常搞錯的問題)並在正文中進行解答:html

  • 1,sqlite的多進程安全是怎麼實現的?性能如何?
  • 2,sqlite的數據庫鏈接是什麼?
  • 3,言sqlite必提的讀寫分離,具體指什麼?能不能提高數據讀寫的性能?爲何
  • 4,sqlite提供的WAL特性解決了什麼問題?
  • 5,sqlite的多線程設置是爲了解決什麼問題?與讀寫分離有什麼關係?
  • 6,什麼狀況下數據庫會發生死鎖?
  • 7,有哪些性能優化的方案?

1,sqlite主要數據結構

在深刻了解sqlite以前,最好先對sqlite的主要數據結構有個概要的理解,sqlite是一個很是完備的關係數據庫系統,由不少部分組成(parser,tokenize,virtual machine等等),同時sqlite的事務模型相對簡化,是入門學習關係數據庫方法論的一個不錯的選擇;下文對事務模型的分析也基於這些核心數據結構。下面這張圖比較準確的描述了sqlite的幾個核心數據結構:linux

1.1 Connection

connection經過sqlite3_open函數打開,表明一個獨立的事務環境(這裏及下文提到的事務,包括顯式聲明的事務,也包括隱式的事務,即每條獨立的sql語句)sql

1.2 B-Tree

B-Tree負責請求pager從disk讀取數據,而後把頁面(page)加載到頁面緩衝區(page cache)數據庫

1.3 Pager

Pager負責讀寫數據庫,管理內存緩存和頁面(即下文提到的page caches),以及管理事務,鎖和崩潰恢復windows

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又是什麼?
// 多線程的設置的實現:設置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
  • 若是FullMutex打開,則每一個數據庫鏈接會初始化一個互斥量成員(db->mutex),也就是社區各類文檔上所說的「bFullMutex是對鏈接的線程保護」
if( isThreadsafe ){    // bFullMutex = 1
    db->mutex = sqlite3MutexAlloc(SQLITE_MUTEX_RECURSIVE);    // 每一個數據庫鏈接會初始化一個成員鎖
    if( db->mutex==0 ){
      sqlite3_free(db);
      db = 0;
      goto opendb_out;
    }
  }
  • 若是CoreMutex打開,則會設置全局的鎖控制函數
/* 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;
  • 而CoreMutext未打開的話,sqlite3NoopMutex()的實現以下(CoreMutext未打開的話,對應使用的鎖函數均爲空實現):
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; }
  • FullMutex保護了什麼?

粗略看了一下,經過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操做必須嚴格串行執行,包括只讀操做。

  • 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 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();
}

3.2 啓用WAL + 讀寫(鏈接)分離

啓用WAL以後,數據庫大部分寫操做變成了串行寫(對WAL文件的串行操做),對寫入性能提高有很是大的幫助;同時讀寫操做能夠互相徹底不阻塞(如#2.3所述)。上述兩點比較好的解釋了啓用WAL帶來的提高;同時推薦一個寫鏈接 + 多個讀鏈接的模型,以下圖所示:

3.2.1 讀寫鏈接分離的細節

  • 讀操做使用不一樣的鏈接併發執行,能夠徹底避免因爲顯式事務、寫操做之間的鎖競爭帶來的死鎖
  • 全部的寫操做、顯式事務操做都使用同一個鏈接,且全部的寫操做、顯式事務操做都串行執行

    • 能夠徹底避免因爲顯式事務、寫操做之間的鎖競爭帶來的死鎖,如#2.2.1提到的死鎖的例子
    • 併發寫並不能有效的提升寫入效率,參考以下僞代碼,哪段執行更快?
// 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();
}

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內部鎖實現性能更高?

5,參考文獻

  1. sqlite configuration reference, https://sqlite.org/c3ref/c_config_getmalloc.html
  2. 深刻理解sqlite, https://www.kancloud.cn/kangdandan/sqlite


本文做者:hamsongliu

閱讀原文

本文爲雲棲社區原創內容,未經容許不得轉載。

相關文章
相關標籤/搜索