The two most important days in your life are the day you were born and the day you find out why. -Mark Twain

#65 資料庫引擎的交易資料鎖定 (Lock) 策略

延續上一次資料庫的交易文章內容,在一個資料庫系統中同一時間可以執行多個交易 (Transaction).在這同時執行的交易內容中,當遇到共同讀取和寫入同一個物件時,此時便有很大的機會將發生如上一篇文章中提到的資料衝突現象.為了要解決這個現象,資料庫引擎得採取一種策略.以學術的角度而言,策略有好幾種,但比較常見和合理的策略將是本篇文章中將討論的資料鎖定 (Lock).

Lock

首先定義上一段文字中所說的 "共同讀取和寫入同一個物件",物件是指交易內容中所感興趣的資料.可能是一筆資料,例如某一個學生的基本資料,可能是符合某條件的資料,例如去年十月份的所有訂單.以邏輯處理而言,通常來說這資料可能只存在於同一個表格,但也有可能存在於多個表格.以實體上而言,資料有可能在同一個 page,但更有可能分散在不同的 page.以邏輯上而言,資料庫引擎可以對一筆資料進行鎖定,也可以對一整個資料表格進行鎖定,或是鎖定某些特定條件的資料.以實體上而言,通常是以鎖定 page 為單位,資料庫引擎比較方便進行鎖定.

資料鎖定策略定義兩種鎖,第一種鎖是共享鎖 (Shared lock),也就是當交易對某資料進行讀取動作時,則必需先取得共享鎖,共享的意思也就是其他的交易對同樣資料進行讀取時,也會取得共享鎖,代表這資料只供讀取.所以取得共享鎖的交易可以馬上對資料進行讀取.第二種鎖是互斥鎖 (Exclusive lock),這是當交易對某資料進行寫入動作所需要取得的鎖,其顧名思義,互斥鎖在同一時間只能被一個交易取得使用,如果其他交易也要取得同份資料的互斥鎖,則必須等待.一般而言,我們將共享鎖簡稱為 S lock,互斥鎖稱為 X lock.透過這兩種 lock 便能解決上一篇文章裡所提出的問題.接下來,舉些例子來說明這兩種 lock 如何確保資料鎖定策略能成功.

首先來看下圖的範例,有兩個交易,他們對同一個資料 (A) 先進行讀取動作 (R) 再進行寫入動作 (W)


由於他們將對資料 A 進行寫入,因此他們都需要取得 X lock.假設是 T1 交易先啟動,因此當 T1 啟得 X lock 之後,T2 就得等待.當 T1 完成後 (Commit完成後),對資料 A 的 X lock 就會被釋出,因此 T2 才能得取資料 A 的 X lock,接著 T2 才能執行.因此,真正的執行情況將變成下圖.


如果執行的情況變成 T2 先啟動,則 T1 就必須等到 T2 執行完成後才能取得 X lock 接著執行.因此,不論是那一個交易先執行,另一個交易都必須等待.這是一個很單純的例子.接下來來看一個對多個資料進行讀和寫的例子.


以上是兩個交易 (T3,T4) 對資料 A,B,C 進行動作, 其中 T3 讀取資料 A,讀取和寫入資料 C,而 T4 讀取資料 A,讀取和寫入資料 B.由於 T3, T4都對資料 A進行讀取,所以他們都會取得 S lock,因此可以同時讀取資料A.之後他們分別對不同的資料 (B ,C) 進行寫入動作,因此取得 X lock時是針對不同的資料,所以不用等待對方就能馬上進行寫入動作.


以上所說的方法是採用漸進式的方式來進行資料鎖定,也就是當交易讀取或寫入某資料時,才需要對該資料取得 S lock 或 X lock,而且 S lock 和 X lock 將決定交易是否馬上繼續執行或是需要等待.因此,這樣的方式對資料庫引擎來說是漸漸增加 lock 的數量,然後在交易 commit 或 abort 時,一次釋放該交易所擁有的 lock,就有如下圖一樣:

DeadLock

以上的策略會讓死結 (deadlock) 有機會產生,主要的原因在於 X lock,舉例如下圖:


