文件數據庫sqlite3 C++ 線程安全和並發


轉載:https://www.cnblogs.com/feng9exe/p/10682567.html(線程安全和並發)

轉載:https://juejin.im/post/5b7d8522e51d4538e5679f5e(WAL模式介紹)

轉載:https://blog.csdn.net/vannachen/article/details/8277344(多線程/WAL/鎖)

轉載:https://blog.csdn.net/wql2rainbow/article/details/73650056(怎么開啟wal機制)

轉載:https://www.cnblogs.com/cchust/category/802864.html(sqlite 詳細教程)

轉載:http://www.cnblogs.com/stephen-liu74/archive/2012/01/19/2326309.html(sqlite備份機制)

轉載:https://blog.csdn.net/northcan/article/details/7231115(打開數據庫例子)

轉載:https://blog.51cto.com/linzimo777/1544202(相同的項目場景,解決方法)

轉載:https://blog.csdn.net/wsmrcool/article/details/8287904(多線程多個數據庫連接)

轉載:https://www.jianshu.com/p/6a1ebd08f003(分庫)

轉載:https://mp.weixin.qq.com/s?__biz=MzAwNDY1ODY2OQ==&mid=2649286361&idx=1&sn=78bbcda7f41a14291ad71289e4821f71&scene=21(多線程並發優化)

轉載:http://www.googleplus.party/2017/10/20/%E4%BD%A0%E4%B8%8D%E5%BE%97%E4%B8%8D%E7%9F%A5%E7%9A%84SQLite/(sqlite讀寫分離)

轉載:https://zhuanlan.zhihu.com/p/23911987(sqlite優化)

一、SQLite 與線程

SQLite 是線程安全的。

線程模型

SQLite 支持如下三種線程模型

  • 單線程模型 這種模型下,所有互斥鎖都被禁用,同一時間只能由一個線程訪問。
  • 多線程模型 這種模型下,一個連接在同一時間內只有一個線程使用就是安全的。
  • 串行模型 開啟所有鎖,可以隨意訪問。

設置線程模型

SQLite 可以通過以下三種方式進行線程模型的設置,在實際應用中選擇任一一項都可以。

  • 編譯期設定 通過 SQLITE_THREADSAFE 這個參數進行編譯器的設定來選擇線程模型
  • 初始化設定 通過調用 sqlite3_config() 可以在 SQLite 初始化時進行設定
  • 運行時設定 通過調用 sqlite3_open_v2() 接口指定數據庫連接的數據庫模型

SQLite 並發和事務

事務

事務是 SQLite 的核心概念。對數據庫的操作 (絕大部分) 會被打包成一個事務進行提交,需要注意的是,這里的打包成事務是自動開啟的。舉例而言,如果簡單在一個 for 循環語句里向數據庫中插入 10 條數據,意味着將自動生成 10 個事務。但需要注意的是事務是非常耗時的,一般而言, SQLite 每秒能夠輕松支持 50000 條的數據插入,但是每秒僅能夠支持幾十個事務。一般而言,事務速度受限於磁盤速度。所以在批量插入時需要考慮禁用自動提交,將其用 BEGIN ... COMMIT 打包成一個事務。

回滾模式和 WAL

為了保證寫入正確,SQLite 在使用事務進行數據庫改寫時將拷貝當前數據庫文件的備份,即 rollback journal,當事務失敗或者發生意外需要回滾時則將備份文件內容還原到數據庫中,並同時刪除該日志。這是默認的 DELETE 模式。

而后 SQLite 也引入了 WAL 模式,即 Write-Ahead Log。在這種模式下,所有的修改會寫入一個單獨的 WAL 文件內。這種模式下,寫操作甚至可以不去操作數據庫,這使得所有的讀操作可以在 "寫的同時" 直接對數據庫文件進行操作,得到更好的並發性能。

鎖和並發

SQLite 通過五種鎖狀態來完成事務。

  • UNLOCKED ,無鎖狀態。數據庫文件沒有被加鎖。
  • SHARED 共享狀態。數據庫文件被加了共享鎖。可以多線程執行讀操作,但不能進行寫操作。
  • RESERVED 保留狀態。數據庫文件被加保留鎖。表示數據庫將要進行寫操作。
  • PENDING 未決狀態。表示即將寫入數據庫,正在等待其他讀線程釋放 SHARED 鎖。一旦某個線程持有 PENDING 鎖,其他線程就不能獲取 SHARED 鎖。這樣一來,只要等所有讀線程完成,釋放 SHARED 鎖后,它就可以進入 EXCLUSIVE 狀態了。
  • EXCLUSIVE 獨占鎖。表示它可以寫入數據庫了。進入這個狀態后,其他任何線程都不能訪問數據庫文件。因此為了並發性,它的持有時間越短越好。

一個線程只有擁有低級別鎖時才能夠獲得更高一級的鎖

/*

** Lock the file with the lock specified by parameter eFileLock - one

** of the following:

**

**     (1) SHARED_LOCK

**     (2) RESERVED_LOCK

**     (3) PENDING_LOCK

**     (4) EXCLUSIVE_LOCK

**

** Sometimes when requesting one lock state, additional lock states

** are inserted in between.  The locking might fail on one of the later

** transitions leaving the lock state different from what it started but

** still short of its goal.  The following chart shows the allowed

** transitions and the inserted intermediate states:

**

**    UNLOCKED -> SHARED

**    SHARED -> RESERVED

**    SHARED -> (PENDING) -> EXCLUSIVE

**    RESERVED -> (PENDING) -> EXCLUSIVE

**    PENDING -> EXCLUSIVE

**

** This routine will only increase a lock.  Use the sqlite3OsUnlock()

** routine to lower a locking level.

*/

總結

綜上所述,要保證數據庫使用的安全,一般可以采用如下幾種模式

  • SQLite 采用單線程模型,用專門的線程/隊列(同時只能有一個任務執行訪問) 進行訪問
  • SQLite 采用多線程模型,每個線程都使用各自的數據庫連接 (即 sqlite3 *)
  • SQLite 采用串行模型,所有線程都公用同一個數據庫連接。

因為寫操作的並發性並不好,當多線程進行訪問時實際上仍舊需要互相等待,而讀操作所需要的 SHARED 鎖是可以共享的,所以為了保證最高的並發性,推薦

  • 使用多線程模式
  • 使用 WAL 模式
  • 單線程寫,多線程讀 (各線程都持有自己對應的數據庫連接)
  • 避免長時間事務
  • 緩存 sqlite3_prepare 編譯結果
  • 多語句通過 BEGIN 和 COMMIT 做顯示事務,減少多次的自動事務消耗

二、WAL 機制的原理是:

     修改並不直接寫入到數據庫文件中,而是寫入到另外一個稱為 WAL 的文件中;如果事務失敗,WAL 中的記錄會被忽略,撤銷修改;如果事務成功,它將在隨后的某個時間被寫回到數據庫文件中,提交修改。 同步 WAL 文件和數據庫文件的行為被稱為 checkpoint(檢查點),它由 SQLite 自動執行,默認是在 WAL 文件積累到 1000 頁修改的時候;當然,在適當的時候,也可以手動執行 checkpoint,SQLite 提供了相關的接口。執行 checkpoint 之后,WAL 文件會被清空。 在讀的時候,SQLite 將在 WAL 文件中搜索,找到最后一個寫入點,記住它,並忽略在此之后的寫入點(這保證了讀寫和讀讀可以並行執行);隨后,它確定所要讀的數據所在頁是否在 WAL 文件中,如果在,則讀 WAL 文件中的數據,如果不在,則直接讀數據庫文件中的數據。 在寫的時候,SQLite 將之寫入到 WAL 文件中即可,但是必須保證獨占寫入,因此寫寫之間不能並行執行。

2.1 wal工作原理

在引入WAL機制之前,SQLite使用rollbackjournal機制實現原子事務。

rollback journal機制的原理是:在修改數據庫文件中的數據之前,先將修改所在分頁中的數據備份在另外一個地方,然后才將修改寫入到數據庫文件中;如果事務失敗,則將備份數據拷貝回來,撤銷修改;如果事務成功,則刪除備份數據,提交修改。

WAL機制的原理是:修改並不直接寫入到數據庫文件中,而是寫入到另外一個稱為WAL的文件中;如果事務失敗,WAL中的記錄會被忽略,撤銷修改;如果事務成功,它將在隨后的某個時間被寫回到數據庫文件中,提交修改。

2.2 wal優點:

1.      讀和寫可以完全地並發執行,不會互相阻塞(但是寫之間仍然不能並發)。

2.      WAL在大多數情況下,擁有更好的性能(因為無需每次寫入時都要寫兩個文件)。

3.      磁盤I/O行為更容易被預測。

2.3 wal缺點:

1.      訪問數據庫的所有程序必須在同一主機上,且支持共享內存技術。

2.      每個數據庫現在對應3個文件:<yourdb>.db,<yourdb>-wal,<yourdb>-shm。

3.      當寫入數據達到GB級的時候,數據庫性能將下降。

4.      3.7.0之前的SQLite無法識別啟用了WAL機制的數據庫文件。

2.4 wal如何記錄數據--checkpoint

使用WAL模式時,改寫操作是附加(append)到WAL文件,而不改動數據庫文件,因此數據庫文件可以被同時讀取。當執行checkpoint操作時,WAL文件的內容會被寫回數據庫文件。當WAL文件達到SQLITE_DEFAULT_WAL_AUTOCHECKPOINT(默認值是1000)頁(默認大小是1KB)時,會自動使用當前COMMIT的線程來執行checkpoint操作。也可以關閉自動checkpoint,改為手動定期checkpoint。 

