iT邦幫忙

第 11 屆 iThome 鐵人賽

DAY 16
5
Software Development

30天之從 0 至 1 盡可能的建立一個好的系統 (性能基礎篇)系列 第 16

30-16 之資料庫層的難題 - 單機『 並行 』一致性難題 ( 1 )

黑色好看版 - 傳送門


https://ithelp.ithome.com.tw/upload/images/20190930/200893583XNdu76MIf.png

正文開始


本篇文章中,咱們要說說另一種資料不一致性產生的場景,那就是 :

『 並行 』產生的不一致性難題

基本上並行所產生的不一致性難題,可以分為以下幾種類型 :

  • 更新不一致
  • 髒讀
  • 不可重讀
  • 幻讀

本篇將會分為以下幾個章節來談談這幾個難題 :

  • 更新不一致難題與解法 - 鎖
  • 髒讀與不可重讀難題與解法 - MVCC
  • 幻讀難題與解法 - MVCC + Next-Lock 鎖

更新不一致難題與解法 - 鎖


這種情境如下圖 1 所示,a 與 b 兩個事務進行更新操作後,事務 a 再看看自已操作的結果,發現自已的更新消失了。這種情境被稱為『 更新不一致問題 』

https://ithelp.ithome.com.tw/upload/images/20191001/20089358EusE2oGPCM.png
圖 1 : 更新不一致問題

那這種情境 innodb 它是如何處理呢 ?

它使用鎖來處理

在 innodb 的預設,它會對要『 update、delete 』的『 行加鎖排他鎖 』,不過比較嚴格定義應該是說 :

它會對有用到『 索引 』的『 行 』加『 排他鎖 』,不然會退化成『 表 』鎖

先來說一下,所謂的排他鎖定義如下 :

排它鎖 ( X 鎖、寫鎖 ) 它保證同一個時刻,只有上鎖的那個事務可以進行修改,其它事務都無法在上鎖。

然後咱們接這來看看使用鎖來處理更新不一致問題的流程。當有兩個事務運行更新時,它會變成如下圖這樣,當事務 A 要更新 account 時,它會被上個排它鎖,而這時事務 B 只能等到事務 A commit 後,才能修改在 commit,因為排它鎖就是不能在上鎖,所以事務 b 如果想在上個排它鎖是不行的 ( update 會上排它鎖 ) 。

https://ithelp.ithome.com.tw/upload/images/20191001/20089358stsiWKwcPW.png
圖 2 : 使用鎖解決更新不一致問題

~ 小知識 1 ~
鎖這個概念在任何只要有併行概念的地方都會看到,包含我們前面提到的多線程,更廣範的說,只要在電腦科學上,鎖這個東西無所不在。寫過 java 的友人應該很有感。

~ 小知識 2 ~
select 正常來說是什麼鎖都沒加,正常時。

~ 小備註 ~
鎖的詳細知識下一篇文章中會更詳細說明。

實務上的更新不一致問題

在實務上咱們有時後會發生下述的情境 :

  1. 事務 A 抓取 X 貨物數量。
  2. 事務 B 抓取 X 貨物數量。
  3. 用戶 A 將 X 貨物數量 - 1000 後,更新資料庫。
  4. 用戶 B 將 X 貨物數量 - 2000 後,更新資料庫。

轉成成 sql 來看。

事務 A

BEGIN;
SELECT count INTO @count FROM product WHERE id = x;
...
UPDATE count SET count = @count - 1000 WHERE id = x;
COMMIT;


事務 B
BEGIN;
SELECT count INTO @count FROM product WHERE id = x;
...
UPDATE count SET count = @count - 2000 WHERE id = x;
COMMIT;

不處理會發生什麼事情呢 ? 那就是事務 a 與 b 更新完後,結果是錯誤。

count 預設 3000

預期情境 : 
事務 a -> 事務 b -> 結果 count = 0

出問題情境 :
事務 a
       -> 結果 count = 1000 ( 因為 a、b 都抓到 3000 後在減,而 a 比 b 快完成 )
事務 b

這種情況要怎麼如何處理呢 ? 由於中間還有一些業務處理,需要改改修修 select 出來的 count,而上面也有提到,正常的情況下 select 是不加鎖的,因此這時就有以下解法 :

