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