當 T1 嘗試著取得資料B 的 X lock,結果資料 B 的 X lock 在被 T2 使用中,因此得等到 T2 完成才行,結果 T2 後面有個動作要對資料A 進行寫入,欲取得資料A 的 X lock 時,此時它正被 T1 所使用,需得到 T1 完成才行.因此就進入了一個你等我,我等你的狀態,也是電腦科學領域中常見的 deadlock 問題.資料庫引擎需要有能力來偵測死結的情況,並且要有能力處理死結.以理論上來說,偵測不是難事,資料庫引擎得維護一個 waits-for graph 來對整體系統裡那些交易在等待那些交易完成,透過 waits-for graph 可讓資料庫引擎偵測死結.這對資料庫引擎來說是一項不得不花費的成本,因為要偵測死結的存在,才能對死結的現象進行解決.解決死結最簡單的方法就是將造成死結的交易終止,好讓其他交易可以順利取得 lock,然後再將被終止的交易重新啟動.無論如何,資料庫引擎在這能做的只是事後的預防與問題的排除,若想要盡量避免死結,還需要程式開發人員的配合.交易是程式開發人員所撰寫,因此在寫交易時要儘量避免死結的發生便很重要.有幾個簡單的準則可供參考:
  • 若非必要,不要為你的 stored procedure 設定成以交易的方式進行.
  • 交易應盡量短.如果交易過長,這表示交易將進行更多的讀取或寫入的動作,無形中也增加了死結的機會.因此,最好把交易分割到最小不可分割的單位.過於複雜的商業邏輯由外面的程式邏輯層執行,資料庫只要做基本的資料操作動作.
  • 盡量讓交易對資料有相同順序的讀取或寫入.如前面的例子,死結的發生往往在於你等我,我等你的情況.因此,若把資料讀寫的順序盡可能排成一樣,這樣就能大大減少你等我我等你的機會.
以理論上來說,資料鎖定的策略不只以上介紹的方法,還有其他不同的鎖定策略以及死結預防和處理的方式.不論是用那一種,對資料庫來說都是相對應要付出成本.若能將這方面的成本降低,這將有助於資料庫引擎效能.

Share:

#64 資料庫引擎交易 (Transaction) 進行中的讀寫異常

前面的文章曾談到交易 (Transaction) 需要具有 ACID 的特性.在一個繁忙的資料庫系統中一定會有許多的交易同時執行,這篇文章便來談論許多交易同時執行時會遇到那些挑戰.

許多交易在進行時,非常有可能會遇到對相同的資料進行讀或寫的動作.如果所有的交易對相同的資料進行讀的動作,則這情況並沒有什麼好擔心的,因為所有的交易對這份資料都是讀的動作,早讀和晚讀都是同一個答案,所以不會造成任何的資料異常現象.但如果情況變成其中有一個交易或多個交易對同一份資料進行寫的動作時,那麼早寫和晚寫就會有很大的影響了.因此,我們在乎的情況便是當有交易在進行寫的動作.以下假設某個資料庫系統中有兩個交易,這兩個交易會對同一份資料進行讀和寫的動作:



如上圖所示,T1 做的動作是 A=A-100 和 B=B+100,T2 做的動作是 A=A*1.5 和 B=B*1.5.如果交易執行的情況如上圖的話,假設 A 和 B 的初始值都是 300,你認為當這兩個交易完成後,A 和 B 的值會多少呢 ? 沒算錯的話,A 應是 300,B 應是 550.如果 T1 和 T2 執行的情況不是像上圖一樣,而是 T1 先執行,完成後再執行 T2,此時答案是多少呢? 沒算錯的話,A 還是 300,但 B 是 600.這時你就會發現怪怪的,執行的順序果然會影響答案,這可是不得了的大事呀.如果你把 A 和 B 想像成是銀行中的戶頭,而 T1 就像在執行匯款的動作,T2 就像是在執行加值的動作.這兩組不同目的動作是可以同時被觸發的,但很顯然地你一定發現 T1 在執行動作時,T2 不應該執行,因為他們會對相同的資料進行寫的動作.如果你允許他們可以同時對相同資料進行寫的動作,則就會發生資料異常的現象.所謂資料異常就是指不應該發生的情況.正常的情況是 T1 先執行再執行 T2,或是 T2 先執行再執行 T1.我們再來看另一種例子:



這一個例子是 T1 的讀寫動作完成後 T2 才開始進行讀寫,但最大的差別是 T2 在進行讀寫時,T1 還沒有 commit.等到 T2 commit 完成之後,最後 T1 才決定 abort.這種情況也是我們不希望看到的,因為這也是一種資料異常的現象,因為 T2 在對 A資料進行讀寫時,它的基礎是建立在 T1 對 A 完成的結果上,結果 T1 對其結果是否定的 (abort),所以 T2 的結果就便成是一個大笑話了.

看到這裡時,你就可以知道當某一個交易對某一個資料進行 "寫" 的動作時,在這交易尚未完成前 (Commit or Abort),我們不希望其他交易能對相同資料進行 "讀" 和 "寫" 的動作.相同地,當某一個交易對某一個資料進行 "讀" 的動作時,在這交易尚未完成前 (Commit or Abort),我們也不希望其他交易能對相同資料進行 "寫" 的動作.如下圖所示:



為了防止以上資料異常的現象發生,資料庫引擎裡需要某些特別的設計來防止這類的事情發生,這個特別的設計稱為 Locking.也就是當某交易在對某份資料進行動作時,便把該份資料鎖住讓其他交易無法使用該資料.下一篇文章將會來談談這個鎖資料的內容.

Share: