本篇文章中,咱們要說說另一種資料不一致性產生的場景,那就是 :
『 並行 』產生的不一致性難題
基本上並行所產生的不一致性難題,可以分為以下幾種類型 :
本篇將會分為以下幾個章節來談談這幾個難題 :
這種情境如下圖 1 所示,a 與 b 兩個事務進行更新操作後,事務 a 再看看自已操作的結果,發現自已的更新消失了。這種情境被稱為『 更新不一致問題 』
圖 1 : 更新不一致問題
那這種情境 innodb 它是如何處理呢 ?
它使用鎖來處理
在 innodb 的預設,它會對要『 update、delete 』的『 行加鎖排他鎖 』,不過比較嚴格定義應該是說 :
它會對有用到『 索引 』的『 行 』加『 排他鎖 』,不然會退化成『 表 』鎖
先來說一下,所謂的排他鎖定義如下 :
排它鎖 ( X 鎖、寫鎖 ) 它保證同一個時刻,只有上鎖的那個事務可以進行修改,其它事務都無法在上鎖。
然後咱們接這來看看使用鎖來處理更新不一致問題的流程。當有兩個事務運行更新時,它會變成如下圖這樣,當事務 A 要更新 account 時,它會被上個排它鎖,而這時事務 B 只能等到事務 A commit 後,才能修改在 commit,因為排它鎖就是不能在上鎖,所以事務 b 如果想在上個排它鎖是不行的 ( update 會上排它鎖 ) 。
圖 2 : 使用鎖解決更新不一致問題
~ 小知識 1 ~
鎖這個概念在任何只要有併行概念的地方都會看到,包含我們前面提到的多線程,更廣範的說,只要在電腦科學上,鎖這個東西無所不在。寫過 java 的友人應該很有感。
~ 小知識 2 ~
select 正常來說是什麼鎖都沒加,正常時。
~ 小備註 ~
鎖的詳細知識下一篇文章中會更詳細說明。
在實務上咱們有時後會發生下述的情境 :
轉成成 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 可以進行更新。
還有別忘了,如果你沒有使用到索引,這個也會變成表鎖。
這種情境如下圖 3 所示,a 事務進行更新,然後 b 事務再進行抓取欄位,但是 a 事務最後回滾,導致 b 事務抓到的資料是有問題的。這種情境被稱為『 髒讀問題 』
髒讀簡單的定義為 :
A 事務可能會讀取到 B 事務『 未提交 』的資料。
圖 3 : 髒讀情境
不可重複讀這個問題本質在於 :
A 事務可能會讀取到 B 事務『 已提交 』的資料。
圖 4 : 不可重複讀情境
當 a 事務要讀取某行時,會將它上鎖,直到 a 事務提交後,b 事務才能處理,這樣正常來說,的確不會發生 a 事務可能讀取到 b 事務已提交的資料,因為同一個時間,只有一個事務能讀那行。
這樣來看不論是髒讀或不可重讀問題,的確是都可以解。
但是上述這種機制的缺點就在於性能,因為要一個等一個。所以後來就誕生出另一種機制,就是所謂的
『 MVCC (Multi-Version Concurrency Control) 』。
它的白話文為 :
對資料庫的何任修改與提交都不會覆蓋掉原本的資料,而是會產生不同的版本分支。
MVCC 實際上是通常在每一行記錄後面保存兩個『 隱藏列 』來實現,這兩列保存以下的事項 :
在這個裡可以簡單的想成這種概念。當 a 事務讀取 x 行時,不用上鎖,而是讀取 x 行的 v1 版本,而就算後來事務 b 來更新 x 行時,它修改後會新增個 v2 版本,這樣就不會和事務 a 打架。
MVCC 的精華就在於使用版本來代替鎖來處理並行問題
而上面有提到版本問題,而這個版本問題大部份都是讀取時來決定要選什麼版本,而這時根據選擇版本方法又分為以下兩個 :
快照讀操作如下 :
當前讀操作如下 :
上述兩個雖然都是以 select 為基本情境,但實際上在某些操作中,實際上是會隱性的用到 select,例如上述當前讀操作的 update 或 delete 它們實際上會用 select 先去找找要修改的欄位在不在,而這時它用的就是『 當前讀 』,也就是直接取得最新版本。
注意 RC 與 RR 的版本會有不同的取法
隔離級別下一篇文章中,會詳細的說明,這裡先簡單談一下。
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 多出一行就算幻讀。
幻讀這個東西嚴格來說有不少種情境,咱們來看看以下三種。
圖 7 : 幻讀情境 1 問題
如上圖 7 所示,事務 b 會讀取到事務 a insert 時增加的資料,導致兩次 select 出來的結果不同。
這種情況的幻讀也可以用上面提到的 mvcc 來解決。
咱們知道 mvcc 會給任何修改的東西一個版號,insert 操作也相同,而上面也有提到,這個版本號有時間順序性,所以事務 b 的 select 只會找尋版本號時間順序是在事務 b 開始前的值。
圖 8 : 幻讀情境 1 使用 mvcc 解法
這種情況比較特別,它有用到咱們之前所提到的兩個上鎖方法,這兩個特別之處在於,一但使用它,就會變成所謂的『 當前讀 』,也就是會讀取最新版號。
圖 9 這種情況下,會變成 select for update 會讀到現在的最新版本,那這是就又會出現幻讀的現像,如下。
圖 9 : 幻讀情境 2
所以這種情況 2 解法又要使用所一種鎖來解決,也就是所謂的『 next-key locking 』機制,也就是說他會上鎖範圍,如下圖 10 所示,它會上鎖 (-oo, 5),所以當你實際運行時,會發現,在執行第一個綠色 insert 時會成功,而之後的兩個卡住直到事務 b commit 才能正常的 insert。
圖 10 : 幻讀情境 2 使用 next-key 鎖
接下來說的幻讀情境目前是無解的情況,如下圖 11 所示。
問題出在於事務 b 更新時,會讓 c 那個 select 操作讀取到最新的版本,因為上面在 mvcc 有提到,當進行 update 操作時會對那行變成『 當前讀 』,也就是讀最新的資料。
圖 11 : 幻讀情境 3 無解情況
在這篇文章中,咱們學習到了 innodb 是如何的解決咱們上述文章中所提到的並行不一致問題的處理。其中並行不一致性又分以下幾個問題 :
順到說一下,你看看單機的不一致性問題就已經那麼的複雜了,分散式會變什麼樣子呢 ?
Read Committed ( RC ) : 事務總是讀取最近一次 commit 的版本 ( 接近當前讀的概念,但差別在於有沒有 commit )。
不太懂這句話的意思,commit 不就表示已經完成事務,為什麼後面又一句 差別在於有沒有 commit ?
要表達的是否為,當下事務的多筆 sql 敘述中,讀取執行 sql 敘述最新狀態,即使當下這個事務還沒 commit
不好意思 ~ 我自已也想不透我當初為啥要寫這樣 ~ 所以我決定移掉括號後面。