SQLite學習手冊(鎖和併發控制)
一、概述:
在SQLite中,鎖和併發控制機制都是由pager_module模組負責處理的,如ACID(Atomic, Consistent, Isolated, and Durable)。在含有資料修改的事務中,該模組將確保或者所有的資料修改全部提交,或者全部回滾。與此同時,該模組還提供了一些磁碟檔案的記憶體Cache功能。
事實上,pager_module模組並不關心資料庫儲存的細節,如B-Tree、編碼方式、索引等,它只是將其視為由統一大小(通常為1024位元組)的資料塊構成的單一檔案,其中每個塊被稱為一個頁(page)。在該模組中頁的起始編號為1,即第一個頁的索引值是1,其後的頁編號以此類推。
二、檔案鎖:
在SQLite的當前版本中,主要提供了以下五種方式的檔案鎖狀態。
1). UNLOCKED:
檔案沒有持有任何鎖,即當前資料庫不存在任何讀或寫的操作。其它的程式可以在該資料庫上執行任意的讀寫操作。此狀態為預設狀態。
2). SHARED:
在此狀態下,該資料庫可以被讀取但是不能被寫入。在同一時刻可以有任意數量的程式在同一個資料庫上持有共享鎖,因此讀操作是併發的。換句話說,只要有一個或多個共享鎖處於活動狀態,就不再允許有資料庫檔案寫入的操作存在。
3). RESERVED:
假如某個程式在將來的某一時刻打算在當前的資料庫中執行寫操作,然而此時只是從資料庫中讀取資料,那麼我們就可以簡單的理解為資料庫檔案此時已經擁有了保留鎖。當保留鎖處於活動狀態時,該資料庫只能有一個或多個共享鎖存在,即同一資料庫的同一時刻只能存在一個保留鎖和多個共享鎖。在Oracle中此類鎖被稱之為預寫鎖,不同的是Oracle中鎖的粒度可以細化到表甚至到行,因此該種鎖在Oracle中對併發的影響程式不像SQLite中這樣大。
4). PENDING:
PENDING鎖的意思是說,某個程式正打算在該資料庫上執行寫操作,然而此時該資料庫中卻存在很多共享鎖(讀操作),那麼該寫操作就必須處於等待狀態,即等待所有共享鎖消失為止,與此同時,新的讀操作將不再被允許,以防止寫鎖飢餓的現象發生。在此等待期間,該資料庫檔案的鎖狀態為PENDING,在等到所有共享鎖消失以後,PENDING鎖狀態的資料庫檔案將在獲取排他鎖之後進入EXCLUSIVE狀態。
5). EXCLUSIVE:
在執行寫操作之前,該程式必須先獲取該資料庫的排他鎖。然而一旦擁有了排他鎖,任何其它鎖型別都不能與之共存。因此,為了最大化併發效率,SQLite將會最小化排他鎖被持有的時間總量。
最後需要說明的是,和其它關係型資料庫相比,如MySQL、Oracle等,SQLite資料庫中所有的資料都儲存在同一檔案中,與此同時,它卻僅僅提供了粗粒度的檔案鎖,因此,SQLite在併發性和伸縮性等方面和其它關係型資料庫還是無法比擬的。由此可見,SQLite有其自身的適用場景,就如在本系列開篇中所說,它和其它關係型資料庫之間的互換性還是非常有限的。
三、回滾日誌:
當一個程式要改變資料庫檔案的時候,它首先將未改變之前的內容記錄到回滾日誌檔案中。如果SQLite中的某一事務正在試圖修改多個資料庫中的資料,那麼此時每一個資料庫都將生成一個屬於自己的回滾日誌檔案,用於分別記錄屬於自己的資料改變,與此同時還要生成一個用於協調多個資料庫操作的主資料庫日誌檔案,在主資料庫日誌檔案中將包含各個資料庫回滾日誌檔案的檔名,在每個回滾日誌檔案中也同樣包含了主資料庫日誌檔案的檔名資訊。然而對於無需主資料庫日誌檔案的回滾日誌檔案,其中也會保留主資料庫日誌檔案的資訊,只是此時該資訊的值為空。
我們可以將回滾日誌視為"HOT"日誌檔案,因為它的存在就是為了恢復資料庫的一致性狀態。當某一程式正在更新資料庫時,應用程式或OS突然崩潰,這樣更新操作就不能順利完成。因此我們可以說"HOT"日誌只有在異常條件下才會生成,如果一切都非常順利的話,該檔案將永遠不會存在。
四、資料寫入:
如果某一程式要想在資料庫上執行寫操作,那麼必須先獲取共享鎖,在共享鎖獲取之後再獲取保留鎖。因為保留鎖則預示著在將來某一時刻該程式將會執行寫操作,所以在同一時刻只有一個程式可以持有一把保留鎖,但是其它程式可以繼續持有共享鎖以完成資料讀取的操作。如果要執行寫操作的程式不能獲取保留鎖,那麼這將說明另一程式已經獲取了保留鎖。在此種情況下,寫操作將失敗,並立即返回SQLITE_BUSY錯誤。在成功獲取保留鎖之後,該寫程式將建立回滾日誌。
在對任何資料作出改變之前,寫程式會將待修改頁中的原有內容先行寫入回滾日誌檔案中,然而,這些資料發生變化的頁起初並不會直接寫入磁碟檔案,而是保留在記憶體中,這樣其它程式就可以繼續讀取該資料庫中的資料了。
或者是因為記憶體中的cache已滿,或者是應用程式已經提交了事務,最終,寫程式將資料更新到資料庫檔案中。然而在此之前,寫程式必須確保沒有其它的程式正在讀取資料庫,同時回滾日誌中的資料確實被物理的寫入到磁碟檔案中,其步驟如下:
1). 確保所有的回滾日誌資料被物理的寫入磁碟檔案,以便在出現系統崩潰時可以將資料庫恢復到一致的狀態。
2). 獲取PENDING鎖,再獲取排他鎖,如果此時其它的程式仍然持有共享鎖,寫入執行緒將不得不被掛起並等待直到那些共享鎖消失之後,才能進而得到排他鎖。
3). 將記憶體中持有的修改頁寫出到原有的磁碟檔案中。
如果寫入到資料庫檔案的原因是因為cache已滿,那麼寫入程式將不會立刻提交,而是繼續對其它頁進行修改。但是在接下來的修改被寫入到資料庫檔案之前,回滾日誌必須被再一次寫到磁碟中。還要注意的是,寫入程式獲取到的排他鎖必須被一直持有,直到所有的改變被提交時為止。這也意味著,從資料第一次被重新整理到磁碟檔案開始,直到事務被提交之前,其它的程式不能訪問該資料庫。
當寫入程式準備提交時,將遵循以下步驟:
4). 獲取排他鎖,同時確保所有記憶體中的變化資料都被寫入到磁碟檔案中。
5). 將所有資料庫檔案的變化資料物理的寫入到磁碟中。
6). 刪除日誌檔案。如果在刪除之前出現系統故障,程式在下一次開啟該資料庫時仍將基於該HOT日誌進行恢復操作。因此只有在成功刪除日誌檔案之後,我們才可以認為該事務成功完成。
7). 從資料庫檔案中刪除所有的排他鎖和PENDING鎖。
一旦PENDING鎖被釋放,其它的程式就可以開始再次讀取資料庫了。
如果一個事務中包含多個資料庫的修改,那麼它的提交邏輯將更為複雜,見如下步驟:
4). 確保每個資料庫檔案都已經持有了排他鎖和一個有效的日誌檔案。
5). 建立主資料庫日誌檔案,同時將每個資料庫的回滾日誌檔案的檔名寫入到該主資料庫日誌檔案中。
6). 再將主資料庫日誌檔案的檔名分別寫入到每個資料庫回滾日誌檔案的指定位置中。
7). 將所有的資料庫變化持久化到資料庫磁碟檔案中。
8). 刪除主日誌檔案,如果在刪除之前出現系統故障,程式在下一次開啟該資料庫時仍將基於該HOT日誌進行恢復操作。因此只有在成功刪除主日誌檔案之後,我們才可以認為該事務成功完成。
9). 刪除每個資料庫各自的日誌檔案。
10).從所有資料庫中刪除掉排他鎖和PENDING鎖。
最後需要說明的是,在SQLite2中,如果多個程式正在從資料庫中讀取資料,也就是說該資料庫始終都有讀操作發生,即在每一時刻該資料庫都持有至少一把共享鎖,這樣將會導致沒有任何程式可以執行寫操作,因為在資料庫持有讀鎖的時候是無法獲取寫鎖的,我們將這種情形稱為"寫飢餓"。在SQLite3中,通過使用PENDING鎖則有效的避免了"寫飢餓"情形的發生。當某一程式持有PENDING鎖時,已經存在的讀操作可以繼續進行,直到其正常結束,但是新的讀操作將不會再被SQLite接受,所以在已有的讀操作全部結束後,持有PENDING鎖的程式就可以被啟用並試圖進一步獲取排他鎖以完成資料的修改操作。
五、SQL級別的事務控制:
SQLite3在實現上確實針對鎖和併發控制做出了一些精巧的變化,特別是對於事務這一SQL語言級別的特徵。在預設情況下,SQLite3會將所有的SQL操作置於antocommit模式下,這樣所有針對資料庫的修改操作都會在SQL命令執行結束後被自動提交。在SQLite中,SQL命令"BEGIN TRANSACTION"用於顯式的宣告一個事務,即其後的SQL語句在執行後都不會自動提交,而是需要等到SQL命令"COMMIT"或"ROLLBACK"被執行時,才考慮提交還是回滾。由此可以推斷出,在BEGIN命令被執行後並沒有立即獲得任何型別的鎖,而是在執行第一個SELECT語句時才得到一個共享鎖,或者是在執行第一個DML語句時才獲得一個保留鎖。至於排它鎖,只有在資料從記憶體寫入磁碟時開始,直到事務提交或回滾之前才能持有排它鎖。
如果多個SQL命令在同一個時刻同一個資料庫連線中被執行,autocommit將會被延遲執行,直到最後一個命令完成。比如,如果一個SELECT語句正在被執行,在這個命令執行期間,需要返回所有檢索出來的行記錄,如果此時處理結果集的執行緒因為業務邏輯的需要被暫時掛起並處於等待狀態,而其它的執行緒此時或許正在該連線上對該資料庫執行INSERT、UPDATE或DELETE命令,那麼所有這些命令作出的資料修改都必須等到SELECT檢索結束後才能被提交。
在SQLite中,鎖和併發控制機制都是由pager_module模組負責處理的,如ACID(Atomic, Consistent, Isolated, and Durable)。在含有資料修改的事務中,該模組將確保或者所有的資料修改全部提交,或者全部回滾。與此同時,該模組還提供了一些磁碟檔案的記憶體Cache功能。
事實上,pager_module模組並不關心資料庫儲存的細節,如B-Tree、編碼方式、索引等,它只是將其視為由統一大小(通常為1024位元組)的資料塊構成的單一檔案,其中每個塊被稱為一個頁(page)。在該模組中頁的起始編號為1,即第一個頁的索引值是1,其後的頁編號以此類推。
二、檔案鎖:
在SQLite的當前版本中,主要提供了以下五種方式的檔案鎖狀態。
1). UNLOCKED:
檔案沒有持有任何鎖,即當前資料庫不存在任何讀或寫的操作。其它的程式可以在該資料庫上執行任意的讀寫操作。此狀態為預設狀態。
2). SHARED:
在此狀態下,該資料庫可以被讀取但是不能被寫入。在同一時刻可以有任意數量的程式在同一個資料庫上持有共享鎖,因此讀操作是併發的。換句話說,只要有一個或多個共享鎖處於活動狀態,就不再允許有資料庫檔案寫入的操作存在。
3). RESERVED:
假如某個程式在將來的某一時刻打算在當前的資料庫中執行寫操作,然而此時只是從資料庫中讀取資料,那麼我們就可以簡單的理解為資料庫檔案此時已經擁有了保留鎖。當保留鎖處於活動狀態時,該資料庫只能有一個或多個共享鎖存在,即同一資料庫的同一時刻只能存在一個保留鎖和多個共享鎖。在Oracle中此類鎖被稱之為預寫鎖,不同的是Oracle中鎖的粒度可以細化到表甚至到行,因此該種鎖在Oracle中對併發的影響程式不像SQLite中這樣大。
4). PENDING:
PENDING鎖的意思是說,某個程式正打算在該資料庫上執行寫操作,然而此時該資料庫中卻存在很多共享鎖(讀操作),那麼該寫操作就必須處於等待狀態,即等待所有共享鎖消失為止,與此同時,新的讀操作將不再被允許,以防止寫鎖飢餓的現象發生。在此等待期間,該資料庫檔案的鎖狀態為PENDING,在等到所有共享鎖消失以後,PENDING鎖狀態的資料庫檔案將在獲取排他鎖之後進入EXCLUSIVE狀態。
5). EXCLUSIVE:
在執行寫操作之前,該程式必須先獲取該資料庫的排他鎖。然而一旦擁有了排他鎖,任何其它鎖型別都不能與之共存。因此,為了最大化併發效率,SQLite將會最小化排他鎖被持有的時間總量。
最後需要說明的是,和其它關係型資料庫相比,如MySQL、Oracle等,SQLite資料庫中所有的資料都儲存在同一檔案中,與此同時,它卻僅僅提供了粗粒度的檔案鎖,因此,SQLite在併發性和伸縮性等方面和其它關係型資料庫還是無法比擬的。由此可見,SQLite有其自身的適用場景,就如在本系列開篇中所說,它和其它關係型資料庫之間的互換性還是非常有限的。
三、回滾日誌:
當一個程式要改變資料庫檔案的時候,它首先將未改變之前的內容記錄到回滾日誌檔案中。如果SQLite中的某一事務正在試圖修改多個資料庫中的資料,那麼此時每一個資料庫都將生成一個屬於自己的回滾日誌檔案,用於分別記錄屬於自己的資料改變,與此同時還要生成一個用於協調多個資料庫操作的主資料庫日誌檔案,在主資料庫日誌檔案中將包含各個資料庫回滾日誌檔案的檔名,在每個回滾日誌檔案中也同樣包含了主資料庫日誌檔案的檔名資訊。然而對於無需主資料庫日誌檔案的回滾日誌檔案,其中也會保留主資料庫日誌檔案的資訊,只是此時該資訊的值為空。
我們可以將回滾日誌視為"HOT"日誌檔案,因為它的存在就是為了恢復資料庫的一致性狀態。當某一程式正在更新資料庫時,應用程式或OS突然崩潰,這樣更新操作就不能順利完成。因此我們可以說"HOT"日誌只有在異常條件下才會生成,如果一切都非常順利的話,該檔案將永遠不會存在。
四、資料寫入:
如果某一程式要想在資料庫上執行寫操作,那麼必須先獲取共享鎖,在共享鎖獲取之後再獲取保留鎖。因為保留鎖則預示著在將來某一時刻該程式將會執行寫操作,所以在同一時刻只有一個程式可以持有一把保留鎖,但是其它程式可以繼續持有共享鎖以完成資料讀取的操作。如果要執行寫操作的程式不能獲取保留鎖,那麼這將說明另一程式已經獲取了保留鎖。在此種情況下,寫操作將失敗,並立即返回SQLITE_BUSY錯誤。在成功獲取保留鎖之後,該寫程式將建立回滾日誌。
在對任何資料作出改變之前,寫程式會將待修改頁中的原有內容先行寫入回滾日誌檔案中,然而,這些資料發生變化的頁起初並不會直接寫入磁碟檔案,而是保留在記憶體中,這樣其它程式就可以繼續讀取該資料庫中的資料了。
或者是因為記憶體中的cache已滿,或者是應用程式已經提交了事務,最終,寫程式將資料更新到資料庫檔案中。然而在此之前,寫程式必須確保沒有其它的程式正在讀取資料庫,同時回滾日誌中的資料確實被物理的寫入到磁碟檔案中,其步驟如下:
1). 確保所有的回滾日誌資料被物理的寫入磁碟檔案,以便在出現系統崩潰時可以將資料庫恢復到一致的狀態。
2). 獲取PENDING鎖,再獲取排他鎖,如果此時其它的程式仍然持有共享鎖,寫入執行緒將不得不被掛起並等待直到那些共享鎖消失之後,才能進而得到排他鎖。
3). 將記憶體中持有的修改頁寫出到原有的磁碟檔案中。
如果寫入到資料庫檔案的原因是因為cache已滿,那麼寫入程式將不會立刻提交,而是繼續對其它頁進行修改。但是在接下來的修改被寫入到資料庫檔案之前,回滾日誌必須被再一次寫到磁碟中。還要注意的是,寫入程式獲取到的排他鎖必須被一直持有,直到所有的改變被提交時為止。這也意味著,從資料第一次被重新整理到磁碟檔案開始,直到事務被提交之前,其它的程式不能訪問該資料庫。
當寫入程式準備提交時,將遵循以下步驟:
4). 獲取排他鎖,同時確保所有記憶體中的變化資料都被寫入到磁碟檔案中。
5). 將所有資料庫檔案的變化資料物理的寫入到磁碟中。
6). 刪除日誌檔案。如果在刪除之前出現系統故障,程式在下一次開啟該資料庫時仍將基於該HOT日誌進行恢復操作。因此只有在成功刪除日誌檔案之後,我們才可以認為該事務成功完成。
7). 從資料庫檔案中刪除所有的排他鎖和PENDING鎖。
一旦PENDING鎖被釋放,其它的程式就可以開始再次讀取資料庫了。
如果一個事務中包含多個資料庫的修改,那麼它的提交邏輯將更為複雜,見如下步驟:
4). 確保每個資料庫檔案都已經持有了排他鎖和一個有效的日誌檔案。
5). 建立主資料庫日誌檔案,同時將每個資料庫的回滾日誌檔案的檔名寫入到該主資料庫日誌檔案中。
6). 再將主資料庫日誌檔案的檔名分別寫入到每個資料庫回滾日誌檔案的指定位置中。
7). 將所有的資料庫變化持久化到資料庫磁碟檔案中。
8). 刪除主日誌檔案,如果在刪除之前出現系統故障,程式在下一次開啟該資料庫時仍將基於該HOT日誌進行恢復操作。因此只有在成功刪除主日誌檔案之後,我們才可以認為該事務成功完成。
9). 刪除每個資料庫各自的日誌檔案。
10).從所有資料庫中刪除掉排他鎖和PENDING鎖。
最後需要說明的是,在SQLite2中,如果多個程式正在從資料庫中讀取資料,也就是說該資料庫始終都有讀操作發生,即在每一時刻該資料庫都持有至少一把共享鎖,這樣將會導致沒有任何程式可以執行寫操作,因為在資料庫持有讀鎖的時候是無法獲取寫鎖的,我們將這種情形稱為"寫飢餓"。在SQLite3中,通過使用PENDING鎖則有效的避免了"寫飢餓"情形的發生。當某一程式持有PENDING鎖時,已經存在的讀操作可以繼續進行,直到其正常結束,但是新的讀操作將不會再被SQLite接受,所以在已有的讀操作全部結束後,持有PENDING鎖的程式就可以被啟用並試圖進一步獲取排他鎖以完成資料的修改操作。
五、SQL級別的事務控制:
SQLite3在實現上確實針對鎖和併發控制做出了一些精巧的變化,特別是對於事務這一SQL語言級別的特徵。在預設情況下,SQLite3會將所有的SQL操作置於antocommit模式下,這樣所有針對資料庫的修改操作都會在SQL命令執行結束後被自動提交。在SQLite中,SQL命令"BEGIN TRANSACTION"用於顯式的宣告一個事務,即其後的SQL語句在執行後都不會自動提交,而是需要等到SQL命令"COMMIT"或"ROLLBACK"被執行時,才考慮提交還是回滾。由此可以推斷出,在BEGIN命令被執行後並沒有立即獲得任何型別的鎖,而是在執行第一個SELECT語句時才得到一個共享鎖,或者是在執行第一個DML語句時才獲得一個保留鎖。至於排它鎖,只有在資料從記憶體寫入磁碟時開始,直到事務提交或回滾之前才能持有排它鎖。
如果多個SQL命令在同一個時刻同一個資料庫連線中被執行,autocommit將會被延遲執行,直到最後一個命令完成。比如,如果一個SELECT語句正在被執行,在這個命令執行期間,需要返回所有檢索出來的行記錄,如果此時處理結果集的執行緒因為業務邏輯的需要被暫時掛起並處於等待狀態,而其它的執行緒此時或許正在該連線上對該資料庫執行INSERT、UPDATE或DELETE命令,那麼所有這些命令作出的資料修改都必須等到SELECT檢索結束後才能被提交。
相關文章
- 併發控制——樂觀鎖和悲觀鎖
- 聊聊併發控制鎖
- Python學習手冊之控制結構(二)Python
- PostgreSQL 併發控制機制(2):表級鎖和行級鎖SQL
- MySQL如何加鎖控制併發MySql
- Java併發知識點快速複習手冊(上)Java
- Java併發知識點快速複習手冊(下)Java
- Redux學習手冊Redux
- gorm操作sqlite3,高併發讀寫如何避免鎖庫?GoORMSQLite
- sqlite 學習SQLite
- SQLite學習SQLite
- Python學習手冊之類和繼承Python繼承
- InnoDB學習(五)之MVCC多版本併發控制MVC
- python學習手冊(8)Python
- python學習手冊(10)Python
- python學習手冊(4)Python
- Java開發技巧——併發控制中的樂觀鎖與悲觀鎖Java
- 分散式鎖不是控制併發冪等的方式分散式
- 深入理解Mysql——鎖、事務與併發控制MySql
- 深入理解 MySQL—鎖、事務與併發控制MySql
- Spring入門學習手冊 1:最簡單的反轉控制Spring
- 初識多版本併發控制(MVCC)-每週學習分享MVC
- Sqlite學習筆記之Sqlite歷史SQLite筆記
- Java併發包原始碼學習系列:ReentrantReadWriteLock讀寫鎖解析Java原始碼
- 和朱曄一起復習Java併發(三):鎖(含鎖效能測試)Java
- 「分散式技術專題」併發系列一:基於加鎖的併發控制分散式
- go併發之goroutine和channel,併發控制入門篇Go
- 併發控制
- 【前端工程師手冊】學習迴流和重繪(reflow和repaint)前端工程師AI
- Python學習手冊之捕獲組和特殊匹配字串Python字串
- SQLite鎖機制SQLite
- JUC併發程式設計學習筆記(四)8鎖現象程式設計筆記
- PHP 手冊 (類與物件) 學習筆記六:訪問控制(可見性)PHP物件筆記
- MySQL的事務機制和鎖(InnoDB引擎、MVCC多版本併發控制技術)MySqlMVC
- 高併發(鎖)
- 併發:死鎖
- 論 MySql InnoDB 如何通過插入意向鎖控制併發插入MySql
- 併發學習計劃-ArrayBlockingQueue和LinkedBlockingQueue02BloC
- Android SQLite學習筆記AndroidSQLite筆記