你好,歡迎來到IOS教程網

 Ios教程網 >> IOS編程開發 >> IOS開發基礎 >> SQLite3性能深入分析

SQLite3性能深入分析

編輯:IOS開發基礎

SQLite3是移動終端最常用的數據庫,它非常輕量,編譯後只有數百KB。但它麻雀雖小,五髒俱全,它可以支持多線程,支持事務、約束以及幾乎所有的SQL常見特性。iOS中很多App經常會使用到SQLite,在使用SQLite的時候經常會遇到其性能問題。本文將深入SQLite內部實現,分析其性能優化途徑。

一些基本概念

在開始分析之前,首先需要了解一下數據庫的基本知識。

什麼是ACID?

這個術語在數據庫設計者非常熟悉的,而使用者往往不關注這些。ACID是“Atomicity, Consistency, Isolation, Durability”英文的縮寫,它用來確保一個數據庫事務的可靠性。中文意思是“原子性,一致性,隔離性,持久性”。維基百科上有其定義解釋:

  • 原子性:一個事務(transaction)中的所有操作,要麼全部完成,要麼全部不完成,不會結束在中間某個環節。事務在執行過程中發生錯誤,會被回滾(Rollback)到事務開始前的狀態,就像這個事務從來沒有執行過一樣。

  • 一致性:在事務開始之前和事務結束以後,數據庫的完整性沒有被破壞。這表示寫入的資料必須完全符合所有的預設規則,這包含資料的精確度、串聯性以及後續數據庫可以自發性地完成預定的工作。

  • 隔離性:當兩個或者多個事務並發訪問(此處訪問指查詢和修改的操作)數據庫的同一數據時所表現出的相互關系。事務隔離分為不同級別,包括讀未提交(Read uncommitted)、讀提交(read committed)、可重復讀(repeatable read)和串行化(Serializable)。

  • 持久性:在事務完成以後,該事務對數據庫所作的更改便持久地保存在數據庫之中,並且是完全的。

SQLite3是符合ACID的,它保證了即使在程序Crash或者進程被殺,甚至是內核崩潰或者斷電的情況下,數據庫依然是完整的。既要維持ACID特性,同時要保證性能最大化,這是數據庫設計上的一大挑戰。

SQLite3中的事務

SQLite3中可以使用BEGIN TRANSACTION和COMMIT TRANSACTION來開始和結束一個事務。如果你沒有添加這些事務語句,SQLite3會為你的每條SQL語句加上一個事務。

一次正常執行的事務過程

要想優化SQLite3的性能,那麼必須要了解SQLite3中一次事務執行過程。這裡已一個有寫操作的並成功執行的事務來舉例。

1. 初始狀態

數據庫打開後,未進行任何數據庫操作時大概是下圖的狀態。

sqlite3-step01.gif

image

圖片來自sqlite.org

這裡分了三個部分,左面是用戶空間,中間是內核緩存區(文件的讀寫緩存),右邊是物理磁盤設備(iOS的閃存)。在SQLite中數據最小的讀寫單位扇區(sector),通常是512B,圖中每個小矩形代表一個扇區。藍色代表未更改的原始數據,中間白色表示是空的,即此時數據沒有讀取到內核緩存區。

2. 准備讀取(加讀鎖)

sqlite3-step02.gif

image

任何寫操作都會先進行讀操作,因為寫之前要讀取數據庫的schema,插入和修改的位置等。在讀取操作之間要加上讀鎖。加讀鎖是為了防止其它數據庫連接進行寫操作,而保證讀取時數據不被破壞。這時其它數據庫的讀取操作依然可以正常執行。

3. 讀取數據

加了讀鎖之後就開始讀取數據了:

sqlite3-step03.gif

image

這裡讀取了3個扇區的數據,讀取時通過系統文件讀取調用,會從內核緩存中拷貝到用戶空間。(疑問:能否直接讀取到用戶空間?)

4. 准備修改數據(加寫鎖)

數據讀取完畢後,就准備開始修改數據了,修改數據之前首先要加寫鎖,此寫鎖可以和其它進程的讀鎖同時存在:

sqlite3-step04.gif

image

5. 建立回滾日志

開始寫操作之前,先建立一個回滾日志文件,已便進行回滾操作。將更改之前的舊數據保存到回滾日志文件中。

sqlite3-step05.gif

image

回滾日志文件包含一個頭信息(綠色部分),記錄回滾必要信息。

6. 在用戶空間中修改數據