為了避免讀取的數據不一致,查詢時也需要讀取WAL文件,並記錄一個結尾標記(end mark)。這樣的代價就是讀取會變得稍慢,但是寫入會變快很多。要提高查詢性能的話,可以減小WAL文件的大小,但寫入性能也會降低。 需要注意的是,低版本的SQLite不能讀取高版本的SQLite生成的WAL文件,但是數據庫文件是通用的。這種情況在用戶進行iOS降級時可能會出現,可以把模式改成delete,再改回WAL來修復。 

要對一個數據庫連接啟用WAL模式,需要執行“PRAGMA journal_mode=WAL;”這條命令,它的默認值是“journal_mode=DELETE”。執行后會返回新的journal_mode字符串值,即成功時為"wal",失敗時為之前的模式(例如"delete")。一旦啟用WAL模式后,數據庫會保持這個模式,這樣下次打開數據庫時仍然是 WAL模式。 要停止自動checkpoint,可以使用wal_autocheckpoint指令或sqlite3_wal_checkpoint()函數。手動執行 checkpoint可以使用wal_checkpoint指令或sqlite3_wal_checkpoint()函數。

三、開啟WAL機制

 int DataSource::InitDataBaseToWal(std::string sPath, bool isWal)
    {
        char* zErrMsg;
 
        sqlite3* db = NULL;
 
        int rc = sqlite3_open_v2(sPath.c_str(), &db, SQLITE_OPEN_READWRITE | SQLITE_OPEN_FULLMUTEX, NULL);
 
        if (rc != SQLITE_OK)
        {
            Logger::LogD("DataSource::sqlite [%s] or [%s] open failed", sPath.c_str(), sqlite3_errmsg(db));
            Logger::LogO("DataSource::sqlite [%s] or [%s] open failed", sPath.c_str(), sqlite3_errmsg(db));
 
            sqlite3_close(db);
 
            return -1;
        }
 
        if(isWal == true)
        {
            rc = sqlite3_exec(db, "PRAGMA journal_mode=WAL;", NULL, 0, &zErrMsg);
 
            if (rc != SQLITE_OK)
            {
                sqlite3_free(zErrMsg);
 
                sqlite3_close(db);
 
                return -1;
            }
 
            rc = sqlite3_exec(db, "PRAGMA wal_autocheckpoint=100;", NULL, 0, &zErrMsg);
 
            if (rc != SQLITE_OK)
            {
                sqlite3_free(zErrMsg);
 
                sqlite3_close(db);
 
                return -1;
            }
        }
        else
        {
            rc = sqlite3_exec(db, "PRAGMA journal_mode=DELETE;", NULL, 0, &zErrMsg);
 
                   if (rc != SQLITE_OK)
               {
                sqlite3_free(zErrMsg);
 
                sqlite3_close(db);
 
                 return -1;
              }
            }
 
        return true;
      }
   

 四、多線程並發寫操作的安全性

    sqlite實際支持的是多線程同時讀但只支持同一時刻一個線程寫,即所謂的多讀單寫,sqlite 支持 single-thread/multi-thread/serialized 三種不同的線程安全模式。可以在編譯sqlite組件時進行配置,或者可以通過 sqlite3_threadsafe()/sqlite3_config() 在程序運行時進行查看並配置線程安全模式。經過實際寫 demo 測試,進行 multi-thread 或 serialized 配置以后,多線程並發讀的場景下,沒有問題。但是多線程並發寫時依舊會拋錯 database is locked。事實證明Sqlite不支持並發執行寫入操作,即使是不同的表,只支持庫級鎖,而且這個Sqlite本身沒有實現,必須自己實現這個庫級鎖,通過查閱官網資料,發現sqlite提供兩個 busy handle 函數sqlite3_busy_timeout()/sqlite3_busy_handle() 在並發訪問失敗時,會調用注冊的 busy handle 函數,在注冊的自定義的 busy handle 函數中可以進行處理(如重試n次等), 這種處理方式必須建立在多線程多個數據庫連接,多個數據庫連接可以理解成,用sqlite3_open或者sqlite3_open_v2打開同一個數據庫文件,每一個線程維護一個數據庫連接對象,這樣發生寫競爭沖突的時候,可以通過回調函數重試,解決並發寫
 
線程安全:是指二個或三個線程可以同時調用獨立的不同的sqlite3_open() 返回的"sqlite3"結構。而不是在多線程中同時使用同一個 sqlite3 結構指針。
一個sqlite3結構只能在調用 sqlite3_open創建它的那個進程中使用。你不能在一個線程中打開一個數據庫然后把指針傳遞給另一個線程使用。這是因為大多數多線程系統的限制


免責聲明!

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



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