for update 強制將 select 操作加上『 排它鎖 』

將 select 指令改成如下 :

SELECT count INTO @count FROM product WHERE id = x FOR UPDATE

就可以強制在這行上面加『 排他鎖 』,這樣事務 b 就只能等到這個鎖被釋放才能進行更新,這樣就可以確保在同一個時間,只有事務 a 可以進行更新。

還有別忘了,如果你沒有使用到索引,這個也會變成表鎖。

髒讀與不可重讀難題與解法 - MVCC


髒讀情境

這種情境如下圖 3 所示,a 事務進行更新,然後 b 事務再進行抓取欄位,但是 a 事務最後回滾,導致 b 事務抓到的資料是有問題的。這種情境被稱為『 髒讀問題 』

髒讀簡單的定義為 :

A 事務可能會讀取到 B 事務『 未提交 』的資料。

https://ithelp.ithome.com.tw/upload/images/20191001/20089358Ivx5wWFFxf.png
圖 3 : 髒讀情境

不可重複讀情境

不可重複讀這個問題本質在於 :

A 事務可能會讀取到 B 事務『 已提交 』的資料。

https://ithelp.ithome.com.tw/upload/images/20191001/20089358nXj0ilyZBJ.png
圖 4 : 不可重複讀情境

傳統有性能問題解法

當 a 事務要讀取某行時,會將它上鎖,直到 a 事務提交後,b 事務才能處理,這樣正常來說,的確不會發生 a 事務可能讀取到 b 事務已提交的資料,因為同一個時間,只有一個事務能讀那行。

這樣來看不論是髒讀或不可重讀問題,的確是都可以解。

但是上述這種機制的缺點就在於性能,因為要一個等一個。所以後來就誕生出另一種機制,就是所謂的
『 MVCC (Multi-Version Concurrency Control) 』。

性能較優的解法 MVCC ( Multi-Version Concurrency Control )

它的白話文為 :

對資料庫的何任修改與提交都不會覆蓋掉原本的資料,而是會產生不同的版本分支。

MVCC 實際上是通常在每一行記錄後面保存兩個『 隱藏列 』來實現,這兩列保存以下的事項 :

  • 行的創建時間 ( 它存的不是實際時間,而是有時間順序性的版號 )
  • 行的過期或刪除時間 ( 同上為有時間順序性的版號 )。

在這個裡可以簡單的想成這種概念。當 a 事務讀取 x 行時,不用上鎖,而是讀取 x 行的 v1 版本,而就算後來事務 b 來更新 x 行時,它修改後會新增個 v2 版本,這樣就不會和事務 a 打架。

MVCC 的精華就在於使用版本來代替鎖來處理並行問題

而上面有提到版本問題,而這個版本問題大部份都是讀取時來決定要選什麼版本,而這時根據選擇版本方法又分為以下兩個 :

快照讀與當前讀

  • 快照讀 : 取得事務開始前的最新的版本的行。
  • 當前讀 : 取得最新的版本的行。

快照讀操作如下 :

  • select

當前讀操作如下 :

  • select xxx lock in share mode
  • select xxx for update
  • insert
  • update
  • delete

上述兩個雖然都是以 select 為基本情境,但實際上在某些操作中,實際上是會隱性的用到 select,例如上述當前讀操作的 update 或 delete 它們實際上會用 select 先去找找要修改的欄位在不在,而這時它用的就是『 當前讀 』,也就是直接取得最新版本。

注意 RC 與 RR 的版本會有不同的取法

隔離級別下一篇文章中,會詳細的說明,這裡先簡單談一下。

  • Read Committed ( RC ) : 事務總是讀取最近一次 commit 的版本 ( 當前讀)。
  • Repeatable Read ( RR ) : 事務總是讀取『 當前事務 』開始前,最後一次 commit 的版本 ( 快照讀 )。

幻讀難題與解法 - MVCC + Next-Lock 鎖


mysql 官網對幻讀的定義如下 :

The so-called phantom problem occurs within a transaction when the same query produces different sets of rows at different times. For example, if a SELECT is executed twice, but returns a row the second time that was not returned the first time, the row is a “phantom” row.

