本文主要闡述SQLTE數(shù)據(jù)庫文件在異常場景下發(fā)生損壞的原因及提供相應(yīng)的解決方案。本文涉及代碼部分的SQLITE庫使用SQLITE_VERSION 3.20.1。
SQLTE數(shù)據(jù)庫在應(yīng)用程序崩潰,操作系統(tǒng)崩潰,甚至在處理事務(wù)過程中發(fā)生電源故障等場景下具有強(qiáng)抗破壞性。SQLite可以抵御數(shù)據(jù)庫損壞,但它并不是免疫的。本章節(jié)描述了SQLite數(shù)據(jù)庫可能損壞的各種操作。
2.1 非法訪問數(shù)據(jù)庫文件
SQLTE數(shù)據(jù)庫文件是普通的磁盤文件。這意味著任何進(jìn)程都可以打開文件并用垃圾覆蓋它。SQLite庫沒有什么能夠防范這種情況。
2.1.1 文件描述符錯(cuò)誤
在一些數(shù)據(jù)庫文件損壞報(bào)告中,會(huì)出現(xiàn)文件描述符錯(cuò)誤。其場景發(fā)生在進(jìn)程擁有多個(gè)文件描述符時(shí),關(guān)閉其中的文件描述符后,打開SQLITE數(shù)據(jù)庫。但一些線程繼續(xù)寫入舊的文件描述符,導(dǎo)致SQLITE數(shù)據(jù)庫被其他數(shù)據(jù)覆蓋。在SQLITE源文件中可以通過設(shè)置相關(guān)參數(shù)使文件描述符拒絕使用低編號(hào)從而避免出現(xiàn)上述問題。
- /*
- ** Do not accept any file descriptor less than this value, in order to avoid
- ** opening database file using file descriptors that are commonly used for
- ** standard input, output, and error.
- */
- #ifndef SQLITE_MINIMUM_FILE_DESCRIPTOR
- # define SQLITE_MINIMUM_FILE_DESCRIPTOR 3
- #endif
2.1.2 數(shù)據(jù)庫備份錯(cuò)誤
SQLITE數(shù)據(jù)庫的狀態(tài)由數(shù)據(jù)庫文件和日志文件控制,日志文件與數(shù)據(jù)庫文件具有相同的名稱,并添加了-journal或-wal后綴。在靜態(tài)狀態(tài)下,日志文件不存在,只有數(shù)據(jù)庫文件。但是在數(shù)據(jù)庫事務(wù)處理過程中,會(huì)產(chǎn)生日志文件。
在一些數(shù)據(jù)庫文件損壞報(bào)告中,會(huì)出現(xiàn)數(shù)據(jù)庫備份錯(cuò)誤。其場景發(fā)生在一些后臺(tái)程序在數(shù)據(jù)庫事務(wù)處理過程中進(jìn)行數(shù)據(jù)庫文件備份副本,備份文件中可能會(huì)存在新數(shù)據(jù)和舊數(shù)據(jù)導(dǎo)致數(shù)據(jù)庫文件損壞。
制作SQLITE數(shù)據(jù)庫的可靠備份最佳方法是使用SQLITE庫中的備份API,對(duì)正在運(yùn)行的數(shù)據(jù)庫進(jìn)行在線備份可以通過sqlite3_backup_step函數(shù)設(shè)置頁數(shù)后,每250us調(diào)用一次,直至備份完成。
- /* pDb為已打開的數(shù)據(jù)庫鏈接,zFilename為數(shù)據(jù)庫副本,xProgress為進(jìn)度條回調(diào)函數(shù),mode==1時(shí)為備份,mode!=1時(shí)為還原 */
- int backupDb(sqlite3 *pDb, const char *zFilename, void(*xProgress)(int, int), int mode)
- {
- int rc = 0;
- sqlite3 *pFile = 0;
- sqlite3_backup *pBackup = 0;
- rc = sqlite3_open(zFilename, &pFile);
- if( rc==SQLITE_OK )
- {
- if(mode == 1)
- {
- pBackup = sqlite3_backup_init(pDb, "main", pFile, "main");
- }
- else
- {
- pBackup = sqlite3_backup_init(pFile, "main", pDb, "main");
- }
- if( pBackup )
- {
- do
- {
- rc = sqlite3_backup_step(pBackup, 5);
- /* Completion = 100% * (pagecount() - remaining()) / pagecount() */
- xProgress(sqlite3_backup_remaining(pBackup), sqlite3_backup_pagecount(pBackup));
- if( rc==SQLITE_OK || rc==SQLITE_BUSY || rc==SQLITE_LOCKED )
- {
- sqlite3_sleep(250);
- }
- }
- while( rc==SQLITE_OK || rc==SQLITE_BUSY || rc==SQLITE_LOCKED );
- (void)sqlite3_backup_finish(pBackup);
- }
- rc = sqlite3_errcode(pFile);
- }
- (void)sqlite3_close(pFile);
- return rc;
- }
2.1.3 數(shù)據(jù)庫日志錯(cuò)誤
SQLITE通常將所有內(nèi)容存儲(chǔ)在單個(gè)磁盤文件中,但在執(zhí)行事務(wù)時(shí)會(huì)將恢復(fù)數(shù)據(jù)庫所需要的信息存儲(chǔ)在日志文件中。在程序崩潰或者電源故障后,SQLITE必須查看日志文件才能從崩潰或電源故障中恢復(fù)。
在一些數(shù)據(jù)庫文件損壞報(bào)告中,會(huì)出現(xiàn)數(shù)據(jù)庫日志錯(cuò)誤。其場景發(fā)生在程序崩潰或電源故障后,用戶移動(dòng)或刪除或重命名日志文件,則自動(dòng)恢復(fù)將不起作用,并且數(shù)據(jù)庫文件可能會(huì)損壞。
為了避免出現(xiàn)數(shù)據(jù)庫日志錯(cuò)誤的出現(xiàn),應(yīng)當(dāng)加強(qiáng)對(duì)用戶的科普,并遵守下面幾點(diǎn):
1 -journal或-wal 結(jié)尾的文件是日志文件而不是垃圾文件,不能刪除。
2 如果需要移動(dòng)數(shù)據(jù)庫文件的位置,請將其與日志文件一起移動(dòng)。
3 日志文件具有唯一性,不同數(shù)據(jù)庫文件不能共用一個(gè)日志文件。
2.2 文件鎖的破壞
SQLITE在數(shù)據(jù)庫文件、日志文件上使用文件鎖來協(xié)調(diào)并發(fā)進(jìn)程之間的訪問。當(dāng)文件鎖被破壞時(shí)。二個(gè)線程或進(jìn)程可能會(huì)嘗試同時(shí)對(duì)數(shù)據(jù)庫進(jìn)行不兼容的更改,從而導(dǎo)致數(shù)據(jù)庫損壞。
2.2.1 文件系統(tǒng)錯(cuò)誤
在一些數(shù)據(jù)庫文件損壞報(bào)告中,會(huì)出現(xiàn)文件系統(tǒng)錯(cuò)誤。SQLITE依賴底層文件來進(jìn)行鎖定,但是一些文件系統(tǒng)在鎖定邏輯中包含錯(cuò)誤,如果在該類文件系統(tǒng)上使用SQLITE,并且存在二個(gè)或二個(gè)以上的線程或進(jìn)程嘗試同時(shí)訪問一個(gè)數(shù)據(jù)庫,則可能造成數(shù)據(jù)庫損壞。
為了避免出現(xiàn)文件系統(tǒng)錯(cuò)誤,在network filesystems 和NFS這類文件系統(tǒng)上使用SQLITE應(yīng)注意鎖協(xié)議問題。
2.2.2 Close錯(cuò)誤
SQLITE在UNIX平臺(tái)使用的默認(rèn)鎖定機(jī)制為POSIX advisory locking。在同一進(jìn)程中,一個(gè)數(shù)據(jù)庫文件的多個(gè)文件描述符共用一個(gè)POSIX advisory locking。
在一些數(shù)據(jù)庫文件損壞報(bào)告中,會(huì)出現(xiàn)Close錯(cuò)誤。其場景發(fā)生在當(dāng)二個(gè)或二個(gè)以上的線程或進(jìn)程訪問同一數(shù)據(jù)庫文件時(shí),建立了不同的數(shù)據(jù)庫鏈接。此時(shí)出現(xiàn)第三個(gè)線程,通過open、read、close函數(shù)獲取同一數(shù)據(jù)庫文件內(nèi)容。由于close函數(shù)會(huì)破壞POSIX advisory locking。
但其他線程并不知道POSIX已損壞,這可能導(dǎo)致二個(gè)或二個(gè)以上線程或進(jìn)程同時(shí)嘗試寫入數(shù)據(jù)庫,導(dǎo)致數(shù)據(jù)庫文件損壞。
為了避免出現(xiàn)Close錯(cuò)誤,程序?qū)QLITE數(shù)據(jù)庫文件的訪問應(yīng)該使用SQLITE庫提供的API接口。
2.2.3 多副本錯(cuò)誤
在一些數(shù)據(jù)庫文件損壞報(bào)告中,會(huì)出現(xiàn)多副本錯(cuò)誤。其場景出現(xiàn)在一個(gè)應(yīng)用程序鏈接多個(gè)數(shù)據(jù)庫副本。SQLITE在解決POSIX advisory locking問題上使用了全局變量(互斥保護(hù))。
當(dāng)存在多個(gè)副本鏈接時(shí),全局列表將有多個(gè)實(shí)例,則無法解決POSIX advisory locking問題。
導(dǎo)致數(shù)據(jù)庫文件損壞。
為了避免出現(xiàn)多副本錯(cuò)誤,應(yīng)用程序在同一時(shí)間應(yīng)該保證只鏈接一個(gè)SQLITE副本。
2.2.3 鎖協(xié)議錯(cuò)誤
SQLITE在unix平臺(tái)使用默認(rèn)鎖定機(jī)制時(shí)POSIX advisory locking。應(yīng)用程序可以通過sqlite3_open_v2函數(shù)可以選擇更適合某些文件系統(tǒng)的sqlite3_vfs。例如NFS文件系統(tǒng)不支持POSIX advisory locking,則應(yīng)當(dāng)選擇dot-file locking。
在一些數(shù)據(jù)庫文件損壞報(bào)告中,會(huì)出現(xiàn)鎖協(xié)議錯(cuò)誤。其場景出現(xiàn)在一個(gè)應(yīng)用程序正在POSIX advisory locking訪問數(shù)據(jù)庫文件,另一個(gè)應(yīng)用程序正在使用dot-file locking訪問同一數(shù)據(jù)庫文件,那么這二個(gè)應(yīng)用程序?qū)⒖床坏奖舜说逆i定而無法協(xié)調(diào)數(shù)據(jù)庫訪問,導(dǎo)致數(shù)據(jù)庫文件發(fā)生損壞。
為了避免出現(xiàn)鎖協(xié)議錯(cuò)誤,當(dāng)數(shù)據(jù)庫文件被多個(gè)應(yīng)用程序共享時(shí),應(yīng)當(dāng)統(tǒng)一鎖協(xié)議。
2.2.4 跨進(jìn)程錯(cuò)誤
在一些數(shù)據(jù)庫文件損壞報(bào)告中,會(huì)出現(xiàn)跨進(jìn)程錯(cuò)誤。其場景出現(xiàn)在一個(gè)應(yīng)用程序打開SQLITE數(shù)據(jù)庫連接后fork。然后嘗試在子進(jìn)程中使用該數(shù)據(jù)庫連接。這種操作將導(dǎo)致各種鎖定問題,很容易損壞數(shù)據(jù)庫文件,
為了避免出現(xiàn)跨進(jìn)程錯(cuò)誤,對(duì)于多進(jìn)程的應(yīng)用程序,子進(jìn)程使用的數(shù)據(jù)庫連接必須在該子進(jìn)程中建立,同時(shí)不要嘗試在父進(jìn)程中建立SQLITE數(shù)據(jù)庫連接,因?yàn)楦高M(jìn)程調(diào)用sqlite3_close會(huì)清理子進(jìn)程的內(nèi)容,導(dǎo)致數(shù)據(jù)庫損壞。
2.3 同步失敗
ACID是數(shù)據(jù)庫事務(wù)正確執(zhí)行的四個(gè)基本要素,即原子性、一致性、隔離性、持久性。
SQLITE支持事務(wù)處理,則必須支持ACID才能保證數(shù)據(jù)的正確性。
計(jì)算機(jī)在數(shù)據(jù)庫運(yùn)行時(shí)可分為磁盤、磁盤緩存和用戶空間,而數(shù)據(jù)庫的數(shù)據(jù)存儲(chǔ)在磁盤緩存或用戶空間時(shí),一旦系統(tǒng)崩潰或者電源故障則這些數(shù)據(jù)將會(huì)丟失。為了保證原子性和一致性,SQLITE在事務(wù)執(zhí)行過程中會(huì)不斷使用fsync系統(tǒng)調(diào)用將數(shù)據(jù)刷新到持久存儲(chǔ)的磁盤中。
SQLITE確定數(shù)據(jù)刷新到磁盤才返回處理結(jié)果。因此在用戶空間層面,SQLITE的事務(wù)處理具備ACID要素。但是在一些場景下,數(shù)據(jù)從用戶空間往磁盤實(shí)現(xiàn)同步的過程中,往往會(huì)違法ACID要素。
2.3.1 磁盤驅(qū)動(dòng)器錯(cuò)誤
在一些數(shù)據(jù)庫文件損壞報(bào)告中,會(huì)出現(xiàn)磁盤驅(qū)動(dòng)器錯(cuò)誤。其場景出現(xiàn)在部分磁盤驅(qū)動(dòng)器一旦達(dá)到軌道緩沖區(qū)并且在實(shí)際寫入氧化物之前就會(huì)報(bào)到內(nèi)容在磁盤上是安全的。一旦在實(shí)際寫入氧化物之前發(fā)生斷電,而SQLITE卻認(rèn)為已實(shí)現(xiàn)數(shù)據(jù)同步,則可能會(huì)發(fā)生數(shù)據(jù)庫文件損壞。
為了避免出現(xiàn)磁盤驅(qū)動(dòng)器錯(cuò)誤,在對(duì)ACID要求高的場景下,應(yīng)該檢測SQLITE所運(yùn)行的操作系統(tǒng)和硬件是否在同步上嚴(yán)謹(jǐn)。此外COMMIT期間的同步失敗可能會(huì)導(dǎo)致數(shù)據(jù)庫文件耐久丟失,但不會(huì)損壞數(shù)據(jù)庫文件。
本章主要闡述了SQLITE數(shù)據(jù)存儲(chǔ)過程中的原子提交錯(cuò)覺技術(shù)。事務(wù)性數(shù)據(jù)庫SQLITE的一個(gè)重要特性是“原子提交”。
原子提交意味著在一個(gè)事務(wù)中,數(shù)據(jù)庫要么發(fā)生改動(dòng),要么不發(fā)生改動(dòng),不存在中間態(tài),對(duì)數(shù)據(jù)庫文件的寫入是瞬間發(fā)生的。在硬件層次上數(shù)據(jù)寫入單個(gè)磁盤扇區(qū)需要時(shí)間,不可能同時(shí)將數(shù)據(jù)瞬間寫入到不同扇區(qū)中。但是SQLITE數(shù)據(jù)庫的原子提交邏輯使得事務(wù)看起來就是及時(shí)和同時(shí)寫入的。這種特性即使是SQLITE處理事務(wù)過程中因操作系統(tǒng)崩潰或電源故障而中斷也不影響其原子性。下面將詳細(xì)說明SQLITE處理事務(wù)的流程。
3.1 初始狀態(tài)
如圖3.1所示,最右側(cè)表示大容量存儲(chǔ)設(shè)備,每一個(gè)矩形表示一個(gè)扇區(qū),藍(lán)色表示數(shù)據(jù)庫的原始數(shù)據(jù)。中間區(qū)域是操作系統(tǒng)的磁盤緩存,最左側(cè)表示使用SQLITE的應(yīng)用程序內(nèi)存內(nèi)容。當(dāng)應(yīng)用程序打開SQLITE連接時(shí),尚未讀寫任何信息,因此用戶空間和磁盤緩存為空。
圖 3.1 初始狀態(tài)
3.2 獲取讀鎖定
應(yīng)用程序在對(duì)SQLITE進(jìn)行寫操作之前,需要對(duì)SQLITE進(jìn)行讀操作,獲取sqlite_master表中的數(shù)據(jù)庫模式。應(yīng)用程序讀取數(shù)據(jù)庫文件的第一步是獲取數(shù)據(jù)庫文件的共享鎖。
共享鎖允許數(shù)據(jù)庫文件被二個(gè)或二個(gè)以上的數(shù)據(jù)庫連接同時(shí)讀取,但是共享鎖會(huì)在應(yīng)用程序讀取數(shù)據(jù)庫文件的過程中,阻止數(shù)據(jù)庫文件被另一個(gè)數(shù)據(jù)庫連接寫入。共享鎖存在于操作系統(tǒng)的磁盤緩存上,如果創(chuàng)建鎖的進(jìn)程退出或者操作系統(tǒng)崩潰或者斷電,則鎖定將立即消失。
圖 3.2 獲取讀鎖定
3.3 讀取數(shù)據(jù)庫信息
應(yīng)用程序獲取到共享鎖后,便從數(shù)據(jù)庫文件中讀取信息。這個(gè)過程首相是將信息從磁盤讀取到磁盤緩存中,然后從磁盤緩存?zhèn)鬏數(shù)接脩艨臻g中。
圖 3.3 讀取數(shù)據(jù)庫信息
3.4 獲取保留鎖
應(yīng)用程序在讀數(shù)據(jù)庫進(jìn)行寫操作之前需要獲取數(shù)據(jù)庫文件的保留鎖。保留鎖類似與共享鎖,保留鎖和共享鎖都允許其他進(jìn)程從數(shù)據(jù)庫文件中進(jìn)行讀操作,保留鎖可以和其他進(jìn)程的共享鎖共存。但是一個(gè)數(shù)據(jù)庫文件上只能有一個(gè)保留鎖,所以在同一時(shí)間,數(shù)據(jù)庫文件只允許一個(gè)進(jìn)程對(duì)數(shù)據(jù)庫進(jìn)行寫操作。
圖 3.4 獲取保留鎖
3.5 創(chuàng)建日志文件
應(yīng)用程序在對(duì)數(shù)據(jù)庫文件進(jìn)行寫操作之前,需要?jiǎng)?chuàng)建一個(gè)單獨(dú)的日志文件,并在日志中寫入要更改的數(shù)據(jù)庫頁面的原始內(nèi)容。日志文件包含數(shù)據(jù)庫恢復(fù)數(shù)據(jù)庫文件原始狀態(tài)所需要的信息。
如圖3.5,日志文件的綠色部分表示日志文件包含一個(gè)小標(biāo)題,它記錄了數(shù)據(jù)庫文件的原始大下,日志文件中的數(shù)據(jù)庫頁面原始內(nèi)容將與頁碼一起存儲(chǔ)。因此應(yīng)用程序在對(duì)數(shù)據(jù)庫文件進(jìn)行寫入操作導(dǎo)致數(shù)據(jù)庫文件增長時(shí),SQLITE仍然知道數(shù)據(jù)庫原始大小。
應(yīng)用程序在創(chuàng)建新文件時(shí),大多數(shù)操作系統(tǒng)將在磁盤緩存中創(chuàng)建它,直到操作系統(tǒng)空閑時(shí),才會(huì)在磁盤上創(chuàng)建該文件。
圖 3.5 創(chuàng)建日志文件
3.6 在用戶空間的寫操作
每一個(gè)數(shù)據(jù)庫連接都有自己的用戶空間私有副本,應(yīng)用程序在用戶空間中所做的更改,僅對(duì)該數(shù)據(jù)庫連接可見。因此一個(gè)進(jìn)程忙于修改數(shù)據(jù)庫,其他進(jìn)程也可以繼續(xù)讀取原始狀態(tài)下的數(shù)據(jù)庫文件。
圖 3.6 在用戶空間的寫操作
3.7 日志文件寫入磁盤
將日志文件刷新到非易失存儲(chǔ)的磁盤上,這是確保數(shù)據(jù)庫能夠承受意外斷電的關(guān)鍵步驟。此步驟需要二個(gè)單獨(dú)的flush操作。第一次flush,日志的基本內(nèi)容寫入磁盤。第二次flush將標(biāo)頭寫入磁盤。
圖 3.7 日志文件寫入磁盤
3.8 獲取獨(dú)家鎖
在更改數(shù)據(jù)庫文件之前,應(yīng)用程序必須獲取數(shù)據(jù)庫的獨(dú)家鎖。獲取獨(dú)家鎖的第一步是獲取數(shù)據(jù)庫文件的掛起鎖,第二步是將掛起鎖升級(jí)為獨(dú)家鎖。
掛起鎖允許具有共享鎖的進(jìn)程繼續(xù)讀取數(shù)據(jù)庫文件,但阻止建立新的共享鎖。掛起鎖的作用是防止出現(xiàn)writer starvation問題。當(dāng)其他進(jìn)程完成讀取數(shù)據(jù)庫文件操作后,該數(shù)據(jù)庫文件上不存在共享鎖,則將掛起鎖升級(jí)為獨(dú)家鎖。
圖 3.8 獲取獨(dú)家鎖
3.9 寫入數(shù)據(jù)庫文件
應(yīng)用程序一旦獲取到獨(dú)家鎖,那么數(shù)據(jù)庫文件則不被其他進(jìn)程所讀取。此刻,應(yīng)用程序?qū)?shù)據(jù)庫進(jìn)行寫操作是安全的。通常這個(gè)寫操作,只會(huì)將更改寫入到磁盤緩存中。
圖 3.9 寫入數(shù)據(jù)庫文件
3.10 刷新到磁盤
SQLITE需要經(jīng)過一次flash操作,以確保將所有數(shù)據(jù)庫的更改寫入到非易失性存儲(chǔ)磁盤中。這是確保數(shù)據(jù)庫在沒有損壞的情況下承受斷電的關(guān)鍵步驟。
圖 3.10 刷新到磁盤
3.11 刪除日志
SQLITE在將數(shù)據(jù)庫的更改安全的寫入到磁盤后,下一步便是刪除日志文件。這是事務(wù)提交的瞬間,相當(dāng)于COMMIT操作。
在刪除日志之前發(fā)生電源故障或者系統(tǒng)崩毀,則應(yīng)用程序重新與數(shù)據(jù)庫建立連接后,SQLITE將通過日志文件自動(dòng)將數(shù)據(jù)庫文件恢復(fù)回原始狀態(tài)。如果在刪除日志之后發(fā)生電源故障或者系統(tǒng)崩潰,此時(shí)事務(wù)已完成,數(shù)據(jù)庫文件不會(huì)發(fā)生損壞。
在代碼層面COMMIT操作是一個(gè)原子操作。從用戶進(jìn)程角度看,進(jìn)程通過詢問操作系統(tǒng)此文件是否存在,返回的只有是或否二種狀態(tài),不存在中間態(tài)。如果存在日志文件,則事務(wù)不完整進(jìn)行回滾,如果存在日志文件,則事務(wù)已提交。
但是刪除文件實(shí)際上不是原子操作,許多系統(tǒng)上刪除文件的行為是需要消耗時(shí)間的。在刪除文件這個(gè)操作上,SQLITE進(jìn)行了優(yōu)化。SQLITE將日志文件截?cái)酁榱阕止?jié)或者用零覆蓋日志文件頭。日志標(biāo)題的任何部分不正確則SQLITE不會(huì)進(jìn)行回滾,SQLITE默認(rèn)此刻事務(wù)已提交。SQLITE假設(shè)標(biāo)頭的第一個(gè)字節(jié)歸零是原子操作。
圖 3.11 刪除日志文件
3.12 釋放鎖定
應(yīng)用程序提交事務(wù)的最后一步是釋放獨(dú)占鎖,以便其他進(jìn)程可以再次訪問數(shù)據(jù)庫文件。
如圖3.12釋放獨(dú)占鎖后,SQLITE將不清除用戶空間的數(shù)據(jù),以便于數(shù)據(jù)重用。
在重用用戶空間的數(shù)據(jù)之前,應(yīng)用程序?qū)⑾热カ@取共享鎖,以保證在釋放獨(dú)占鎖之后和獲取共享鎖之前,數(shù)據(jù)庫文件沒有進(jìn)行修改。數(shù)據(jù)庫文件的第一頁存在計(jì)數(shù)器,每次對(duì)數(shù)據(jù)庫文件進(jìn)行的修改,都會(huì)使計(jì)數(shù)器遞增,應(yīng)用程序可以通過計(jì)數(shù)器查明用戶空間的數(shù)據(jù)是否可以重用。
圖3.12 釋放鎖定
聯(lián)系客服