您好,登錄后才能下訂單哦!
這篇文章給大家介紹SQLite原子提交的原理是什么,內容非常詳細,感興趣的小伙伴們可以參考借鑒,希望對大家能有所幫助。
1.0 簡介
“原子提交”是SQLite這種支持事務的數據庫的一個重要特性。原子提交意味著某個事務中數據庫的變化會完整完成或者根本不完成。原子提交意味著不同的寫入分別寫入到數據庫的不同部分就似同時發生在同一個時間點一樣。
實際上硬件會連續的寫到海量存儲器中,只是寫一個扇區所用的時間非常少。所以,同時或瞬間寫入到數據文件的不同部分成為可能。SQLite的原子提交邏輯會使得一個事務中的變化就象同時發生的一樣。
事務的原子是SQLite的重要特性,即使事務由于操作系統出錯或掉電發生中斷也能保持其原子性。
本文描述了SQLite實現原子操作的技術。
2.0 硬件設定
在這往篇文章中,我們把海量存儲特指定為“硬盤”,即使它可能是flash memory.
我們假定硬盤是以扇區為單位進行整塊寫入的。我們不能單獨修改硬盤的小于扇區的部分。如果需要修改硬盤小于扇區的部分,你也必須整個讀入此部分所在扇區,對此扇區進行修改,然后將整個扇區寫回硬盤。
在傳統的Spinning disk中,扇區是最小的傳輸單元---無論是讀還是寫。然而,對于flash memory,每次讀的最小數目通常都遠小于最小寫操作數目。SQLite 只關心寫操作的最小數目,因此在本文中,當我們說“扇區”的時候,就是指單次寫入的最少字節總數。
SQLite 3.3.14以前的版本,我們假定任何情況下,一個扇區是512字節。這是一個編譯時設定的值,而且從沒針對更大數進行測試過。當磁盤驅動器內部使用的是以512字節為單位的扇區時,512字節的假定顯得非常合理。然而,現在的磁盤都已經發展到4k每扇區了。同樣, flash memory 的扇區大小通常都大于512字節。因此,從3.3.14版本開始,SQLite有一個函數去獲取文件系統的扇區真實大小。在當前的實現中(3.5.0),這個函數仍然簡單的返回512—因為在win32及unix環境下,沒有標準方法去取得扇區的真實大小。但這個方法在人們需要針對他們應用進行調整的時候是非常有意義的。
SQLite并不假定扇區寫操作是原子的。然而,我們假定扇區寫操作是線性的。所謂“線性”是指,當開始扇區寫操作時,硬件從前一個扇區的結束點開始,然后一字節一字節的寫入,直到此扇區的結束點。這個寫操作可能是從尾向頭寫,也可能是從頭向尾寫。如果在一個扇區寫入操作時發生掉電故障,這個扇區可能會一部分已經修改完成,還有一部分還沒來得及進行修改。SQLite的關鍵設定是這樣的:如果一個扇區的任何部分發生修改,那么不是它開始的部分發了變化,就是它結束部分發生了變化。所以硬件從來都不會從一個扇區的中間部分開始寫入。我們不知道這個假定是否總是真實的,但無論如何,看起來還是蠻合理的。
上段中,SQLite并沒有假定扇區寫操作是原子的。在SQLite3.5.0版本中,新增了一個VFS(虛擬文件系統)接口。SQLite通過VFS與實際的文件系統進行交互。SQLite已經為windows及unix編寫了一個缺省的VFS實現。并且可以讓用戶在運行時實現一個自定義的VFS實現。VFS接口有一個方法叫:xDeviceCharacteristics.此方法讀取實際的文件系統各種特性。xDeviceCharacteristics方法可以指明扇區寫操作是原子的,如果確實指定扇區寫是原子的,SQLite是不會放過這等好處的。但在windows及unix中,缺省xDeviceCharacteristics的實現并沒有指明扇區寫是原子的,所以這些優化通常會忽略掉了。
SQLite假定操作系統會對寫進行緩沖,因此寫入請求返回時,有可能數據還沒有真實的寫入到存儲中。SQLite 同時還假定這種寫操作會被操作系統記錄。因此, SQLite需要在關鍵點做"flush" 或 "fsync" 函數調用。SQLite假定flush或fsync在數據沒有真實的寫入到硬盤之前是不會返回的。不幸的是,我們知道在一些windows及unix版本中,缺少flush或fsync的真正實現。這使得SQLite在寫入一個提交發生掉電故障后數據文件得到損壞。然而,這不要緊,SQLite能夠做一些測試或補救。SQLite假定操作系統會是廣告中那樣漂亮運行。如果這些都不是問題,那么剩下的只期望你家的電源不要間歇性的休息。
SQLite假定文件增長方式是指新分配的文件空間,剛分配的時候是隨機內容,后來才被填入實際的數據。換而言之,文件先變大,然后再填充其內容。這是一悲觀假定,因而SQLite不得不做一些額外的操作來防止因斷電發生的破壞數據文件—發生在文件大小已經增大,而文件內容還沒完全填入之間的掉電。VFS的xDeviceCharacteristics可以指明文件系統是否總是先寫入數據然后才更變文件大小的。(這就是那個:SQLITE_IOCAP_SAFE_APPEND屬性,如果你想查看代碼的話)
當xDeviceCharacteristics方法指示了文件內容先寫入然后才改變文件大小的話,SQLite會減少一些相當的數據保護及錯誤處理過程,這將大大減少一個提交磁盤IO操作。然而在當前的版本,windows及unix的VFS實現并沒有這樣假定。
SQLite假定文件刪除從用戶進程角度來講是原子的。也就說當SQLite要求刪除一個文件,也在這刪除的過程中間,斷電了,一旦電源恢復,只有下列二種情況之一分發生:文件仍然存在,所有內容都沒有發生變化;或者文件已經被刪除掉了。如果電源恢復之后,文件只發生了部分刪除,或者部分內容發生了變化或清除,或者文件只是清空,那么數據庫還有用才怪呢。
SQLite假定發現或修改由于宇宙射線,熱噪聲,量子波動,設備驅動bug等等其他可能所引發的錯誤,都由操作系統或硬件來完成。SQLite并不為此類問題增加任何數據冗余處理。SQLite假定在寫入之后去讀取所獲得的數據,是與寫入的數據完全一致的!
3.0 單個文件提交
我們著手觀察SQLite在針對一個數據庫文件時,為保證一個原子提交所采取的步驟。關于在多個數據庫文件之間為防止電源故障損壞數據庫及保證提交的原子性所采用的技術及具體的文件格式在下一節進行討論。
3.1 初始狀態
當一個數據庫第一次打開時計算機的狀態示意圖如右圖所示。圖中最右邊("Disk”標注)表示保存在存儲設備中的內容。每個方框代表一個扇區。藍色的塊表示這個扇區保存了原始資料。圖中中間區域是操作系統的磁盤緩沖區。在我們的案例開始的時候,這些緩存是還沒有被使用—因此這些方框是空白的。圖中左邊區域顯示SQLite用戶進程的內存。因為這個數據庫聯接剛剛打開,所以還沒有任何數據記錄被讀入,所以這些內存也是空的。
3.4 申請一個Reserved Lock
在修改一個數據庫之前,SQLite首先得擁有一個針對數據庫文件的“Reserved”鎖。Reserved鎖類似于共享鎖,它們都允許其他數據庫連接讀取信息。單個Reserved
鎖能夠與其他進程的多個共享鎖一起協作。然后一個數據庫文件同時只能存在一個Reserved 。因此只能有一個進程在某一時刻嘗試去寫一個數據庫文件。
Reserved 鎖的存在是宣告一個進程將打算去更新數據庫文件,但還沒有開始。因為還沒有開始修改,因此其他進程可以讀取數據,但不應該去嘗試修改該數據庫。
3.5 生成一個回滾日志文件
在修改數據庫文件之前,SQLite會生成一個單獨的回滾日志文件,并在其中寫進將被修改的頁的原始數據。回滾日志文件意味它將包含了所有可以將數據庫文件恢復到原始狀態的數據。
回滾日志文件有一個小的頭部(圖中綠色標記部分)記錄了數據庫文件的原始大小。因此,如果一旦即使數據庫文件變大,我們還是會知道它原始大小。數據庫文件中被修改的頁碼及他們的內容都被寫進了回滾日志文件中。
當一個新文件剛被創建,大部分的桌面操作系統(windows,linux,macOSX)實際并不會馬上寫入數據到硬盤。此文件還只是存在于操作系統磁盤緩存中。這個文件還不會立即寫到存儲設備中,一般都會有一些延遲,或者到操作系統相當空閑的時候。用戶的對于文件生成感覺是要遠遠快(先)于其真實的發生磁盤I/O操作。右圖中我們用圖例說明了這一點,當新的回滾日志文件創建之后,它還只是出現在操作系統磁盤緩存之中,還沒真實在寫入到硬盤之上。
3.8 獲得一個獨享(Exclusive)鎖
在修改數據庫文件本身之前,我們必須取得一個針對此數據庫文件的獨享鎖。取得此鎖的過程是分二步走的。首先SQLite取得一個“臨界”(Reserved)鎖,然后將此鎖提升成一個獨享鎖。
一個臨界鎖允許其他所有已經取得共享鎖的進程從數據庫文件中繼續讀取數據。但是它會阻止新的共享鎖的生成。也就說,臨界鎖將會防止因大量連續的讀操作而無法獲得寫入的機會。這些讀取者可能有一打,也可能上百,甚至于上千。任何一個讀取者在開始讀取之前都要申請一個共享鎖,然后開始讀取它需要的數據,然后釋放共享鎖。然而存在這樣一種可能:如果有太多的進程來讀取同一個數據文件,在老的進程釋放它的共享鎖之前總是會有新的進程申請共享鎖,因此不會存在某一時刻這個數據庫文件上沒有共享鎖的存在,也因此寫入者不會擁有取得一個獨享鎖的機會。臨界鎖的概念可以使現有的讀取者完成他們的讀取,同時阻止新的讀取者讀取,最后所有的讀取者都讀完之后,這個臨界鎖就可以被提升為獨享鎖了。
3.10 刷新變更到存儲
一個附加的flush操作是必要的,這樣才可以保證針對此文件的變化真正的寫入到永久存儲器中。這也是一個重要的步驟,將可以保證數據在掉電之后也將是完整無損的。然而,因為寫入到磁盤所固有的慢,這個步驟同上面3.7節將日志文件flush到磁盤中一樣,占據了SQLIite事務提交操作的絕大部分時間。
3.11 刪除回滾日志文件
當數據變更已經安全的寫入到硬盤之后,回滾日志文件就沒有必要再存在了,因此立即刪除之。如果在刪除之前又掉電了或者系統崩潰了,恢復進程(在后面將會提到)會將日志文件的內容寫回到數據庫文件中—即使這個數據庫沒有發生變化。如果刪除之后系統崩潰或者又停電了,看起來好象所有變化都已經寫入到磁盤。因此,SQLite判斷數據庫文件是否完成了變更是依賴于回滾日志文件是否存在。
刪除一個文件實際上不是一個原子操作,但從用戶進程的角度來看,它是一個原子操作。一個進程總是可以向操作系統詢問某個文件存在否,而它得到的答案只有“YES”和“NO”二種。在一個事務提交的中間,系統崩潰或又停了,之后,SQLite會向操作系統咨詢回滾日志文件存在與否,如果存在,則這個事務是沒有完成,被中斷了,需要對數據庫文件進行回滾。如果日志文件不存在,意味著事務已經提交ok了。.
事務存在的可能性依賴于是否有回滾日志文件。刪除一個文件對于一個用戶進程來說是原子性的。因此,整個事務看起來也是一個原子操作。.
4.4 回滾沒有完成的變更
一旦進程獲得一個獨享鎖,它就被允許更新數據庫文件。然后從日志文件中讀取原始的內容,并寫回到數據庫文件中。是否還記得在這個被中止的事務的開始的時候,數據庫文件原始大小已經被寫進了日志文件的頭部。SQLite使用這些信息來截斷數據庫文件,讓文件恢復到原始大小—如果這個沒有完成的事務使得數據庫變大了。最后,數據庫文件大小及內容肯定與這個被中斷事務開始之前是一樣的了。
6.0原子操作的一些實現細節
3.0節大致描述了SQLite中原子提交是如何工作的。但它略過了許多重要的細節。下面的這些部分將嘗試補充說明這些地方。
6.1 總是記錄整個扇區
當數據庫文件的原始代碼被寫入到日志文件時(參見3.5節),SQLite總是寫入完整的扇區,即使數據文件頁大小是小于一個扇區。由于歷史上的原因,SQLite的扇區大小原先是固定為512字節,此外由于最小的頁大小是512字節,因此這從來都不是一個問題。自SQLite3.3.14版本以來,SQLite便有可能使用最小扇區大于512字節的海量存儲設備。所以,自從3.3.14版本開始,只要一個扇區中的任何一頁被寫進到回滾日志文件中,那么同一扇區中的所有節都會寫入到日志文件中去。
將扇區中的所有頁都寫入日志文件中去是很重要的,它將可以防止因為在寫一個扇區時發生掉電故障而導致數據庫損壞。假充頁1,2,3,4都是保存扇區1中,頁2被修改了。為了將這種變更寫回到頁2中,實際的硬件設備將也會同時重寫頁1,3及4的內容—這是因為硬件必須以扇區為單元作寫操作。如果一個寫操作正在進行的時候,由于電源的原因,發生了中斷,這樣,頁1,3,4中會有1頁或者多頁數據是不完整,不正確的。因此為了防止這種損壞,數據庫文件的同一扇區中的所有頁都必須寫入到日志文件中去。
6.2 寫日志文件時垃圾的處理
當向一個日志文件追加數據時,SQLite總是悲觀的假定文件會首先變大,變大的部分會填之一些無效的垃圾數據,在此之后正確的數據才會取代這些垃圾。換而言之,SQLite假定文件先改變大小,然后內容才會寫進來。如果在文件大小增大之后,在內容還沒有寫完之前發生掉電故障,那么這些日志文件就會留下一些垃圾數據在其中。下次當電源恢復,另一個SQLite進程就會看到這些保存了垃圾數據的日志文件,并同時會把這些垃圾數據回滾到數據庫文件中去,然后整個數據庫就玩完了。
SQLite采用了兩種預防措施。第一種,SQLite會在日志文件的頭部記錄下該日志文件中包含的頁的數量。這個數量初始值是0。所以在嘗試回滾一個不完整(或不正確)的回滾日志文件時,處理回滾的進程會看到該日志只包含0個頁面,那么它就會不對數據庫作任何改變。提交之后前,日志文件會被flush到硬盤中以確保所有的內容都同步到硬盤,同時沒有任何垃圾內容留在其中,然后日志文件頭部的頁總數值才會置成真實有效的數據(原先數值是0)。日志文件的頭部總是存放在區別于所有的頁數據之外的獨立扇區中,以此來保證它可被單獨修改并且flush,即使發生掉電也不會危及數據頁。請注意,日志文件會被flush兩次:第一次寫頁數據,第二次是將頁面數量寫入到文件頭部中。
前面的章節描述了當synchronouspragma設置成”full”發生的事情。
PRAGMAsynchronous=FULL;
缺省的synchronous設置是“full”,所以上面描述是通常會發生的情形。然而,如果synchronous設置成“normal”,那SQLite只會flush日志文件一次,就是在頁面數量寫入之后。這將意味著會有數據損壞的風險。因為有可能被修改的頁面數量(非0)比所有的頁數據更早一步寫入到硬盤中。也數據的寫入請求雖然會先被發起,但SQLite假定底層的文件系統可能會對寫入請求重新排序,所以有可能頁面數量會先寫到磁盤中,即使是它的寫請求是在最后。所以作為第二個預防手段,SQLite會為日志文件中的每一頁數據使用一個32位的校驗和,當回滾數據時(節4.4),這些值用來驗證這些頁是否有效。一旦發現有不正確的校驗和時,那么就會放棄回滾。要注意的是,校驗值并不確保頁面數據百分百的正確,有極小的可能會出現即便數據錯誤校驗和也是正確的。但使用校驗和還是能使出錯的可能性降到少之又少。
注意,如果synchronous設置成full時校驗和不是必須的。只有當synchronous設置成normal時,我們才使用這些校驗和。不過,這些校驗和是沒有壞處的,所以無論synchronous設是什么,它們都包括在日志文件里了。
6.3 提交前緩存溢出
節3.0描述的提交過程都假設所有的數據庫變更在提交前都適合用戶的內存大小。這是通常情況。但有時一個非常大的修改在事務提交前會超出用戶空間的內存緩存大小。在這種情況下,事務完成之前,緩存必須先將數據先寫入到數據庫中。
在緩存溢出開始時,這個數據庫聯接的狀態如3.6節提到的。原始的頁數據已經被寫入到回滾日志文件中了,修改的部分還保存在用戶內存中。要處理這種緩存溢出,SQLite會執行3.7節到3.9節的內容。換言之,回滾日志被flush到硬盤,獨享鎖已經申請到,修改已經被寫入到數據庫了。但剩余的步驟會推遲到這個事務被真正提交。新的日志文件頭會追加到回滾日志文件尾部(處于它自己單獨的扇區中),獨享鎖仍然保留,但其他處理則回到3.6節.當這個事務提交時,或者另外的緩存溢出發生, 3.7節及3.9節會再次發生(3.8節在第二次或以后過程中被省略掉,因為獨享鎖已經拿到了)。
一次緩存溢會使數據庫的臨界鎖提升為獨享鎖。這將減少并發。一次緩存溢出也會導致額外的硬盤flush(fsync)操作,這些操作比較慢,因此緩存溢出會嚴重降低性能。因此,應該盡可能的避免緩存溢出。
7.0 優化
性能分析顯示,在大部分的操作系統和環境下面,SQLite主要耗時是在磁盤IO上面。如果我們能夠減少磁盤IO數量就會顯著的提高SQLite的性能。本節將描述SQLite在不影響提交原子性的前提下,為減少磁盤IO數量所采用的一些技術。
7.1 在事務間保存緩存
事務提交處理過程中,節3.12指出一旦共享鎖被釋放,用戶空間所有的緩存的數據庫內容鏡像都必須得拋棄。這是因為如果沒有一個共享鎖,其他進程就可以隨便修改數據庫的內容,所以任何一塊數據庫數據在用戶空間的緩存都可能會過期無效。因此,每一個新的事務會嘗試去重新讀取它以前讀取過的數據。這并不像聽起來這樣糟糕,因為第一次讀取過的數據還可能存在于操作系統的磁盤緩存中。所以這個讀實際上只是一次數據從內核空間到用戶空間的復制。但盡管這樣,這還是需要占用cpu時間的。
自從SQLite3.3.14開始,新增了一個機制用來減少一些不必要的數據重復讀取操作。最新的SQLite中,用戶空間的頁面緩存在用戶鎖釋放之后仍然保留。之后,當要開始一個新事務,在取得一個共享鎖之后,SQLite會嘗試檢查在此期間是否有進程對數據進行了修改。如果在鎖釋放這段時間,數據庫發生過任何的變化,那么用戶空間的緩存就會被釋放。但通常情況下,數據文件是沒有被修改過的,因此用戶空間的緩存因而得到保留,一些不必要的讀取操作從而得到了減免。
為了判斷數據庫文件是否被修改過,SQLite使用了一個計數器,存于數據庫文件頭部(處于字節24~27),每針對數據庫做一次修改,就會對此值進行一回增長。SQLite會在釋放一個鎖之前記錄一份這個值的。當下回取得鎖之后,就會去與原先保存的值進行比較。如果值不一致,則必須清除這些緩存,反之緩存可以重新使用。
7.2 獨享訪問模式
SQLite從3.3.14版本之后增加一個“獨享訪問模式”概念。當處于獨享訪問模式時,SQLite會在一個事務完成之后仍然保留獨享鎖。這將阻止其他進程訪問這個數據庫;由于大部分的開發都只有一個進程訪問數據庫,所以大部分情況下這不是一個嚴重的問題。獨享訪問模式的好處可以在三個方面減少磁盤IO數量:
1) 不再需要在每個事務完成之后修改文件頭部的變更計數器。這可以為回滾日志及數據庫文件減少一次頁寫入。
2) 沒有其他進程會修改數據庫,所以不必在一個事務開始的時候去檢查變更計數器或者清除掉用戶空間的緩存。
3) 當一個事務完成之后,可以采用將日志文件頭清零的方式,而不必去刪除這個日志文件。這樣就避免了修改日志文件的目錄項,也不必釋放日志文件對應的磁盤扇區。而且,下一個事務可以重寫(overwrite)已有日志文件的內容,而不是在新的文件后追加新內容。在大多數的操作系統中,重寫操作要遠快于追加操作。
上述的第三點優化,將日志文件頭清空而不是刪除日志文件,不再依賴于一直持有一個獨享鎖。在理論上,我們可以在任何時刻做這項優化,并不是只有在獨享訪問模式時。This optimization can be set independently of exclusive lock modeusing the journal_mode
pragma asdescribed in section 7.6 below.
7.3 不必將空閑頁寫進日志
SQLite數據庫的信息被刪除之后,這些被刪除的數據所使用的頁會被加入到空頁鏈表之中。后來的插入操作會盡量先使用空頁鏈表中的頁。
一些空白頁包含緊要數據:特別是其他空百頁的位置。但是大多數的空白頁并不包含有用信息。這類頁被稱之為“葉子”頁。我們可以隨意修改這些葉子頁的內容而不會影響數據庫。
因為葉子頁的內容是不重要的,SQLite避免保存這些葉子頁的內容到回滾日志文件中去(3.5節)。如果一個葉子頁的內容被修改了,那么在事務恢復過程中這些針對葉子頁的修改并不會回滾。這不會對數據庫產生傷害。同樣的,新的空頁鏈表的內容也從不會在節3.9中寫回到到數據庫,也不會在節3.3從數據庫讀入。當針對數據庫文件的變化包含有空白頁時,這種優化可以大量的減少磁盤io操作總數
7.4 單頁更新及扇區原子寫
從3.5.0開始,新的VFS接口包含了一個新的方法:xDeviceCharacteristics ,它能夠讀取實際的文件系統可能有的特性。xDeviceCharacteristics會報告是否文件系統能夠支持扇區寫原子操作。
回想前面,在一般情況下SQLite假定扇區寫是線性的,但是非原子的。線性寫從另一個扇區結束點開始一字節一字節進行修改,直到扇區的結束點。如果在寫一個扇區時,線性寫會將修改一個扇區的一部分,而另一部分是沒有變動的。在一個扇區原子寫的情況下,要么整個扇區被重寫了,要么扇區沒有發生變化。
我們相信大部分現代磁盤驅動器實現了原子寫操作。當停電發生時,磁盤驅動器可以利用電容中的電能,同時(或者)利用盤片旋轉的角動量來完成正在進行中的任何操作。然而,在系統寫調用與磁盤電子器材之間,存在有太多的層次。因此在unix及win32上面的VFS實現比較安全的選擇是,我們假定扇區寫操作是非原子性的。On the otherhand, device manufactures with more control over their filesystems might wantto
consider enabling the atomic write property of xDeviceCharacteristics iftheir hardware really does do atomic writes.
當一個扇區寫是原子性的,并且扇區大小與頁大小是相同,并且一次數據庫的變化只是某一個單獨的頁發生變化時,SQLite會跳過整個日志記錄過程,直接簡單地將被修改過的數據寫回到數據庫文件。數據庫首頁中的變更計數器將會被獨立進行修改—因為不會對數據庫產生任何影響—即使在計數器更新以前發生停電。.
7.5 FilesystemsWith Safe Append Semantics
SQLite3.5.0中介紹的另一個優化是利用實際磁盤的“安全追加”行為。回想上面,SQLite假定為一個文件追加數據時(特別是針對回滾日志文件),會先增大文件的大小,之后才會把數據內容寫入。所以在文件的大小已經變化,而內容還沒有寫完的情況下發生掉電,那么文件新增部分將會有一些無效的垃圾數據。VFS的xDeviceCharacteristics可以用來指示文件系統是否實現了“安全追加”語義。這意味著在文件大小變大之前會先寫入文件內容。這就防止當系統崩潰或掉電后,垃圾數據出現在回滾日志文件中
當文件系統有安全追加特性時,SQLite總是保存一個特別的值:-1來標明日志文件中頁總數。頁面數量為-1告訴任何嘗試進行回滾操作程序頁面數量需要從日志文件大小計算得來。同時,這-1值會從不進行修改。所以,在一個提交過程中,我們節省一個flsuh操作及日志文件首頁的扇區寫入操作。此外,當發生緩存溢出時,也不必要在日志文件后面增加一個新的日志頭。我們能夠簡單的在一個現有的日志文件中添加一些新的頁。
7.6持續的回滾日志
在許多系統中刪除文件都是一個昂貴的操作。因此作為一個優化方案,SQLite可以通過配置避免3.11節中涉及到的刪除操作。在事務提交時,通過將日志文件的文件頭長度截為0或是用0重寫文件頭內容的方法來代替刪除日志文件。將長度截為0的做法節省了必須要對文件的所在目錄做的修改(因為文件依舊存在于這個目錄中)。重寫文件頭的方案還有另外一個好處,不必更新文件(許多系統中的i節點)的長度,而且不需要處理新釋放的磁盤扇區。更進一步講,下一個事務的日志文件是通過重寫已有內容而產生,而不是在文件末尾追加新內容,并且重寫操作通常是要比追加操作更快的。
SQLite可以通過將日志模式設置為“PERSIST”使提交事務時使用用0重寫日志文件頭的方式來代替刪除日志文件。例如:
PRAGMA journal_mode=PERSIST;
在很多系統中,使用持續的日志模式會帶來顯著的性能提升。當然,缺點就是事務提交很久以后,日志文件還會留在磁盤上,占用磁盤空間,導致目錄雜亂。刪除持續日志文件唯一安全的方法就是提交事務時將日志模式設置為DELETE:
PRAGMA journal_mode=DELETE;
BEGIN EXCLUSIVE;
COMMIT;
注意:因為日志文件可能依然在用(hot),如果使用其它途徑刪除持續日志文件會導致對應的數據庫文件損壞。
從SQLite 3.6.4開始支持 TRUNCATE 日志模式:
PRAGMA journal_mode=TRUNCATE;
截斷(truncate)日志模式中,事務提交時將日志文件長度置為0,而不是DELETE模式中的刪除文件或是PERSIST模式中的清零文件頭。 TRUNCATE模式也有PERSIST模式中不需要更新日志文件和數據庫所在目錄的好處。因此,截斷一個文件通常比刪除它要快。TRUNCATE還有一個好處就是它后面不跟系統調用(比如:fsync())來將更新同步回磁盤,當然如果做了會更安全。但是在很多現代的文件系統中,截斷操作是原子的同步操作,并且我們認為在遇到斷電情況時,截斷操作也是安全的。如果你不確定截斷操作在你的文件系統上的同步性和原子性,并且斷電或宕機時的數據庫安全對你很重要,那你應該考慮使用其他的日志模式。
在具有同步文件系統的嵌入式操作系統中,TRUNCATE會導致比PERSIST較慢的行為。提交操作的速度是相同的,但是TRUNCATE操作之后的事務會慢一些,因為重寫已存在的內容比在文件尾追加新內容要快。TRUCATE之后新的日志文件總是使用追加操作,而PERSIST則是使用重寫操作。
8.0 原子提交行為測試
SQLite的開發者對SQLite在面對電源故障及系統崩潰時所擁有健壯性具有足夠的自信。因為自動化的測試過程做了大量的面對模擬的電源故障的SQLite恢復能力測試。我們稱之為“崩潰測試”。
SQLite的崩潰測試是使用一個修改過的VFS,它能夠模擬種種發生掉電或系統崩潰時文件系統發生的損壞。崩潰測試用的VFS能夠模擬未完成的扇區寫操作,未完成的寫操作造成的頁面垃圾,還有無序寫操作,一個測試場景中各種種各樣的變化。崩潰測試不停地執行事務,讓模擬的掉電或系統崩潰發生在不同的各種時刻,造成不同的數據損壞。在模擬的事件之后,任何一次測試重新打開數據庫之后,會檢測事務是否完成或者沒有完成,數據庫狀態是否正常。
SQLite的這些崩潰測試發現恢復機制的大量細微的BUG(現在都已經修復了)。其中一些BUG是非常模糊的,如果只是單單觀察、分析代碼所不能發現的。通通過這試驗,SQLite的開發者感覺很自信,因為其他的數據庫沒有采用類似的崩潰測試,很可能他們都包含一些沒有被檢測出的bug,在一次掉電或者系統崩潰之后會導致數據庫損壞。
9.0 會導致完蛋的事情
SQLite的原子提交機制已經被證明是健壯的。但它也可能被一些不完整的操作系統實現所陷害。本節描述一些會在掉電或系統崩潰下會導致SQLite數據損壞的情形
9.1 缺乏文件鎖實現
SQLite通過文件系統的鎖來實現在同一時刻只有一個進程及一個數據庫聯接能夠修改數據庫。文件鎖機制由VFS層實現,不同的操作系統具有不同的實現方式。SQLite依賴于這種實現的正確性。如果在某種情況下,二個或更多進程能夠在同一時間寫同一個數據庫文件,這將會沒有什么好果子吃的。
我們已經接收到報告說windows的網絡文件系統及NFS的鎖存在一些微妙的缺陷。我們不能驗證這些報告。但是因為網絡文件系統本身實現鎖很困難,所以我們沒有理由懷疑這些報告。首先,既然性能不足,建議你不要在網絡文件系統中使用SQLite。但是如果你不得不使用一個網絡文件來保存SQLite的數據文件,那們考慮采用其他的鎖機制來防止本身的文件鎖機制出錯時發生多個進程同時寫一個數據文件的現象。
蘋果MacOSX預裝的SQLite版本已經擴展擁有一種可供選擇的鎖策略可以工作在蘋果支持的所有網絡文件系統上。這些蘋果使用的擴展在多個進程在同時訪問數據庫文件時工作得很好。不幸的是,這些鎖機制并不互相排斥,如果一個進程使用AFP鎖去訪問文件,而另一個進程(或許是另一臺機器)使用dot-file鎖去訪問這個文件,那么這二個進程可能發生沖突,因為AFP鎖并不排斥dot-file鎖,反之亦然。
9.2 不完整的磁盤刷新
SQLite 在unix使fsysnc,在win32下面使用FlushFileBuffers,用來將文件內容同步到磁盤中(節3.7及節3.10)。不幸的是,我們也收到報告,在許多平臺上,這二者都沒有象廣告中宣稱的那樣工作。我們聽說FlushFileBuffersc在一些windows版本中,可以通過修改注冊表,能夠完全禁止其工作。我們也被告之,Linux的一些早先版本,他們的一些文件系統中的fsync完全是一個空操作。即使是FlushFileBuffers及fsync被告之可以工作的系統中,IDE硬盤經常會撒謊說數據已經寫入到盤片中,其實還只是存在狀態可變的磁盤控制器緩存中。
在Mac你可設置下面項:
PRAGMA fullfsync=ON;
在Mac上設置fullfsync能夠保證數據通過flush會真實的寫入到盤片中。但fullfsync會導致磁盤控制進行重設。這并不是一般意義上的慢,它還會導致其他磁盤IO降速,所以此項配置并不推薦。
9.3 文件部分地刪除
SQLite假設從用戶進程角度來看是一個原子操作。當刪除過程中發生掉電,當電源恢復之后,SQLite希望看到文件要么完整的存在,要么根本找不到了。如果操作系統不能做到這一點,那事務就可能不是原子性的了。
9.4 寫入到文件中的垃圾
SQLite的數據文件是一種普通的磁盤文件,可以由普通用戶進行讀寫。一些流氓進程可能會打開一個SQLite文件,并在其中寫入一些混亂的數據。混亂的數據也可能由于操作系統的BUG而寫入到一個SQLite的數據文件中。對于這些情況,SQLite無能為力。
9.5 刪除掉或更名了“hot”日志文件
如果掉電或系統崩潰導致留下了一個”hot”日志文件在磁盤上。實際上,原來的數據文件再加上留下來的“hot“日志文件, 是SQLite下回打開時發生回滾使用的,這可以恢復SQLite數據的正常狀態(節4.2)。SQLite會在數據庫所在同一目錄下用打開的文件名來尋找可能存在的”hot”日志文件。如果數據文件或者日志文件被移動或者改名,或者刪除掉了,那么這些日志文件將不會被回滾,數據庫也就可能損壞,無法使用了。
我們常懷疑SQLite發生的恢復失敗的例子是這樣的:停電了,之后電又恢復了。一個好心的用戶或者系統管理管理員開始查看磁盤損壞。他們看到名為"important.data"數據庫文件,或許類似的文件。但由于停電,這里也同樣有一個日志文件名為"important.data-journal".這個用戶刪除了這個“hot”日志文件,認為他是清理系統。那于這種情況,除了進行用戶培訓,沒有其他辦法。
如果有多個聯接(硬或者符號聯接)指向一個數據文件,這個日志文件會以被打開的聯接文件名相關來創建的。如果系統崩潰之后,數據庫以一個新的聯接重新打開,這個“hot”日志文件就不會被找到,數據也不會發生回滾。
有時,電源問題會導致文件系統出現毛病,如最新修改的文件名被丟失了,并會轉移至類似于"/lost+found"這樣的目錄中。當這種情況發生的時候,這個hot日志文件就不會被找到,同樣恢復也不會發生。SQLite在同步一個日志文件時通過打開并同步日志文件所在目錄來嘗試阻止這類事件發生。然后,轉移文件到"/lost+found"可能會由不相關的其他進程在相同的目錄中產生與主數據庫文件名相同的不相關文件。既然這都是SQLite所無法控制,所以SQLite沒有什么好辦法。如果你運行在一種易導致名稱空間沖突的文件系統上,那么你最好把每一個SQLite的數據文件放在你私有的子目錄中。
10.0 總結及未來的路
即使到了現在,還是有人發現了一些關于原子提交機制失敗模式,開發者不得不為此做一些補丁。這樣的事情發生得越來越少了,失敗模型也變得越來越模糊了。但如果就認為SQLite的原子提交邏輯是沒有任何bug,那是相當愚昧的。開發者承諾將盡可能快的修復被發現的bug。
開發者同時在考慮新的優化提交機制的辦法。當前的linux,macOSX,win32的VFS實現使用這些系統之上的一些悲觀設定。或許在與一些了解這些系統如何工作的專家交流之后,我們或許可能放松一些這些系統上的設定,使其跑得更快些。特別的,我們懷疑的大部分現代文件系統現在已經展現安全追加特性,或許他們都已經支持了扇區的原子操作。但是除非這些得到明確,SQLite仍將采用更安全、保守的方法,作最壞的打算。
關于SQLite原子提交的原理是什么就分享到這里了,希望以上內容可以對大家有一定的幫助,可以學到更多知識。如果覺得文章不錯,可以把它分享出去讓更多的人看到。
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。