說到事務一定會提到ACID,所謂事務的原子性,一致性,隔離性和持久性。對於一個數據庫而言,通常通過並發控制和故障恢復手段來保證事務在正常和異常情況下的ACID特性。sqlite也不例外,雖然簡單,依然有自己的並發控制和故障恢復機制。Sqlite學習筆記(五)&&SQLite封鎖機制 已經講了一些鎖機制的原理,本文將會詳細介紹一個事務從開始,到執行,最后到提交所經歷的過程,其中會穿插講一些sqlite中鎖管理,緩存管理和日志管理的機制,同時會介紹在異常情況下(軟硬件故障,比如程序異常crash,主機掉電等),sqlite如何將數據庫恢復到事務之前的狀態。本文大量參考了sqlite的官方文檔,結合自己的理解,希望能把這個過程說清楚。
1.事務提交
1.1 開啟一個事務
在向數據庫文件寫數據前,sqlite首先需要訪問sqlite_master表獲取元數據信息,用來對SQL語句進行語義分析,判斷語句的合法性。從數據庫讀數據第一步,是對數據庫文件上一個Shared Lock。Shared Lock允許多個事務同時讀一個數據庫文件,但是Shared Lock會阻止寫事務向數據庫文件寫入數據。
1.2 讀數據
獲取Shared Lock后,我們可以從數據庫文件中讀取數據了。我們假設緩存中沒有我們的page,因此需要通過讀文件讀取我們需要的page。這里說明下,sqlite的數據庫文件實質是有一個個大小相同的page組成,默認情況下,一個page大小為1024B。通常情況下,我們需要讀取若干個page,並把這些page緩存在應用本地的cache中,這樣下次訪問就不需要再次從文件中讀取。這里我們假設需要讀取3個page,用綠色塊表示。
1.3 獲取Reserved Lock
在向數據庫寫數據之前,Sqlite需要獲取一個Reserved Lock,Reserved Lock與Shared Lock類似,同時允許其他事務讀取數據庫。Reserved Lock與Shared Lock兼容,但與Reserved Lock互斥,即同一個時刻只允許有一個Reserved Lock。持有Reserved Lock表示事務准備要修改數據文件了,由於還沒有真正修改文件,因此允許其他事務繼續進行讀操作,但不允許其他事務進行寫數據庫操作。
1.4 創建日志文件
在sqlite中,有兩種日志技術,影子分頁技術和WAL(write ahead log)技術。影子分頁技術是sqlite默認采用的方式,后面的討論都是基於這種假設。在操作數據文件之前,sqlite首先創建一個日志文件,並將准備要修改的page的內容寫入日志,通過這種方式保留了恢復事務的所有原始信息。無論是數據庫文件,還是日志文件,最基本的操作單位都是page。
1.5 修改數據
前面提到,sqlite修改數據前,先將page讀到cache中,因此修改會首先修改cache中的數據。由於每個連接都有自己獨立的page cache,因此寫事務修改自己page cache中的數據,不會影響其他事務,其他事務依然會讀到原始的page數據,不會導致臟讀。下圖中紅色表示修改塊,從圖中可以看到,只有用戶自身cache的page變成了紅色。
1.6 刷日志文件
修改完成后,首先將日志文件寫入磁盤。這個過程非常重要,只有通過刷盤操作(fsync)將日志持久化,才能在掉電的情況下,通過日志恢復數據頁。同時,這個動作也非常耗時。
1.7 獲取Exclusive Lock
現在我們需要將之前對page cache的修改寫入數據庫文件,達到持久化目的。在這個動作之前,首先需要持有Exclusive Lock,獲取該鎖實際包含兩個步驟,首先持有一個Pending Lock,然后再持有Exclusive Lock。Pending Lock允許持有讀鎖事務繼續進行讀操作,但不允許新的讀事務進來。由於新的讀事務被阻止,則將讀事務數目限制在一定的范圍,而已有的讀事務遲早都會執行完,寫事務最終可以獲取到Exclusive Lock,通過這種方式避免寫事務餓死的情況。
1.8 將修改寫入數據文件並刷盤
一旦持有了Exclusive Lock,則此時sqlite中只有一個事務,沒有其他讀事務去讀文件。因此,這時候向數據文件中寫數據是安全的。為了保證寫入動作真正落入磁盤,還需要進行刷盤動作。與刷日志一樣,將數據文件修改刷盤動作也是為了保證掉電情況下,更新依然可以持久化,同樣這個操作也很耗時。其中紅色塊表示修改塊,此時用戶進程空間,OS buffer,以及DISK都已經修改。
1.9 刪除日志文件
進行這步時,日志文件和數據文件修改都已經固化到磁盤。如果在1.8步之前,發生掉電,由於日志文件已經安全落盤,因此可以將數據庫恢復到事務開始前的狀態。由於數據文件修改已經固化,我們可以將日志文件刪除。通過日志文件的存在與否,判斷我們是需要將事務回滾還是提交。由於刪文件也是一個比較耗時的動作,sqlite對此進行了優化,通過參數選項,可以選擇將日志全部初始化0,或是直接將文件截斷,達到提高性能的目的。
1.10 釋放Exclusive Lock
最后一步是釋放Exclusive Lock,這樣其他事務才有機會讀、寫數據文件。這里有一個問題,每個連接都有自己的page cache,如果page cache中的內容已經被改了,並寫入到了文件中,那么其它事務如何感知,將自己本地的old-page清理,重新從文件中讀?sqlite通過一個計數器來控制,這個計數器存在數據庫文件的第一個page中。每次數據文件修改時,這個值也會同時自增。事務開始時,會讀取計數器,在讀取page 時,會再次檢查計數器是否發生變化,如果發生變化,說明有事務提交,則將本地的cache全部清空,重新從數據庫文件中獲取。
2.事務回滾
正常情況下,通過上述的事務提交流程,就可以保證事務的ACID特性。但是事務在執行過程中發生異常呢,這時候就需要通過事務回滾來將數據庫恢復到事務開始前的狀態。下面假設一種情況,來介紹回滾流程。
2.1 發生故障
假設在1.8之前,寫數據庫文件時,發生了掉電故障。當故障恢復后,情形可能如右圖所示,只有部分頁寫入了磁盤,甚至有一個頁可能只寫入了一部分。由於執行到這個步驟時,日志已經安全落盤,因此可以借助日志進行恢復。
2.2 熱日志
任何一個連接在操作數據庫之前,會首先判斷是否有熱日志存在,因為有熱日志存在意味着可能需要故障恢復。所謂熱日志,是指需要事務提交過程中發生了故障,需要利用日志恢復。
2.3 回滾未完成的操作
在利用日志進行恢復前,首先持有Exclusive Lock,這樣避免多個連接同時進行故障恢復,持有Exclusive Lock后,才可以開始修改數據庫文件。sqlite從日志文件中讀取原始的數據頁,然后將數據頁寫回到數據文件中。由於日志文件頭部記錄了事務開始時數據文件的大小,sqlite利用這個信息來講數據文件進行截斷到原來的大小,保持文件大小恢復到事務開始時的水平。
2.4 刪除日志文件
當所有日志文件中的數據頁都已經拷貝到數據文件中后,進行一次刷盤操作,確保修改持久化,這時候日志文件可以被刪除了。恢復完成后,將Exclusive Lock 降級到Shared Lock。這個過程完成后,數據庫完成恢復。由於整個過程都是sqlite自動完成,用戶完全無感知。對於用戶而言,任何時候使用sqlite操作數據文件都是安全的,即使在發生了異常的情況下。
參考文檔
https://www.sqlite.org/atomiccommit.html