sqlite3 多線程和鎖 ,優化插入速度及性能優化

来源:http://www.cnblogs.com/jaejaking/archive/2016/04/12/5383403.html
-Advertisement-
Play Games

一、 是否支持多線程? SQLite官網上的“Is SQLite threadsafe?”這個問答。 簡單來說,從3.3.1版本開始,它就是線程安全的了。而iOS的SQLite版本沒有低於這個版本的,當然,你也可以自己編譯最新版本。 不過這個線程安全仍然是有限制的,在這篇《Is SQLite thr ...


一、 是否支持多線程?   SQLite官網上的“Is SQLite threadsafe?”這個問答。 簡單來說,從3.3.1版本開始,它就是線程安全的了。而iOS的SQLite版本沒有低於這個版本的,當然,你也可以自己編譯最新版本。
不過這個線程安全仍然是有限制的,在這篇《Is SQLite thread-safe?》里有詳細的解釋。
另一篇重要的文檔就是《SQLite And Multiple Threads》。它指出SQLite支持3種線程模式:
  1. 單線程:禁用所有的mutex鎖,併發使用時會出錯。當SQLite編譯時加了SQLITE_THREADSAFE=0參數,或者在初始化SQLite前調用sqlite3_config(SQLITE_CONFIG_SINGLETHREAD)時啟用。
  2. 多線程:只要一個資料庫連接不被多個線程同時使用就是安全的。源碼中是啟用bCoreMutex,禁用bFullMutex。實際上就是禁用資料庫連接和prepared statement(準備好的語句)上的鎖,因此不能在多個線程中併發使用同一個資料庫連接或prepared statement。當SQLite編譯時加了SQLITE_THREADSAFE=2參數時預設啟用。若SQLITE_THREADSAFE不為0,可以在初始化SQLite前,調用sqlite3_config(SQLITE_CONFIG_MULTITHREAD)啟用;或者在創建資料庫連接時,設置SQLITE_OPEN_NOMUTEX flag。
  3. 串列:啟用所有的鎖,包括bCoreMutex和bFullMutex。因為資料庫連接和prepared statement都已加鎖,所以多線程使用這些對象時沒法併發,也就變成串列了。當SQLite編譯時加了SQLITE_THREADSAFE=1參數時預設啟用。若SQLITE_THREADSAFE不為0,可以在初始化SQLite前,調用sqlite3_config(SQLITE_CONFIG_SERIALIZED)啟用;或者在創建資料庫連接時,設置SQLITE_OPEN_FULLMUTEX flag。
  而這裡所說的初始化是指調用sqlite3_initialize()函數,這個函數在調用sqlite3_open()時會自動調用,且只有第一次調用是有效的。   調用sqlite3_threadsafe()可以獲得編譯期的SQLITE_THREADSAFE參數。標準發行版是1,也就是串列模式;而iOS上是2,也就是多線程模式;Python的sqlite3模塊也預設使用串列模式,可以用sqlite3.threadsafety來配置。      另一個要說明的是prepared statement,它是由資料庫連接(的pager)來管理的,使用它也可看成使用這個資料庫連接。因此在多線程模式下,併發對同一個資料庫連接調用sqlite3_prepare_v2()來創建prepared statement,或者對同一個資料庫連接的任何prepared statement併發調用sqlite3_bind_*()和sqlite3_step()等函數都會出錯(在iOS上,該線程會出現EXC_BAD_ACCESS而中止)。這種錯誤無關讀寫,就是只讀也會出錯。文檔中給出的安全使用規則是:沒有事務正在等待執行,所有prepared statement都被finalized。   但是預設情況下,一個線程只能使用當前線程打開的資料庫連接,除非在連接時設置了check_same_thread=False參數。如果是用不同的資料庫連接,每個連接都不能讀取其他連接中未提交的數據,除非使用read-uncommitted模式。

