毫無疑問,并發控制方向的內容是我們學習的重點和難點,在一段時間的學習之后,通常會有一些挫敗感,這是一種似懂非懂的感覺,主要的原因其實細究起來理解為:使用并發時需要解決的問題有多個,而要實現并發的方案有多種,它們兩者之間沒有明顯的映射關系,如下圖所示。
接下來我們來聊一下對于并發控制的理解,首先需要明確一個問題,那就是為什么需要事務。
- 為什么需要事務
為什么需要事務,聽起來是個多余的問題,究其原因,事務處理機制,要保證用戶的數據操作對數據是“安全”的,比如我們要守護的銀行卡余額,我們希望對它的操作是穩定準確,而且絕對是安全的。
那么什么樣的操作才是安全的呢,這就引出了事務的ACID特性,ACID的解釋和說明如下所示。
ACID特性
解釋
原子性(atomicity)
一個事務要么全部執行,要么完全不執行
一致性(consistency)
事務在開始和結束時,應該始終滿足一致性約束
隔離性(isolation)
在事務操作時,其他事務的操作不能影響到當前的事務操作
持久性(durability)
事務操作的結果是具有持久性的
這個理解起來就相對簡單了,比如我去ATM機取款,要么成功,要么提示余額不足(原子性),比如我取了1000元,那么從ATM里面取出的也應該是1000元,不多不少(一致性),我取款的時候有人給我轉賬,我不應該拒絕這樣的操作(隔離性),取款完畢,我們可以打一張回執單,上面會有我們的余額(持久化),之后查多少次都不會變。
順著這個思路來看,我們把查詢余額看做是讀操作,存錢,取款看做是寫操作,很多讀寫操作的并發都相對容易理解了。
對于這樣的操作我們分為讀和寫,它有如下兩種組合:
(1)讀-讀操作
(2)讀-寫操作
其中我們經常聽到的臟讀,不可重復讀,幻讀都是在讀-寫操作中出現的概念,我們可以用下面的三句話來概括:
l 寫在前,讀在后:臟讀
l 讀在前,寫在后:不可重復讀
l 讀在前,寫在后,然后又讀:幻讀
我們可以假設生活中的幾個場景,來吃透這三種不是很容易理解的概念,我們就以購物車為例吧,故事的背景是一對情侶,某天早上女生上班前對男生說,幫我關一下電腦,男生關電腦時發現桌面首頁顯示女生的賬號登錄了一個購物網站,購物車里有一個化妝品套裝,但是還沒有下單如下圖所示:
l 1)男生說原來不是關電腦這么簡單啊,于是就默默下單提交了,這種情況就是臟讀,事務B讀到了事務A未提交的數據狀態。
l 2)男生想多大點事,一套不夠,再買一套,于是點擊添加了一套,結果女生下班后,帶著期待的心情打開購物車,發現化妝品沒變,但是數量是2套,這就是不可重復讀,重點在于修改,一個事務前后兩次讀取的結果值并不一致,導致了不可重復讀,面對的是相同的查詢數據,類似 product_code=’化妝品套裝’
l 3)男生明白了,查看了女生瀏覽的其他幾款化妝品,把它們都加入了購物車,結果女生下班后,查看購物車,發現除了之前的那款化妝品,一下子又多了好幾款其他的化妝品套裝,明明只中意了一款啊,這種情況就是幻讀,幻讀面對的是一類數據,在這里就是以購物車里的所有商品作為參考。
我們簡單總結下,不可重復讀和幻讀有些類似:一個事務多次讀取某條數據,發現讀取的數據不完全相同 ,
兩者的不同點在于,不可重復讀針對數據的修改造成的讀不一致,而幻讀針對數據的插入和刪除造成的讀不一致,如同發生幻覺一樣。
- MySQL并發控制技術方案
數據庫的一個核心方向就是并發控制了,并發是對臨界資源進行操作,通過并發控制技術來確保整個過程中對于數據的操作是“安全”的。
總體來說,有以下的兩類并發控制技術:鎖機制 (Locking)和多版本并發控制(MVCC)
(1)鎖機制 (Locking)
通過鎖機制可以保證數據一致性,整體的場景感覺無非是讀-讀,讀-寫,寫-寫這幾類并發,看起來容易,但是融合到業務場景中是千差萬別,相對是比較復雜的。
(2)多版本并發控制(MVCC)
MVCC(Multiversion Concurrency Control)是側重于讀寫并發的改善機制,它可以避免寫操作堵塞讀操作的并發問題,通過使用數據的多個版本保證并發讀寫不沖突的一種機制,它只是一種標準,并不是規定了明細的實現細節,所以在數據庫方向上大體會有一些MVCC的不同實現。
寫-寫的場景其實相對容易理解,為了保證在同一時間完成數據的一致性操作,我們需要通過鎖的方式來控制,為了方便理解,整個過程簡單理解是串行的,有一些改進的細節我們在后面會說。
這里要先引出一個概念,就是2PL(Two-Phase Locking ,二階段鎖),這個過程我們舉個例子就很容易理解了。加鎖階段只加鎖,解鎖階段只放鎖,就好像我們呼吸一樣,吸氣,呼氣,一張一弛,但是不會彼此交叉。把這個過程細化到一個數據并發中的場景:
(1) 操作數據前,加鎖,互相排斥,不允許其他并發任務操作。
(2) 操作數據后,解鎖,其他任務可以繼續執行。
這種鎖定的方式相對比較單一而且粒度太粗,這樣會導致在并發讀任務都會阻塞,對于并發的性能影響是很大的,所以InnoDB實現了兩種類型的行鎖。
l 共享鎖(S):允許一個事務去讀一行,阻止其他事務獲得相同的數據集的排他鎖。
l 排他鎖(X):允許獲得排他鎖的事務更新數據,但是組織其他事務獲得相同數據集的共享鎖和排他鎖。
簡單小結為:
l 共享鎖(S)之間不互斥,讀讀操作可以并行。
l 排它鎖(X)是互斥關系,讀寫,寫寫操作不可以并行。
一些常見的共享鎖的使用方式有:
共享鎖:
select * from table_name where .....lock in share mode
排他鎖:
select * from table_name where .....for update
通過這一層的改進,可以對于讀讀并發的場景有了較好的支撐,但是寫入的過程中,讀任務還是會被阻塞,對于讀寫的操作還是存在瓶頸,所以在這個層面上引入了MVCC,在詳細展開之前,我們需要了解下MVCC并發控制中的兩類讀操作,快照讀(Snapshot Read)和當前讀(Current Read),其中快照讀讀取的是數據的可見版本,可能是數據的歷史鏡像,這個過程是不加鎖的,而當前讀讀取的是最新的版本,會加上鎖,保證其他事務不會再修改這條記錄。
比如我們觸發了一條select操作: select * from test where id=100; id為主鍵,這條語句對應的操作就是快照讀,而我們上面剛剛列舉的共享鎖和排它鎖的SQL還有常見的DML都屬于當前讀,操作過程中會讀取當前最新的版本,保證其他事務不能修改當前記錄。
我們通過思維導圖的形式簡單對并發控制技術做個總結,如下圖所示。