久久精品人人爽,华人av在线,亚洲性视频网站,欧美专区一二三

數據庫并發控制的作用及示例分析

171次閱讀
沒有評論

共計 7326 個字符,預計需要花費 19 分鐘才能閱讀完成。

數據庫并發控制的作用及示例分析,相信很多沒有經驗的人對此束手無策,為此本文總結了問題出現的原因和解決方法,通過這篇文章希望你能解決這個問題。

1. 數據庫并發控制的作用

1.1 事務的概念

在介紹并發控制前,首先需要了解事務。數據庫提供了增刪改查等幾種基礎操作,用戶可以靈活地組合這幾種操作,實現復雜的語義。在很多場景下,用戶希望一組操作可以做為一個整體一起生效,這就是事務。事務是數據庫狀態變更的基本單元,包含一個或多個操作 (例如多條 SQL 語句)。經典的轉賬事務,就包括三個操作:(1) 檢查 A 賬戶余額是否足夠。(2)如果足夠,從 A 扣減 100 塊。(3)B 賬戶增加 100 塊。

事務有個基本特性:這一組操作要么一起生效,要么都不生效,事務執行過程中如遇錯誤,已經執行的操作要全部撤回,這就是事務的原子性。如果失敗發生后,部分生效的事務無法撤回,那數據庫就進入了不一致狀態,與真實世界的事實相左。例如轉賬事務從 A 賬戶扣款 100 塊后失敗了,B 賬戶還未增加款項,如果 A 賬戶扣款操作未撤回,這個世界就莫名奇妙丟失了 100 塊。原子性可以通過記日志 (更改前的值) 來實現,還有一些數據庫將事務操作緩存在本地,如遇失敗,直接丟棄緩存里的操作。

事務只要提交了,它的結果就不能改變了,即使遇到系統宕機,重啟后數據庫的狀態與宕機前一致,這就是事務的持久性。數據只要存儲非易失存儲介質,宕機就不會導致數據丟失。因此數據庫可以采用以下方法來保證持久性:(1)事務完成前,所有的更改都保證存儲到磁盤上了。或 (2) 提交完成前,事務的更改信息,以日志的形式存儲在磁盤,重啟過程根據日志恢復出數據庫系統的內存狀態。一般而言,數據庫會選擇方法(2),原因留給讀者思考。

數據庫為了提高資源利用率和事務執行效率、降低響應時間,允許事務并發執行。但是多個事務同時操作同一對象,必然存在沖突,事務的中間狀態可能暴露給其它事務,導致一些事務依據其它事務中間狀態,把錯誤的值寫到數據庫里。需要提供一種機制,保證事務執行不受并發事務的影響,讓用戶感覺,當前仿佛只有自己發起的事務在執行,這就是隔離性。隔離性讓用戶可以專注于單個事務的邏輯,不用考慮并發執行的影響。數據庫通過并發控制機制保證隔離性。由于隔離性對事務的執行順序要求較高,很多數據庫提供了不同選項,用戶可以犧牲一部分隔離性,提升系統性能。這些不同的選項就是事務隔離級別。

數據庫反映的是真實世界,真實世界有很多限制,例如:賬戶之間無論怎么轉賬,總額不會變等現實約束; 年齡不能為負值,性別最多只能有男、女、跨性別者三種選項等完整性約束。事務執行,不能打破這些約束,保證事務從一個正確的狀態轉移到另一個正確的狀態,這就是一致性。不同與前三種性質完全由數據庫實現保證,一致性既依賴于數據庫實現(原子性、持久性、隔離性也是為了保證一致性),也依賴于應用端編寫的事務邏輯。

1.2 事務并發控制如何保證隔離性

為了保證隔離性,一種方式是所有事務串行執行,讓事務之間不互相干擾。但是串行執行效率非常低,為了增大吞吐,減小響應時間,數據庫通常允許多個事務同時執行。因此并發控制模塊需要保證:事務并發執行的效果,與事務串行執行的效果完全相同(serializability),以達到隔離性的要求。

為了方便描述并發控制如何保證隔離性,我們簡化事務模型。事務是由一個或多個操作組成,所有的操作最終都可以拆分為一系列讀和寫。一批同時發生的事務,所有讀、寫的一種執行順序,被定義為一個 schedule,例如:

T1、T2 同時執行,一個可能的 schedule: T1.read(A),T2.read(B),T1.write(A),T1.read(B),T2.write(A)

如果并發事務執行的 schedule 效果與串行執行的 schedule(serial  schedule)等價,就可以滿足 serializability。一個 schedule 不斷調換讀寫操作的順序,總會變成一個 serializable  schedule,但是有的調換可能導致事務執行的結果不一樣。一個 schedule 中,相鄰的兩個操作調換位置導致事務結果變化,那么這兩個操作就是沖突的。沖突需要同時滿足以下條件:

1. 這兩個操作來自不同事務

2. 至少有一個是寫操作

3. 操作對象相同

因此常見的沖突包括:

讀寫沖突。事務先 A 讀取某行數據、事務 B 后修改該行數據,和事務 B 先修改某行事務、事務 A 后讀該行記錄兩種 schedule。事務 A 讀到的結果不同。這種沖突可能會導致不可重復讀異象和臟讀異象。

寫讀沖突。與讀寫沖突產生的原因相同。這種沖突可能會導致臟讀異象。

寫寫沖突。兩個操作先后寫一個對象,后一個操作的結果決定了寫入的最終結果。這種沖突可能會導致更新丟失異象。

數據庫只要保證,并發事務的 schedule,保持沖突操作的執行順序不變,只調換不沖突的操作,可以成為 serial  schedule,就可以認為它們等價。這種等價判斷方式叫做 conflict equivalent:兩個 schedule 的沖突操作順序相同。例如下圖的例子,T1  write(A)與 T3 read(A)沖突,且 T1 先于 T3 發生。T1 read(B)和 T2  write(B)沖突,且 T2 先于 T1,因此左圖事務執行的 schedule,與 T2,T1,T3 串行執行的 serial schedule(右圖)   等價。左圖的執行順序滿足 conflict serializablity。

再分析一個反例:T1 read(A)與 T2 write(A)沖突且 T1 先于 T2,T2 write(A)與 T2  write(A)沖突且 T2 先于 T1。下圖這個個 schedule 無法與任何一個 serial schedule 等價,是一個不滿足 conflict  serializablity 的執行順序,會造成更新丟失的異象。

總體來說,serializability 是比較嚴格的要求,為了提高數據庫系統的并發性能,很多用戶愿意去降低隔離性的要求以尋求更好的性能。數據庫系統往往會實現多種隔離級別,供用戶靈活選擇,關于事務隔離級別,可以參看這篇文章。

并發控制的要求清楚了,如何實現呢? 后文將依據沖突檢測的樂觀程度,一一介紹并發控制常見的實現方法。

2. 基于兩階段鎖的并發控制

2.1 2PL

既然要保證操作按正確的順序執行,最容易想到的方法就是加鎖保護訪問對象。數據庫系統的鎖管理器模塊,專門負責給訪問對象加鎖和釋放鎖,保證只有持有鎖的事務,才能操作相應的對象。鎖可以分為兩類:S-Lock 和 X -Lock,S-Lock 是讀請求使用的共享鎖,X-Lock 是寫請求使用的排他鎖。它們的兼容性如下:操作同一個對象,只有兩個讀請求相互兼容,可以同時執行,讀寫和寫寫操作都會因為鎖沖突而串行執行。

2PL(Two-phase locking)是數據庫最常見的基于鎖的并發控制協議,顧名思義,它包含兩個階段:

階段一:Growing,事務向鎖管理器請求它需要的所有鎖(存在加鎖失敗的可能)。

階段二:Shrinking,事務釋放 Growing 階段獲取的鎖,不允許再請求新鎖。

為什么加鎖和放鎖要涇渭分明地分為兩個階段呢?

2PL 并發控制目的是為了達到 serializable,如果并發控制不事先將所有需要的鎖申請好,而是釋放鎖后,還允許再次申請鎖,可能出現事務內兩次操作同一對象之間,其它事務修改這一對象(如下圖所示),進而無法達到 conflict  serializable,出現不一致的現象(下面的例子是 lost update)。