現在3種模式都有所瞭解了,清楚SQLite並不是對多線程無能為力後,接下來就瞭解下事務吧。   二、事務
  資料庫只有在事務中才能被更改。所有更改資料庫的命令(除SELECT以外的所有SQL命令)都會自動開啟一個新事務,並且當最後一個查詢完成時自動提交。
  而BEGIN命令可以手動開始事務,並關閉自動提交。當下一條COMMIT命令執行時,自動提交再次打開,事務中所做的更改也被寫入資料庫。當COMMIT失敗時,自動提交仍然關閉,以便讓用戶嘗試再次提交。若執行的是ROLLBACK命令,則也打開自動提交,但不保存事務中的更改。關閉資料庫或遇到錯誤時,也會自動回滾事務。
     經常有人抱怨SQLite的插入太慢,實際上它可以做到每秒插入幾萬次,但是每秒只能提交幾十次事務。因此在插入大批數據時,可以通過禁用自動提交來提速。   還有一個很重要的知識點需要強調:事務是和資料庫連接相關的,每個資料庫連接(使用pager來)維護自己的事務,且同時只能有一個事務(但是可以用SAVEPOINT來實現內嵌事務)。也就是說,事務與線程無關,一個線程里可以同時用多個資料庫連接來完成多個事務,而多個線程也可以同時(非併發)使用一個資料庫連接來共同完成一個事務。   而要實現事務,就不得不用到
一個SQLite資料庫文件有5種鎖的狀態:
  • UNLOCKED:表示資料庫此時並未被讀寫。
  • SHARED:表示資料庫可以被讀取。SHARED鎖可以同時被多個線程擁有。一旦某個線程持有SHARED鎖,就沒有任何線程可以進行寫操作。
  • RESERVED:表示準備寫入資料庫。RESERVED鎖最多只能被一個線程擁有,此後它可以進入PENDING狀態。
  • PENDING:表示即將寫入資料庫,正在等待其他讀線程釋放SHARED鎖。一旦某個線程持有PENDING鎖,其他線程就不能獲取SHARED鎖。這樣一來,只要等所有讀線程完成,釋放SHARED鎖後,它就可以進入EXCLUSIVE狀態了。
  • EXCLUSIVE:表示它可以寫入資料庫了。進入這個狀態後,其他任何線程都不能訪問資料庫文件。因此為了併發性,它的持有時間越短越好。
一個線程只有在擁有低級別的鎖的時候,才能獲取更高一級的鎖。SQLite就是靠這5種類型的鎖,巧妙地實現了讀寫線程的互斥。同時也可看出,寫操作必須進入EXCLUSIVE狀態,此時併發數被降到1,這也是SQLite被認為併發插入性能不好的原因。
另外,read-uncommitted和WAL模式會影響這個鎖的機制。在這2種模式下,讀線程不會被寫線程阻塞,即使寫線程持有PENDING或EXCLUSIVE鎖。

提到鎖就不得不說到死鎖的問題,而SQLite也可能出現死鎖。
下麵舉個例子:
連接1:BEGIN (UNLOCKED)
連接1:SELECT ... (SHARED)
連接1:INSERT ... (RESERVED)
連接2:BEGIN (UNLOCKED)
連接2:SELECT ... (SHARED)
連接1:COMMIT (PENDING,嘗試獲取EXCLUSIVE鎖,但還有SHARED鎖未釋放,返回SQLITE_BUSY)
連接2:INSERT ... (嘗試獲取RESERVED鎖,但已有PENDING鎖未釋放,返回SQLITE_BUSY)
現在2個連接都在等待對方釋放鎖,於是就死鎖了。當然,實際情況並沒那麼糟糕,任何一方選擇不繼續等待,回滾事務就行了。

不過要更好地解決這個問題,就必須更深入地瞭解事務了。
實際上BEGIN語句可以有3種起始狀態:
  • DEFERRED:預設值,開始事務時不獲取任何鎖。進行第一次讀操作時獲取SHARED鎖,進行第一次寫操作時獲取RESERVED鎖。
  • IMMEDIATE:開始事務時獲取RESERVED鎖。
  • EXCLUSIVE:開始事務時獲取EXCLUSIVE鎖。

現在考慮2個事務在開始時都使用IMMEDIATE方式:
連接1:BEGIN IMMEDIATE (RESERVED)
連接1:SELECT ... (RESERVED)
連接1:INSERT ... (RESERVED)
連接2:BEGIN IMMEDIATE (嘗試獲取RESERVED鎖,但已有RESERVED鎖未釋放,因此事務開始失敗,返回SQLITE_BUSY,等待用戶重試)
連接1:COMMIT (EXCLUSIVE,寫入完成後釋放)
連接2:BEGIN IMMEDIATE (RESERVED)
連接2:SELECT ... (RESERVED)
連接2:INSERT ... (RESERVED)
連接2:COMMIT (EXCLUSIVE,寫入完成後釋放)
這樣死鎖就被避免了。

