MySQL系列(二):MySQL是怎么處理并發(fā)操作的?

作者:z小趙
★一枚用心堅持寫原創(chuàng)的“無趣”程序猿,在自身受益的同時也讓朋友們在技術(shù)上有所提升。
目錄
為什么需要鎖? MySQL 中鎖分類? 什么是事務(wù)? 事務(wù)的隔離級別 MySQL 是怎么實現(xiàn)事務(wù)機(jī)制的? MVCC 機(jī)制 總結(jié)
為什么需要鎖?
相信大家都比較熟悉電商系統(tǒng)中庫存管理的場景,對于日?;顒哟黉N、618、雙 11 等場景,會在規(guī)定時間內(nèi)對商品進(jìn)行促銷活動,假設(shè)現(xiàn)在有一款 HHKB 機(jī)械鍵盤要參與促銷活動,數(shù)據(jù)庫中準(zhǔn)備了 10 件,促銷活動開始時,多位買家開始爭搶,每賣出一件商品,庫存減 1,直到賣完,那么怎么能保證商品不會賣超呢?
對于以上這個場景來說,我們需要用到鎖機(jī)制來保證每賣出一件商品,對庫存進(jìn)行更新操作時,其他用戶請求不能對該商品庫存進(jìn)行修改;換句話說,用戶 1 拿到了修改庫存的鎖,則只有用戶 1 能修改數(shù)據(jù),而用戶 2 只能等著不能修改數(shù)據(jù)。如下圖所示:
相反,如果沒有鎖的加持,用戶 1 和用戶 2 發(fā)現(xiàn)庫存還有 1 件商品,同時都開始下單,用戶 1 先將庫存更新為 0,此時商品已經(jīng)售完,而用戶 2 也將庫存更新為 0,就導(dǎo)致了賣超的尷尬情況。
MySQL 中鎖分類?
鎖根據(jù)使用場景不同,被分成了各種各樣的鎖。比如讀寫可以分為讀鎖和寫鎖,對于讀請求之間相互是互不影響的,因為數(shù)據(jù)沒有被所有,大家讀取到的數(shù)據(jù)都是一樣的,所以讀鎖也稱之為共享鎖;對于寫請求,由于存在數(shù)據(jù)的變更,所以請求之間是互斥的,所以也稱之為排它鎖。
對于根據(jù)鎖鎖定的范圍大小,可以分為全局鎖、表鎖、元數(shù)據(jù)鎖、行鎖:
全局鎖:顧名思義就是對整個數(shù)據(jù)庫進(jìn)行加鎖操作,加鎖期間,整個數(shù)據(jù)庫只能夠進(jìn)行讀操作。 表鎖:是對數(shù)據(jù)庫中的某張表進(jìn)行加鎖,此時表與表之間可以同時進(jìn)行寫操作而互不影響,但是同一時刻同一張表只能有一個寫操作。 頁面鎖:頁面鎖是介于表鎖和行鎖之間的一種鎖,其優(yōu)勢是中和表鎖和行鎖的鎖開銷。 行鎖:是對數(shù)據(jù)庫表中的某一行進(jìn)行加鎖操作。
從上也能夠看出,鎖的范圍是逐漸減小的,在實際生產(chǎn)環(huán)境中需要根據(jù)業(yè)務(wù)場景來選擇不同粒度的鎖。
什么是事務(wù)?
由多個事件組成的一組動作,要么同時成功,要么發(fā)生失敗時全體進(jìn)行回滾;換句話說就是多個原子操作合并為一個原子操作。怎么能夠保證一個事務(wù)能夠正確執(zhí)行呢?想要保證一個事務(wù)正確執(zhí)行的依據(jù)是其必須滿足 ACID,即原子性(atomicity)、一致性(consistency)、隔離性(isolation)、持久性(durability)。
舉個例子:初始狀態(tài) A 賬戶有 100 塊,B 賬戶有 100 塊。
原子性:一個事務(wù)或執(zhí)行動作不能被分割為多個階段去處理,所以整個執(zhí)行流程要么全部成功,要么失敗全部回滾。舉例:A 向 B 轉(zhuǎn)賬 100 塊,A 的賬戶變成 0,B 的賬戶變成 200,兩個賬戶的錢增減整體視為一個動作。 一致性:一個狀態(tài)在經(jīng)歷一些動作之后轉(zhuǎn)變成另外一個狀態(tài)。舉例:A 向 B 轉(zhuǎn)賬 100 塊,經(jīng)過 A 向 B 的一個轉(zhuǎn)賬動作并且執(zhí)行成功時,A 的賬戶變成 0,B 的賬戶變成 200 塊,執(zhí)行轉(zhuǎn)賬動作前后賬戶的總金額沒有發(fā)生改變。 隔離性:多個事務(wù)在并發(fā)操作時,相互之間不能夠看到對方執(zhí)行的動作,即相互之間是隔離的。隔離也分為多個級別,不同的隔離級別所保證的隔離程度也是不一樣的。 持久性:一個事務(wù)一旦提交以后,其對數(shù)據(jù)源的修改是永久性的保存到了數(shù)據(jù)庫中。如何做到 100%的持久性是一個幾乎不太可能實現(xiàn)的事情,比如數(shù)據(jù)雖然持久化到了磁盤上,但由于一些不可抗拒的外力因素導(dǎo)致數(shù)據(jù)發(fā)生了丟失,所以在實際情況中需要規(guī)定一個持久性的級別,即認(rèn)為只需要數(shù)據(jù)持久化到磁盤上就可以認(rèn)為達(dá)到了持久性的特性。
事務(wù)的隔離級別
MySQL 的事務(wù)隔離級別分為一下幾種:
舉個例子:初始狀態(tài) A 賬戶有 100 塊,B 賬戶有 100 塊。
讀未提交(Read Uncommitted):在一個事務(wù)中,部分提交后對其他事務(wù)可見。舉例:A 轉(zhuǎn)賬給 B,A 的賬戶金額變成 0,B 的賬戶還未增加到 200,A 讀取自己的賬戶時,發(fā)現(xiàn)自己的賬戶金額變?yōu)?0,但是由于事務(wù)執(zhí)行中途出現(xiàn)故障(假設(shè) B 賬戶因為某種原因賬戶被鎖定不能被轉(zhuǎn)賬),此時事務(wù)進(jìn)行了回滾操作,那么就會導(dǎo)致 A 讀取到賬戶金額是錯誤的。這種現(xiàn)象也稱之為臟讀。 讀已提交(Read Committed):讀操作只能夠讀取到自己事務(wù)中的數(shù)據(jù)或者是事務(wù)提交后的結(jié)果。舉例:轉(zhuǎn)賬操作,如果一個請求讀取賬戶 A 的金額,如果事務(wù)正常提交了,則其讀取的賬戶金額一定是 0,如果事務(wù)回滾,則讀取到的金額一定是 100。但是讀已提交不能夠避免重復(fù)讀,有可能兩次讀取到結(jié)果不一致。 可重復(fù)讀(Repeatable Read):該級別能夠保證多次讀取到的結(jié)果是相同的,但是不能夠解決幻讀的情況?;米x在數(shù)據(jù)庫中具體的體現(xiàn)是范圍查詢,比如第一次一個范圍查詢到的結(jié)果集的同時,另外一個事務(wù)插入了數(shù)據(jù),第二次查詢的結(jié)果和第一次查詢的結(jié)果集不一樣。 串行化(Serializable):強(qiáng)制所有事務(wù)按照順序依次執(zhí)行,只有一個事務(wù)執(zhí)行完成后,下一個事務(wù)才能接著執(zhí)行。這樣就能夠避免產(chǎn)生臟讀、不可重復(fù)讀、幻讀等情況,但是同時也降低了數(shù)據(jù)庫的并發(fā)度。
在實際開發(fā)場景中,同樣需要根據(jù)實際業(yè)務(wù)場景來選擇合適的隔離級別,一般用的比較普遍的兩種隔離級別是讀已提交和可重復(fù)讀。
MySQL 是怎么實現(xiàn)事務(wù)機(jī)制的?
MySQL 中實現(xiàn)了事務(wù)機(jī)制的常見存儲引擎有 InnoDB 和 NDB(上篇文章也介紹過,本系列全程以 InnoDB 展開介紹)。使用 InnoDB 提交事務(wù)的時候,首先需要關(guān)閉自動提交,通過 set autocommit = 0 命令關(guān)閉自動提交功能,然后通過 start transaction 開啟一個事務(wù),接著編寫在事務(wù)內(nèi)要執(zhí)行的 SQL,最后通過 commit 提交事務(wù)。
如果兩個事務(wù)同時提交,并且都需要操作同一個資源,此時會產(chǎn)生 死鎖,那么 MySQL 是怎么處理這樣情況的呢?InnoDB 采用的是將持有最少行級排他鎖進(jìn)行回滾操作,什么叫持有最少行級排排它鎖?大白話解釋一下:就是每個事務(wù)開啟后,需要執(zhí)行增刪改等操作 SQL 的個數(shù)。比如有兩個事務(wù),一個需要執(zhí)行 2 個 select 語句和 3 個 update 語句,另外一個需要執(zhí)行 1 個 select 語句和 1 個 update 語句,當(dāng)兩個事務(wù)的 update 同一時刻需要對方鎖住的資源時,會將后者的事務(wù)進(jìn)行回滾操作。
MVCC 機(jī)制
為了降低死鎖情況的發(fā)生,MySQL 引入錄入 MVCC 機(jī)制,從而來進(jìn)一步降低因為加減鎖而造成的系統(tǒng)開銷。通過在數(shù)據(jù)表上增加兩個隱藏列,一個列用于存儲當(dāng)前行的創(chuàng)建時的版本,另外一個列用于存儲當(dāng)前行的刪除時的版本。下面我們來看看 MVCC 機(jī)制在 CRUD 中是怎么工作的。
SELECT 操作(由兩個條件決定): InnoDB 會查找創(chuàng)建行的版本小于等于當(dāng)前事務(wù)版本的數(shù)據(jù)行。比如目前數(shù)據(jù)表里的第一行現(xiàn)在有兩個版本,分別為 1 和 2,而當(dāng)前事務(wù)的版本號是 1,則此時會選擇版本號為 1 的這一行. InnoDB 會查找刪除行的版本大于當(dāng)前事務(wù)版本的數(shù)據(jù)行。比如目前數(shù)據(jù)表里的第一行有兩個版本,分別為 1 和 2,但是版本號為 1 的那行的刪除列標(biāo)記為 0,而當(dāng)前書屋的版本號為 1,則此時會選擇版本號為 1 的這一行。
當(dāng)同時滿足以上兩個條件的時候,select 語句就可以得到一條最新的已提交的且未被刪除的行記錄。
UPDATE 操作:新插入一條數(shù)據(jù)到表中,并且將創(chuàng)建列的版本號設(shè)置為當(dāng)前事務(wù)的版本號,并且將當(dāng)前事務(wù)的版本號保存到原來記錄的刪除列上。
INSERT 操作:新插入一條數(shù)據(jù)到表中,并且將創(chuàng)建列的版本號設(shè)置為當(dāng)前的事務(wù)版本號。
DELETE 操作:會將要刪除的行的歷史所有版本號全部設(shè)置上當(dāng)前的事務(wù)版本號。
通過 CRUD 四種操作可以看出,MVCC 機(jī)制只能作用于 讀已提交 和 可重復(fù)讀 兩個事務(wù)隔離級別下,因為讀未提交會使得 SELECT 產(chǎn)生臟讀,而串行化本身已經(jīng)是順序操作了,沒有增加 MVCC 機(jī)制的必要。
至于 MVCC 的具體實現(xiàn)細(xì)節(jié),它結(jié)合了 undo log 來實現(xiàn)的,我們在后面講解 MySQL 的相關(guān)日志文件功能的時候再詳細(xì)展開。
總結(jié)
本文介紹了 MySQL 的鎖機(jī)制和事務(wù)的概念及實現(xiàn)原理。下篇文章我們來接著研究MySQL 的索引機(jī)制,看看它到底該怎么用才能使得查詢操作變得更加高效,敬請期待。
歡迎關(guān)注微信公眾號:互聯(lián)網(wǎng)全棧架構(gòu),收取更多有價值的信息。