sqlite3-step06.gif

image

圖中粉色表示已修改的數據。

7. 沖(fsync)回滾日志文件

用戶空間修改數據後,未確保回滾日志文件可靠,必須把回滾日志文件沖入物理磁盤進行持久存儲。這樣以確保內核崩潰或斷電後依然可恢復數據。

sqlite3-step07.gif

image

8. 加互斥鎖

准備開始真正的寫文件了,要加互斥鎖了。互斥鎖可以和已經打開的讀鎖同時存在,但不允許新建讀鎖了。

sqlite3-step08.gif

image

9. 寫數據庫文件

現在可以安全的寫數據文件了。

sqlite3-step09.gif

image

10. 沖(fsync)數據庫文件

沖數據庫文件到持久存儲設備。

sqlite3-step10.gif

image

11. 刪除回滾日志

沖入數據庫文件後才能刪除回滾日志,確保內核崩潰或斷電後依然可恢復數據。

sqlite3-step11.gif

image

12. 釋放鎖

sqlite3-step12.gif

image

過程分析

為確保ACID,一次數據庫事務竟需要這麼多步驟。下面對此過程進行分析一下:

  • 一次文件創建(回滾日志)

  • 兩次文件寫入

  • 兩次文件沖入(回滾日志,數據庫文件)

  • 一次文件刪除(回滾日志)

  • 加了3次鎖,最後一次不允許讀取

mmap

在讀取和寫入過程中,每次都將用戶空間的數據和內核空間的數據拷貝一次,能否直接將文件讀取到用戶空間?SQLite3提供了mmap方式的IO。

打開mmap方式的IO只要執行下面語句即可:

sqlite3_exec(db, "PRAGMA mmap_size=268435456;", NULL, NULL, NULL);

理論上mmap方式能減少內核和用戶空間的IO,但在iOS系統中,這個從我這裡測試效果看,影響並不大。

異步IO

在事務操作中有大量寫操作,能否將寫操作放到後台線程執行?SQLite3是支持這種的,SQLite可以自定義文件的讀取、寫入等操作方式。需要配置一下VFS結構即可:

struct sqlite3_vfs {
  int iVersion;            /* Structure version number (currently 3) */
  int szOsFile;            /* Size of subclassed sqlite3_file */
  int mxPathname;          /* Maximum file pathname length */
  sqlite3_vfs *pNext;      /* Next registered VFS */
  const char *zName;       /* Name of this virtual file system */
  void *pAppData;          /* Pointer to application-specific data */
  int (*xOpen)(sqlite3_vfs*, const char *zName, sqlite3_file*,

int flags, int *pOutFlags);

int (*xDelete)(sqlite3_vfs*, const char *zName, int syncDir);
int (*xAccess)(sqlite3_vfs*, const char *zName, int flags, int *pResOut);
int (*xFullPathname)(sqlite3_vfs*, const char *zName, int nOut, char *zOut);
void *(*xDlOpen)(sqlite3_vfs*, const char *zFilename);
void (*xDlError)(sqlite3_vfs*, int nByte, char *zErrMsg);
void (*(*xDlSym)(sqlite3_vfs*,void*, const char *zSymbol))(void);
void (*xDlClose)(sqlite3_vfs*, void*);
int (*xRandomness)(sqlite3_vfs*, int nByte, char *zOut);
int (*xSleep)(sqlite3_vfs*, int microseconds);
int (*xCurrentTime)(sqlite3_vfs*, double*);
int (*xGetLastError)(sqlite3_vfs*, int, char *);

這個結構定義了各種文件操作的函數指針。開啟異步IO需要實現這個結構的定制,官方已提供了這個擴展,下載後加入工程即可:

http://www.sqlite.org/src/tree?name=ext/async

這個我測試了一下,性能提升並不明顯,雖然做了異步,但主線程獲取鎖的等待時間增加太多,實際性能影響不大。

關閉沖文件

事務過程中有兩次沖文件操作,能否將這兩次沖文件關閉?SQLite可以關閉這兩次強制沖文件操作的。

可以通過一下方式關閉:

sqlite3_exec(db, "PRAGMA synchronous=OFF;", NULL, NULL, NULL);

關閉沖文件確實提高了不少性能,但在內核崩潰或者系統斷電時導致數據庫寫入不完整。即使如此,程序本身Crash還是安全的。

Write-Ahead Logging (WAL) 模式

SQLite3.7.0中新增了WAL模式,iOS大概在5.0中引入此支持。WAL模式正好和傳統模式相反,WAL模式會將修改的數據單獨寫到一個WAL文件中,而且多個事務可以共用一個WAL文件。

checkpoint

WAL模式裡有一個重要概念checkpoint。每個checkpoint默認是1000扇區的數據,此值可動態調整。當WAL文件裡的數據更改量達到checkpoint時才會將WAL裡的數據寫回實際的數據庫。

WAL沖入優化

WAL默認在checkpoint時進行文件沖入(fsync)操作,你也可以使其每次事務都進行沖入,以確保數據完全可靠:

sqlite3_exec(db, "PRAGMA synchronous=FULL;", NULL, NULL, NULL);

WAL並發優化

WAL模式會在共享內存中根據數據順序建立索引,每個讀操作都會記錄一下最新的數據更改索引,讀操作只會讀取此索引之前的數據,而寫操作可以繼續在WAL中追加數據,並發性能有一定提升。

那麼WAL模式下相比傳統模式有以下改進:

  • 一次文件創建(回滾日志) —> 僅在第一次和達到checkpoint時創建一次文件

  • 兩次文件寫入 -> 大部分情況下只有WAL文件一次寫入

  • 兩次文件沖入(回滾日志,數據庫文件)-> FULL模式下有一次沖入,Normal模式下僅在checkpoint時沖入

  • 一次文件刪除(回滾日志) -> 大部分情況下不用刪除

  • 加了3次鎖,最後一次不允許讀取 ->

當然WAL模式也有一些缺點:

  • 當每個事務數據量比較大時,接近或超過1000頁的數據量時,會導致WAL內容頻繁同步至實際數據庫文件,導致性能下降。

  • WAL在並發性方面的優化使用了系統共享內存,那麼在一些網絡文件系統中就無法使用。iOS目前並不存在這種文件系統。

各種模式性能實戰分析

下面我對各種模式進行測試,各模式如下:

  • 正常模式:正常創建的數據庫,不做任何配置。

  • 內存映射:使用語句“PRAGMA mmap_size=268435456;”開啟內存映射。

  • 異步IO:加入官方異步IO擴展並開啟。

  • WAL模式:使用語句“PRAGMA journal_mode=WAL;”開啟WAL模式。

  • sync OFF:使用語句“PRAGMA synchronous=OFF;”關閉文件強制同步。

  • WAL(sync Full):使用語句“PRAGMA journal_mode=WAL;PRAGMA synchronous=FULL;”開啟WAL模式和全同步。

  • 內存數據庫:使用特殊文件名“:memory:”打開的數據庫。

測試分三種情況:

  • 1000條小數據寫入

  • 1000條小數據寫入(合並為一個事務)

  • 100條大數據(474940 Bytes)寫入

測試機型為iPhone4,系統為iOS7.1。

1000條小數據寫入

sqlite-perfermance-chart01.png

image

小數據WAL存在明顯優勢,關閉fsync的表現也不賴,但不完全可靠了,不過卻還沒有WAL(sync Full)模式快。

1000條小數據寫入(合並為一個事務)

sqlite-perfermance-chart02.png

image

合並為一個事務後,各模式差別不大。因為IO次數有限。相比不合並事務,性能急劇提升到100毫秒級,請注意本圖的單位。

100條大數據(474940 Bytes)寫入

sqlite-perfermance-chart03.png

image

100條大數據時WAL模式性能相比其它模式都差一些,單個事務數據量比較大的情況不推薦WAL,或者要修改WAL的checkpoint設置,改的更大一些,以免產生過多的checkpoint。

結論

  • 異步IO似乎並不能提高多少性能,官方已經deprecate它了,推薦使用WAL模式。

  • 大量小記錄寫入(不合並為事務)時,一般模式即使關閉文件sync,還沒有WAL全sync模式快。

  • 總體數據占用量少的,而且可重建恢復的數據,建議使用內存數據庫,必要時做備份到閃存文件。

  • 總體數據占用量大,但是可重建恢復的數據庫,可以關閉synchronous以提高性能。

  • 操作頻繁,單條記錄數據量小的,建議使用WAL模式。

  • 操作少,單條記錄數據量大,建議使用一般數據庫,不要使用WAL模式。


  1. 上一頁:
  2. 下一頁:
蘋果刷機越獄教程| IOS教程問題解答| IOS技巧綜合| IOS7技巧| IOS8教程
Copyright © Ios教程網 All Rights Reserved