而EXCLUSIVE方式則更為嚴苛,即使其他連接以DEFERRED方式開啟事務也不會死鎖:
連接1:BEGIN EXCLUSIVE (EXCLUSIVE)
連接1:SELECT ... (EXCLUSIVE)
連接1:INSERT ... (EXCLUSIVE)
連接2:BEGIN (UNLOCKED)
連接2:SELECT ... (嘗試獲取SHARED鎖,但已有EXCLUSIVE鎖未釋放,返回SQLITE_BUSY,等待用戶重試)
連接1:COMMIT (EXCLUSIVE,寫入完成後釋放)
連接2:SELECT ... (SHARED)
連接2:INSERT ... (RESERVED)
連接2:COMMIT (EXCLUSIVE,寫入完成後釋放)
不過在併發很高的情況下,直接獲取EXCLUSIVE鎖的難度比較大;而且為了避免EXCLUSIVE狀態長期阻塞其他請求,最好的方式還是讓所有寫事務都以IMMEDIATE方式開始。
順帶一提,要實現重試的話,可以使用sqlite3_busy_timeout()或sqlite3_busy_handler()函數。

由此可見,要想保證線程安全的話,可以有這4種方式:
  1. SQLite使用單線程模式,用一個專門的線程訪問資料庫。
  2. SQLite使用單線程模式,用一個線程隊列來訪問資料庫,隊列一次只允許一個線程執行,隊列里的線程共用一個資料庫連接。
  3. SQLite使用多線程模式,每個線程創建自己的資料庫連接。
  4. SQLite使用串列模式,所有線程共用全局的資料庫連接。
  三、sqlite3插入速度慢   1.像上述一樣顯示的給多個insert加上事務   sqlite在沒有顯式使用事務的時候會為每條insert都使用事務操作,而sqlite資料庫是以文件的形式存在磁碟中,就相當於每次訪問時都要打開一次文件,如果對數據進行大量的操作,時間都耗費在I/O操作上,所以很慢。 解決方法是顯式使用事務的形式提交:因為我們開始事務後,進行的大量操作的語句都保存在記憶體中,當提交時才全部寫入資料庫,此時,資料庫文件也就只用打開一次。   2.如果加上事務還是不行,可以嘗試修改同步模式   初用sqlite3插入數據時,插入每條數據大概需要100ms左右。如果是批量導入,可以引進事務提高速度。但是假設你的業務是每間隔幾秒插入幾條數據,顯然100ms是不能容許的。
