參考鏈接:https://www.sqlite.org/lockingv3.html#rollback
1、sqlite 版本3.0.0 為了降低版本2 帶來的寫飢餓的問題引入了新的鎖和回滾機制。新的機制同時允許多個數據庫的原子事務提交。
2、sqlite 內的pager module 負責sqlite的ACID,同時對磁盤文件一些內容的內存緩存。pager模塊不會去關注b-tree,編碼,索引等。在pager看來,sqlite數據庫是由統一大小的塊組成的單個文件。每個塊我們稱之為''頁’‘,每個頁通常是1024byte大小。這些個頁從數字1開始編排,比如 page 1,page 2 ...。
3、鎖狀態分類
無鎖 : 默認狀態,對數據庫無讀寫。
共享鎖 : 只有讀無寫,同一時刻可以有任何的讀,但是不允許存在寫。
保留鎖 : 計划在未來的某個時刻寫,但目前僅是在讀。同一時刻只會有1個活動的保留鎖,盡管多個共享鎖可以同時和1個保留鎖存在。與意向鎖不同,當存在保留鎖,還可以獲取新的共享鎖。
意向鎖 : 表示想盡可能快的寫入數據庫,正在等待所有當前共享鎖釋放,以使自身能獲取到排他鎖。如果意向鎖激活,將無法再獲得共享鎖,已存在的共享鎖允許繼續。是個中間狀態。
排他鎖 : 寫入鎖。排它鎖持有時不允許有其他鎖存在。為了最大化並發,sqlite 最小化了排它鎖持有時間。
4、回滾日志 文件
在更新之前,數據庫首先會記錄改變之前的數據庫內容到回滾日志文件中.回滾文件和數據庫文件在同一目錄下,並以-journal 結尾。日志文件同時會記錄原始庫的大小以便在回滾時將數據庫撤回原始大小。
attach命令時,操作多個庫將會產生一個聚合的journal文件,叫super-journal。super-journal文件不會包含將要作為回滾變化的頁數據,相反只包含每個庫的日志文件的名稱。每個庫的日志文件同時也會包含該super-journal文件的名稱。如果沒有attached別的庫,就不會有super-journal文件,單是相應庫的日志文件還是會在其存在super-journal名稱的位置留空。
在操作數據庫前,sqlite會先處理日志文件,以保證數據一致性,處理步驟如下:
- 現獲得數據庫共享鎖. 未獲得,失敗,立即返回sqlite_busy.
- 檢查是否有journal文件,如果沒有直接返回,完成。如果有按接下來的步驟執行。
- 獲取意向鎖然后排它鎖,失敗表明有其他線程在處理此過程。此時釋放所有鎖,關閉數據,返回sqlite_busy。
- 讀取journal文件,回滾改變。
- 等待回滾完成,保證完整性。
- 刪除日志文件。
- 刪除super-jounal文件,如果有。
- 釋放排他,意向鎖,保持共享鎖。
5、刪除壞的super-journals文件
如果沒有相應數據庫的journal文件指向super-journal文件,則認為super-journal文件無用。判斷super-journal是壞的,先讀取super-jounal文件獲取包含的相應庫的journal文件名稱,檢查這個名稱的journal文件是否存在,或者這個journal文件的回指是否是當前super-journal文件。如果有異常則可以進行刪除。
6、數據寫過程
先獲取共享鎖,再獲取保留鎖。保留鎖表示在未來的某個時刻就寫入數據。同一時刻只能有1個保留鎖。但是其他讀可以繼續。如果獲取不到保留鎖,表明已被其他人獲取,返回sqlite_busy。
先寫創建的journal文件,文件頭初始化為 數據庫文件大小。同時留空間個super-journal文件名。
先將要寫入的那頁數據備份到journal文件。然后將該頁數據的變更寫入內存而不是磁盤。此時源庫未改變,其他人還是可以讀。
接下來,當內存緩存充滿或者事務提交,准備寫入磁盤。
-
確保jouranl文件已經實實在在寫入
-
獲取意向鎖 然后排他鎖。如果其他人持有共享鎖,寫入必須等待,直到能獲取到排他鎖。
-
寫入所有內存緩存當中持有的頁數據到源庫。
如果寫入到數據庫文件的原因是因為cache已滿,那么寫入進程將不會立刻提交,而是繼續對其它頁進行修改。但是在后續的修改被寫入到數據庫文件之前,回滾日志必須被再一次刷新到磁盤中。還要注意的是,寫入進程獲取的排他鎖必須被一直持有,直到所有的更改被提交為止。這意味着從數據第一次被刷新到磁盤文件開始,直到事務被提交之前,其它的進程不能訪問該數據庫
接下來,寫入准備提交事務,步驟如下:
-
獲取排他鎖,確保所有內存數據變化按照1-3步驟刷入磁盤
-
將所有修改刷入磁盤
-
刪除日志文件
-
釋放排他,意向鎖
如果一個事務涉及多個庫,則寫入更為復雜:
- 確保每個庫都有1個排他鎖和1個有效的日志文件.
- 創建主數據庫日志文件,文件名隨機,同時將每個數據庫的回滾日志文件名寫入主數據庫日志文件,刷入磁盤。
- 將主數據庫日志文件名寫入每個數據庫日志文件,刷入磁盤。
- 將所有數據庫的變化持久化到磁盤。
- 刪除主日志文件,如果在刪除之前出現系統故障,進程在下一次打開該數據庫時仍將基於該HOT日志進行恢復操作。因此只有在成功刪除主日志文件之后,我們才可以認為該事務成功完成
- 刪除每個數據庫的日志文件
- 釋放所有庫的排他鎖,意向鎖
-
寫飢餓的處理
在版本2中,如果多個進程正在從數據庫中讀取數據,也就是說該數據庫始終都有讀操作發生,即在每一時刻該數據庫都持有至少一把共享鎖,這樣將會導致沒有任何進程可以執行寫操作,因為在數據庫持有讀鎖的時候是無法獲取寫鎖的,我們將這種情形稱為“寫飢餓”。
在版本3中,通過使用PENDING鎖則有效的避免了“寫飢餓”情形的發生。當某一進程持有PENDING鎖時,已經存在的讀操作可以繼續進行,直到其正常結束,但是新的讀操作將不會再被SQLite接受,所以在已有的讀操作全部結束后,持有PENDING鎖的進程就可以被激活並試圖進一步獲取排他鎖以完成數據的修改操作。
-
sql級別的事物控制
在缺省情況下,版本 3會將所有的SQL操作置於antocommit模式下,這樣所有針對數據庫的修改操作都會在SQL命令執行結束后被自動提交。在SQLite中,SQL命令"BEGIN TRANSACTION"(其中TRANSACTION關鍵字可選)用於顯式地聲明一個事務,禁用autocommit模式,即其后的SQL語句在執行后都不會自動提交,而是需要等到SQL命令"COMMIT"或"ROLLBACK"被執行時,才考慮提交還是回滾。
注意BEGIN命令並不獲得任何類型的鎖,在BEGIN之后,當執行第一個SELECT語句時才得到一個共享鎖,當執行第一個DML語句(INSERT、UPDATE或DELETE)時才獲得一個保留鎖。至於排它鎖,只有在數據從內存寫入磁盤時開始后,直到事務提交或回滾之前才能持有排它鎖。
SQL命令COMMIT命令並不實際提交更改到磁盤,它只是重新打開autocommit模式。然后,在命令結束時,正式的自動提交邏輯才實際提交更改到磁盤。SQL命令ROLLBACK也是打開autocommit模式,但是它設置一標志,以告訴自動提交邏輯執行回滾,而不是提交。如果另外有進程持有共享鎖,自動提交邏輯提交更改失敗,則autocommit模式會自動關閉。這允許用戶在共享鎖釋放之后重新COMMIT。
如果多個SQL命令在同一個時刻同一個數據庫連接中被執行,autocommit將會被延遲執行,直到最后一個命令完成。比如,如果一個SELECT語句正在被執行,在這個命令執行期間,需要返回所有檢索出來的行記錄,如果此時處理結果集的線程因為業務邏輯的需要被暫時掛起並處於等待狀態,而其它的線程此時或許正在該連接上對該數據庫執行INSERT、UPDATE或DELETE命令,那么所有這些命令作出的數據修改都必須等到SELECT檢索結束后才能被提交。