只要在一個事務中,第二次的 select 多出一行就算幻讀。

傳送門

幻讀這個東西嚴格來說有不少種情境,咱們來看看以下三種。

幻讀情境 1 : 範例查詢不一致性問題

https://ithelp.ithome.com.tw/upload/images/20191001/20089358rk9NI0SrCu.png
圖 7 : 幻讀情境 1 問題

如上圖 7 所示,事務 b 會讀取到事務 a insert 時增加的資料,導致兩次 select 出來的結果不同。

這種情況的幻讀也可以用上面提到的 mvcc 來解決。

咱們知道 mvcc 會給任何修改的東西一個版號,insert 操作也相同,而上面也有提到,這個版本號有時間順序性,所以事務 b 的 select 只會找尋版本號時間順序是在事務 b 開始前的值。

https://ithelp.ithome.com.tw/upload/images/20191001/20089358rH8Glo54eA.png
圖 8 : 幻讀情境 1 使用 mvcc 解法

幻讀情境 2 : for update 問題

這種情況比較特別,它有用到咱們之前所提到的兩個上鎖方法,這兩個特別之處在於,一但使用它,就會變成所謂的『 當前讀 』,也就是會讀取最新版號。

  • select xxx lock in share mode
  • select xxx for update

圖 9 這種情況下,會變成 select for update 會讀到現在的最新版本,那這是就又會出現幻讀的現像,如下。

https://ithelp.ithome.com.tw/upload/images/20191001/200893589BhYrctzeB.png
圖 9 : 幻讀情境 2

所以這種情況 2 解法又要使用所一種鎖來解決,也就是所謂的『 next-key locking 』機制,也就是說他會上鎖範圍,如下圖 10 所示,它會上鎖 (-oo, 5),所以當你實際運行時,會發現,在執行第一個綠色 insert 時會成功,而之後的兩個卡住直到事務 b commit 才能正常的 insert。

https://ithelp.ithome.com.tw/upload/images/20191001/20089358Qb8orcPgYh.png
圖 10 : 幻讀情境 2 使用 next-key 鎖

幻讀情境 3 : 無解

接下來說的幻讀情境目前是無解的情況,如下圖 11 所示。

問題出在於事務 b 更新時,會讓 c 那個 select 操作讀取到最新的版本,因為上面在 mvcc 有提到,當進行 update 操作時會對那行變成『 當前讀 』,也就是讀最新的資料。

https://ithelp.ithome.com.tw/upload/images/20191001/20089358SIoS2M48cd.png
圖 11 : 幻讀情境 3 無解情況

結論與心得


在這篇文章中,咱們學習到了 innodb 是如何的解決咱們上述文章中所提到的並行不一致問題的處理。其中並行不一致性又分以下幾個問題 :

  • 更新不一致 : 使用鎖來解決。
  • 髒讀 : 使用 mvcc 解決。
  • 不可重讀 : 使用 mvcc 解決。
  • 幻讀 : 使用 mvcc + next-key 鎖 來解決 ( 但無法完全解如幻讀情境 3 )。

順到說一下,你看看單機的不一致性問題就已經那麼的複雜了,分散式會變什麼樣子呢 ?

參考資料



上一篇
30-15 之資料庫層的難題 - 單機『 故障 』一致性難題
下一篇
30-17 之資料庫層的難題 - 單機『 並行 』一致性難題 ( 2 )
系列文
30天之從 0 至 1 盡可能的建立一個好的系統 (性能基礎篇)30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

1 則留言

0
西撒
iT邦新手 5 級 ‧ 2020-03-08 20:27:18

Read Committed ( RC ) : 事務總是讀取最近一次 commit 的版本 ( 接近當前讀的概念,但差別在於有沒有 commit )。

不太懂這句話的意思,commit 不就表示已經完成事務,為什麼後面又一句 差別在於有沒有 commit ?

要表達的是否為,當下事務的多筆 sql 敘述中,讀取執行 sql 敘述最新狀態,即使當下這個事務還沒 commit

馬克 iT邦研究生 3 級 ‧ 2020-03-09 16:08:13 檢舉

不好意思 ~ 我自已也想不透我當初為啥要寫這樣 ~ 所以我決定移掉括號後面。

我要留言

立即登入留言