解決辦法是,在調用sqlite3_open函數後添加下麵一行代碼:     sqlite3_exec(db, "PRAGMA synchronous = OFF; ", 0,0,0);       上面的解決辦法貌似治標不治本,為什麼加上上面的代碼行,速度會提高那麼多?   磁碟同步  1.如何設置: PRAGMA synchronous = FULL; (2)  PRAGMA synchronous = NORMAL; (1)  PRAGMA synchronous = OFF; (0)
  2.參數含義: 當synchronous設置為FULL (2), SQLite資料庫引擎在緊急時刻會暫停以確定數據已經寫入磁碟。這使系統崩潰或電源出問題時能確保資料庫在重起後不會損壞。FULL synchronous很安全但很慢。
  當synchronous設置為NORMAL(1), SQLite資料庫引擎在大部分緊急時刻會暫停,但不像FULL模式下那麼頻繁。 NORMAL模式下有很小的幾率(但不是不存在)發生電源故障導致資料庫損壞的情況。但實際上,在這種情況 下很可能你的硬碟已經不能使用,或者發生了其他的不可恢復的硬體錯誤。
  設置為synchronous OFF (0)時,SQLite在傳遞數據給系統以後直接繼續而不暫停。若運行SQLite的應用程式崩潰, 數據不會損傷,但在系統崩潰或寫入數據時意外斷電的情況下資料庫可能會損壞。另一方面,在synchronous OFF時 一些操作可能會快50倍甚至更多。在SQLite 2中,預設值為NORMAL.而在3中修改為FULL。  www.2cto.com  
  3.建議: 如果有定期備份的機制,而且少量數據丟失可接受,用OFF。      註意上面紅色加粗的字樣。總結:如果你的數據對安全性完整性等要求不是太高,可以採用設置為0的方法,畢竟只是“資料庫可能會損壞”,至於損壞幾率為多大,筆者也暫不知曉。。。。。。還沒遇到過損壞,不知什麼時候才會發生。   四、性能優化(可參考http://blog.csdn.net/tietao/article/details/6890350

很多人直接就使用了,並未註意到SQLite也有配置參數,可以對性能進行調整。有時候,產生的結果會有很大影響。
主要通過pragma指令來實現。
比如: 空間釋放、磁碟同步、Cache大小等。

1 auto_vacuum

最好不要打開auto_vacuum, Vacuum的效率非常低!

  PRAGMA auto_vacuum; 
  PRAGMA auto_vacuum = 0 | 1;
  查詢或設置資料庫的auto-vacuum標記。
  正常情況下,當提交一個從資料庫中刪除數據的事務時,資料庫文件不改變大小。未使用的文件頁被標記併在以後的添加操作中再次使用。這種情況下使用VACUUM命令釋放刪除得到的空間。
  當開啟auto-vacuum,當提交一個從資料庫中刪除數據的事務時,資料庫文件自動收縮, (VACUUM命令在auto-vacuum開啟的資料庫中不起作用)。資料庫會在內部存儲一些信息以便支持這一功能,這使得資料庫文件比不開啟該選項時稍微大一些。
  只有在資料庫中未建任何表時才能改變auto-vacuum標記。試圖在已有表的情況下修改不會導致報錯。

2 cache_size
建議改為8000
  PRAGMA cache_size; 
  PRAGMA cache_size = Number-of-pages;
  查詢或修改SQLite一次存儲在記憶體中的資料庫文件頁數。每頁使用約1.5K記憶體,預設的緩存大小是2000. 若需要使用改變大量多行的UPDATE或DELETE命令,並且不介意SQLite使用更多的記憶體的話,可以增大緩存以提高性能。
  當使用cache_size pragma改變緩存大小時,改變僅對當前對話有效,當資料庫關閉重新打開時緩存大小恢復到預設大小。 要想永久改變緩存大小,使用default_cache_size pragma.

3 case_sensitive_like
打開。不然搜索中文字串會出錯。
  PRAGMA case_sensitive_like; 
  PRAGMA case_sensitive_like = 0 | 1;
  LIKE運算符的預設行為是忽略latin1字元的大小寫。因此在預設情況下'a' LIKE 'A'的值為真。可以通過打開case_sensitive_like pragma來改變這一預設行為。當啟用case_sensitive_like,'a' LIKE 'A'為假而 'a' LIKE 'a'依然為真。

4 count_changes
打開。便於調試
  PRAGMA count_changes; 
  PRAGMA count_changes = 0 | 1;
  查詢或更改count-changes標記。正常情況下INSERT, UPDATE和DELETE語句不返回數據。 當開啟count-changes,以上語句返回一行含一個整數值的數據——該語句插入,修改或刪除的行數。

  註意:返回的行數不包括由(觸發器產生的插入,修改或刪除等改變的行數)。

5 page_size
  PRAGMA page_size; 
  PRAGMA page_size = bytes;
  查詢或設置page-size值。只有在未創建資料庫時才能設置page-size。頁面大小必須是2的整數倍且大於等於512小於等於8192。 上限可以通過在編譯時修改巨集定義SQLITE_MAX_PAGE_SIZE的值來改變。上限的上限是32768.

6 synchronous
如果有定期備份的機制,而且少量數據丟失可接受,用OFF
  PRAGMA synchronous; 
  PRAGMA synchronous = FULL; (2) 
  PRAGMA synchronous = NORMAL; (1) 
  PRAGMA synchronous = OFF; (0)
  查詢或更改"synchronous"標記的設定。第一種形式(查詢)返回整數值。 當synchronous設置為FULL (2), SQLite資料庫引擎在緊急時刻會暫停以確定數據已經寫入磁碟。 這使系統崩潰或電源出問題時能確保資料庫在重起後不會損壞。FULL synchronous很安全但很慢。 當synchronous設置為NORMAL, SQLite資料庫引擎在大部分緊急時刻會暫停,但不像FULL模式下那麼頻繁。 NORMAL模式下有很小的幾率(但不是不存在)發生電源故障導致資料庫損壞的情況。但實際上,在這種情況下很可能你的硬碟已經不能使用,或者發生了其他的不可恢復的硬體錯誤。 設置為synchronous OFF (0)時,SQLite在傳遞數據給系統以後直接繼續而不暫停。若運行SQLite的應用程式崩潰, 數據不會損傷,但在系統崩潰或寫入數據時意外斷電的情況下資料庫可能會損壞。另一方面,在synchronous OFF時 一些操作可能會快50倍甚至更多。
  在SQLite 2中,預設值為NORMAL.而在3中修改為FULL.

7 temp_store
使用2,記憶體模式。
  PRAGMA temp_store; 
  PRAGMA temp_store = DEFAULT; (0) 
  PRAGMA temp_store = FILE; (1) 
  PRAGMA temp_store = MEMORY; (2)
  查詢或更改"temp_store"參數的設置。當temp_store設置為DEFAULT (0),使用編譯時的C預處理巨集 TEMP_STORE來定義儲存臨時表和臨時索引的位置。當設置為MEMORY (2)臨時表和索引存放於記憶體中。 當設置為FILE (1)則存放於文件中。temp_store_directorypragma 可用於指定存放該文件的目錄。當改變temp_store設置,所有已存在的臨時表,索引,觸發器及視圖將被立即刪除。
  經測試,在類BBS應用上,通過以上調整,效率可以提高2倍以上。

附指令表集:

序號

指令

含義

預設值

1

auto_vacuum

空間釋放

0

2

cache_size

緩存大小

2000

3

case_sensitive_like

LIKE大小寫敏感

(註意:SQLite3.6.22不支持)

4

count_changes

變更行數

0

5

page_size

頁面大小

1024

6

synchronous

硬碟大小

2

7

temp_store;

記憶體模式

0


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 實例:ORACLE到ORACLE的數據傳遞 編寫job.xml文件,添加變數參數 執行datax.py文件時記得帶參數 格式:./datax.py –p"-Ddbname=*** -Dip=***" job.xml 實現指定的列名數據傳遞 修改reader裡面的colums和writer裡面的col... ...
  • 這篇隨筆用來記錄資料庫的學習,隨時更新補充,主要寫一些比較少關註的內容 PERSISTED: 說明物理存儲計算列的值(書本上的解釋,什麼鬼意思...) 現實操作中,CHECK、FOREIGN KEY 和 NOT NULL 約束要求計算列是持久化(PERSISTED)的。 大概它的作用就是這個吧? A ...
  • 最近項目用到了對oracle大欄位的讀寫,小白在這裡記錄下,方便自己以後用到,也希望對其他朋友有一點幫助。 由於項目的原因,這裡的blob只是對xml報文的讀寫,並沒有涉及到保存圖片等,因此下麵涉及的方法可能不全面,如有需要請自行查看其它大神博客。 一、讀blob 這裡對blob的讀是直接在資料庫建 ...
  • 修改用戶密碼 刷新系統許可權 ...
  • 1、從資料庫中檢索出的數據往往並不是最後要展示的格式,可以在資料庫端或者客戶端應用程式中完成轉換和格式化,但一般說來,在資料庫伺服器中完成要快很多。 2、拼接欄位:將幾個值連接到一起構成單個值。Oracel使用“||”連接,不支持“+”。例如: (1)別名:在欄位名後面加上“ AS 新列名”,重新賦 ...
  • 我們的項目用到了spring框架和mongdb資料庫,隨著mongodb升級到3.0已有半年時間,我們也開始隨之升級,但是3.0的用戶驗證有所更改,導致原來的很多配置無法再用。 經過幾天的嘗試後,終於成功的用spring配置驗證。 升級用了兩個新的jar包,分別是pring-data-mongodb ...
  • 簡介: Mysql 中提供了多種類型的日誌文件,分別反映 Mysql 的不同信息,瞭解它們很有必要。 1、Error log ( 錯誤日誌 ) 錯誤日誌記錄了 Mysql Server 運行過程中所有較為嚴重的警告和錯誤信息,以及 Mysql Server 每次啟動和關閉的詳細信息。 在預設情況下, ...
  • CHAR char (M) M字元,長度是M*字元編碼長度,M最大255。 驗證如下: VARCHAR VARCHAR(M),M同樣是字元,長度是M*字元編碼長度。它的限制比較特別,行的總長度不能超過65535位元組。 註意,以上表的預設字元集是latin1,字元長度是1個位元組,所以對於varchar ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...