2PL 可以保證 conflict  serializability,因為事務必須拿到所有需要的鎖才能執行。例如正在執行的事務 A 與事務 B 沖突,事務 B 要么已經執行完,要么還在等待。因此那些沖突操作的執行順序,與 BA 或 AB 串行執行時沖突操作執行順序一致。

所以,數據庫只要采用 2PL 就能保證一致性和隔離性了嗎? 來看一下這個例子:

以上執行順序是符合 2PL 的,但 T2 讀到了未提交的數據。如果此時 T1 回滾,則會引發級聯回滾(T1 的更改,不能被任何事務看到)。因此,數據庫往往使用的是加強版的 S(trong)S(trict)2PL,它相較于 2PL 有一點不同:shrinking 階段,只能在事務結束后再釋放鎖,完全杜絕了事務未提交的數據被讀到。

2.2 死鎖處理

并發事務加鎖放鎖必然繞不開一個問題 – 死鎖:事務 1 持有 A 鎖等 B 鎖,事務 2 持有 B 鎖等 A 鎖。目前解決死鎖問題有兩種方案:

Deadlock Detection:

數據庫系統根據 waits-for 圖記錄事務的等待關系,其中點代表事務,有向邊代表事務在等待另一個事務放鎖。當 waits-for 圖出現環時,代表死鎖出現了。系統后臺會定時檢測 waits-for 圖,如果發現環,則需要選擇一個合適的事務 abort。

Deadlock Prevention:

當事務去請求一個已經被持有的鎖時,數據庫系統為防止死鎖,殺死其中一個事務(一般持續越久的事務,保留的優先級越高)。這種防患于未然的方法不需要 waits-for 圖,但提高了事務被殺死的比率。

2.3 意向鎖

如果只有行鎖,那么事務要更新一億條記錄,需要獲取一億個行鎖,將占用大量的內存資源。我們知道鎖是用來保護數據庫內部訪問對象的,這些對象根據大小可能是:屬性(Attribute)、記錄(Tuple)、頁面(Page)、表(Table),相應的鎖可分為行鎖、頁面鎖、表鎖(沒人實現屬性鎖,對于 OLTP 數據庫,最小的操作單元是行)。對于事務來講,獲得最少量的鎖當然是最好的,比如更新一億條記錄,或許加一個表鎖就足夠了。

層次越高的鎖 (如表鎖),可以有效減少對資源的占用,顯著減少鎖檢查的次數,但會嚴重限制并發。層次越低的鎖(如行鎖),有利于并發執行,但在事務請求對象多的情況下,需要大量的鎖檢查。數據庫系統為了解決高層次鎖限制并發的問題,引入了意向(Intention) 鎖的概念:

Intention-Shared (IS):表明其內部一個或多個對象被 S -Lock 保護,例如某表加 IS,表中至少一行被 S -Lock 保護。

Intention-Exclusive (IX):表明其內部一個或多個對象被 X -Lock 保護。例如某表加 IX,表中至少一行被 X -Lock 保護。

Shared+Intention-Exclusive  (SIX):表明內部至少一個對象被 X -Lock 保護,并且自身被 S -Lock 保護。例如某個操作要全表掃描,并更改表中幾行,可以給表加 SIX。讀者可以思考一下,為啥沒有 XIX 或 XIS

意向鎖和普通鎖的兼容關系如下所示:

意向鎖的好處在于:當表加了 IX,意味著表中有行正在修改。(1)這時對表發起 DDL 操作,需要請求表的 X 鎖,那么看到表持有 IX 就直接等待了,而不用逐個檢查表內的行是否持有行鎖,有效減少了檢查開銷。(2)這時有別的讀寫事務過來,由于表加的是 IX 而非 X,并不會阻止對行的讀寫請求(先在表上加 IX,再去記錄上加 S /X),事務如果沒有涉及已經加了 X 鎖的行,則可以正常執行,增大了系統的并發度。

3. 基于 Timing Order(T/O)的并發控制

為每個事務分配 timestamp,并以此決定事務執行順序。當事務 1 的 timestamp 小于事務 2 時,數據庫系統要保證事務 1 先于事務 2 執行。timestamp 分配的方式包括:(1)物理時鐘;(2)邏輯時鐘;(2)混合時鐘。

3.1 Basic T/O

基于 T / O 的并發控制,讀寫不需加鎖,  每行記錄都標記了最后修改和讀取它的事務的 timestamp。當事務的 timestamp 小于記錄的 timestamp 時 (不能讀到”未來的”數據),需要 abort 后重新執行。假設記錄 X 上標記了讀寫兩個 timestamp:WTS(X) 和 RTS(X),事務的 timestamp 為 TTS,可見性判斷如下:

讀:

TTS WTS(X):該對象對該事務不可見,abort 事務,取一個新 timestamp 重新開始。

TTS WTS(X):該對象對事務可見,更新 RTS(X) = max(TTS,RTS(X))。為了滿足 repeatable  read,事務復制 X 的值。

為了防止讀到臟數據,可以在記錄上做特殊標記,讀請求需等待事務提交后再去讀。

寫:

TTS WTS(X) || TTS RTS(X):abort 事務,重新開始。

TTS WTS(X) TTS RTS(X):事務更新 X,WTS(X) = TTS。

這里之所以要求 TTS RTS(X),是為了防止如下情況:讀請求的時間戳為 rts,已經讀過 X,時間戳設為 RTS(X)=rts,如果新事務的 TTS   RTS(X),并且更新成功,則 rts 讀請求再來讀一次就看到新的更改了,違反了 repeatable  read,因此這是為了避免讀寫沖突。記錄上存儲了最后的讀寫時間,可以保證 conflict serializable

這種方式也能避免 write skew,例如:初始狀態,X 和 Y 兩條記錄,X=-3,Y=5,X+Y   0,RTS(X)=RTS(Y)=WTS(X)=WTS(Y)=0。事務 T1 的時間戳為 TTS1=1,事務 T2 的時間戳 TTS2=2。

它缺陷包括:

長事務容易餓死,因為長事務的 timestamp 偏小,大概率會在執行一段時間后讀到更新的數據,導致 abort。

讀操作也會產生寫(寫 RTS)。

4. 基于 Validation(OCC)的并發控制

執行過程中,每個事務維護自己的寫操作 (Basic  T/ O 在事務執行過程中寫就將數據寫入 DB) 和相應的 RTS/WTS,提交時判斷自己的更改是否和數據庫中已存在的數據沖突,如果不沖突才寫入 DB。OCC 分為三個階段:

Read Write Phase:即讀寫階段,事務維護讀的結果和即將提交的更改,以及寫入記錄的 RTS 和 WTS。

Validation Phase:檢查事務是否與數據庫中的數據沖突。

Write Phase:不沖突就寫入,沖突就 abort,restart。

Read Write Phase 結束,進入 Validation Phase 相當于事務準備完成,進入提交階段了,進入 Validation  Phase 的時間被選做記錄行的時間戳,來定序。不用事務開始時間是因為:事務執行時間可能較長,導致后開始的事務可能先提交,這會加大事務沖突的概率,較小時間戳的事務后寫入數據庫,肯定會 abort。

Validation 過程

假設當前只有兩個事務 T1 和 T2,并修改了相同數據行,T1 的時間戳 T2 的時間戳(即 validation 順序:T1  T2,對用戶而言,T1 先發生于 T2),則有如下情況:

(1)T1 在 validate 階段,T2 還在 Read Write Phase。此時只要 T1 和 T2 已經發生的讀寫沒有沖突,就可以提交。

如果 WS(T1) cap; (RS(T2) cup; WS(T2)) = empty;,說明 T2 和 T1 寫的記錄無沖突,validation 通過,可以寫入。

否則,T2 與 T1 之間存在讀寫沖突或寫寫沖突,T1 需要回滾。讀寫沖突:T2 讀到了 T1 寫之前的版本,T1 提交后,它可能讀到 T1 寫的版本,不可重復讀。寫寫沖突:T2 有可能在舊版本基礎上更新,再次寫入,造成 T1 的更新丟失。

(2)T1 完成 validate 階段,進入 write 階段直到提交完成,這已經是不可逆的了。T2 在 T1 進入 write  phase 之前的讀寫,肯定和 T1 的操作不沖突(因為 T1  validation 通過了)。T2 之后繼續的讀寫操作,有可能沖突與 T1 要提交的操作,因此 T2 進入 validate 階段:

如果 WS(T1) cap; RS(T2)=   empty;,說明 T2 沒讀到 T1 寫的記錄,validation 通過,T2 可以寫入。(為什么不驗證 WS(T2)了呢?WS(T1)已經提交了,且它的時間戳小于 WS(T2),WS(T2)里之前的一部分肯定沒有沖突,之后的一部分,因為沒有讀過 T1 的寫入的對象,寫進去也沒問題,不會覆蓋 WS(T1)的寫)

否則,T2 與 T1 之間存在讀寫沖突和寫寫沖突,T2 需要回滾。讀寫沖突:T2 讀到了 T1 寫之前的版本,T1 提交后,它可能讀到 T1 寫的版本,不可重復讀。寫寫沖突:T2 有可能在舊版本基礎上更新,再次寫入,造成 T1 的更新丟失。

5. 基于 MVCC 的并發控制

數據庫維護了一條記錄的多個物理版本。事務寫入時,創建寫入數據的新版本,讀請求依據事務 / 語句開始時的快照信息,獲取當時已經存在的最新版本數據。它帶來的最直接的好處是:寫不阻塞讀,讀也不阻塞寫,讀請求永遠不會因此沖突失敗 (例如單版本 T /O) 或者等待(例如單版本 2PL)。對數據庫請求來說,讀請求往往多于寫請求。主流的數據庫幾乎都采用了這項優化技術。

MVCC 是讀和寫請求的優化技術,沒有完全解決數據庫并發問題,它需要與前述的幾種并發控制技術組合,才能提供完整的并發控制能力。常見的并發控制技術種類包括:MV-2PL,MV-T/ O 和 MV-OCC,它們的特點如下表:

MVCC 還有兩個關鍵點需要考慮:多版本數據的存儲和多余多版本數據的回收。

多版本數據存儲方式,大致可以分為兩類:(1)Append  only 的方式,新舊版本存儲在同一個表空間,例如基于 LSM-Tree 的存儲引擎。(2)主表空間記錄最新版本數據,前鏡像記錄在其它表空間或數據段,例如 InnoDB 的多版本信息記錄在 undo  log。多版本數據回收又稱為垃圾回收(GC),那些沒有機會再被任何讀請求獲取的舊版本記錄,應該被及時刪除。

依據沖突處理的時機 (樂觀程度),依次介紹了基于鎖(在事務開始前預防沖突)、基于 T /O(在事務執行中判斷沖突) 和基于 Validation(在事務提交時驗證沖突)的事務并發控制機制。不同的實現適用于不同的 workload,并發沖突小的 workload,當然適合更樂觀的并發控制方式。而 MVCC 可以解決只讀事務和讀寫事務之間相互阻塞的問題,提高了事務的并發讀,被大多數主流數據庫系統采用。

看完上述內容,你們掌握數據庫并發控制的作用及示例分析的方法了嗎?如果還想學到更多技能或想了解更多相關內容,歡迎關注丸趣 TV 行業資訊頻道,感謝各位的閱讀!

正文完
 
丸趣
版權聲明:本站原創文章,由 丸趣 2023-07-18發表,共計7326字。
轉載說明:除特殊說明外本站除技術相關以外文章皆由網絡搜集發布,轉載請注明出處。
評論(沒有評論)
主站蜘蛛池模板: 师宗县| 湖北省| 镶黄旗| 宁德市| 巍山| 射洪县| 安龙县| 永和县| 邹平县| 卢氏县| 友谊县| 宿州市| 宝山区| 崇明县| 邵阳县| 乐至县| 富阳市| 浪卡子县| 太白县| 虎林市| 巴青县| 和静县| 富川| 朝阳区| 临邑县| 乳源| 禹城市| 乐亭县| 丹江口市| 阿合奇县| 寿宁县| 鄂州市| 阳山县| 丰都县| 松滋市| 呼玛县| 鸡东县| 宜川县| 济南市| 灵寿县| 若尔盖县|