1、事務(wù)及其特性
首先看看什么是事務(wù)?事務(wù)具有哪些特性?
簡單來說,事務(wù)是指作為單個邏輯工作單元執(zhí)行的一系列操作,這些操作要么全做,要么全不做,是一個不可分割的工作單元。
一個邏輯工作單元要成為事務(wù),在關(guān)系型數(shù)據(jù)庫管理系統(tǒng)中,必須滿足 4 個特性,即所謂的 ACID:原子性、一致性、隔離性和持久性。
ACID 及它們之間的關(guān)系如下圖所示,比如 4 個特性中有 3 個與 WAL 有關(guān)系,都需要通過 Redo、Undo 日志來保證等。
- 一致性(consistency):事務(wù)開始之前和事務(wù)結(jié)束之后,數(shù)據(jù)庫的完整性限制未被破壞。
一致性其實(shí)包括兩部分內(nèi)容,分別是約束一致性和數(shù)據(jù)一致性。
(1)約束一致性:數(shù)據(jù)庫中創(chuàng)建表結(jié)構(gòu)時(shí)所指定的外鍵、Check、唯一索引等約束。可惜在 MySQL 中,是不支持 Check 的,只支持另外兩種,所以約束一致性就非常容易理解了。
(2)數(shù)據(jù)一致性:是一個綜合性的規(guī)定,或者說是一個把握全局的規(guī)定。它是由原子性、持久性、隔離性共同保證的結(jié)果,而不是單單依賴于某一種技術(shù)。
- 原子性(atomicity):事務(wù)的所有操作,要么全部完成,要么全部不完成,不會結(jié)束在某個中間環(huán)節(jié)。
原子性也就是說用戶感受不到一個正在改的狀態(tài)。MySQL 是通過 WAL(Write Ahead Log)技術(shù)來實(shí)現(xiàn)這種效果的。
原子性和 WAL 到底有什么關(guān)系呢?其實(shí)關(guān)系非常大。舉例來講,如果事務(wù)提交了,那改了的數(shù)據(jù)就生效了,如果此時(shí) Buffer Pool 的臟頁沒有刷盤,如何來保證改了的數(shù)據(jù)生效呢?就需要使用 Redo 日志恢復(fù)出來的數(shù)據(jù)。而如果事務(wù)沒有提交,且 Buffer Pool 的臟頁被刷盤了,那這個本不應(yīng)該存在的數(shù)據(jù)如何消失呢?就需要通過 Undo 來實(shí)現(xiàn)了,Undo 又是通過 Redo 來保證的,所以最終原子性的保證還是靠 Redo 的 WAL 機(jī)制實(shí)現(xiàn)的。
- 持久性(durability):事務(wù)完成之后,事務(wù)所做的修改進(jìn)行持久化保存,不會丟失。
所謂持久性,就是指一個事務(wù)一旦提交,它對數(shù)據(jù)庫中數(shù)據(jù)的改變就應(yīng)該是永久性的,接下來的操作或故障不應(yīng)該對其有任何影響。前面已經(jīng)講到,事務(wù)的原子性可以保證一個事務(wù)要么全執(zhí)行,要么全不執(zhí)行的特性,這可以從邏輯上保證用戶看不到中間的狀態(tài)。但持久性是如何保證的呢?一旦事務(wù)提交,通過原子性,即便是遇到宕機(jī),也可以從邏輯上將數(shù)據(jù)找回來后再次寫入物理存儲空間,這樣就從邏輯和物理兩個方面保證了數(shù)據(jù)不會丟失,即保證了數(shù)據(jù)庫的持久性。
- 隔離性(isolation):當(dāng)多個事務(wù)并發(fā)訪問數(shù)據(jù)庫中的同一數(shù)據(jù)時(shí),所表現(xiàn)出來的相互關(guān)系。
所謂隔離性,指的是一個事務(wù)的執(zhí)行不能被其他事務(wù)干擾,即一個事務(wù)內(nèi)部的操作及使用的數(shù)據(jù)對其他的并發(fā)事務(wù)是隔離的。鎖和多版本控制就符合隔離性。
InnoDB 支持的隔離性有 4 種,隔離性從低到高分別為:讀未提交、讀提交、可重復(fù)讀、可串行化。
(1)讀未提交(RU,Read Uncommitted)。它能讀到一個事務(wù)的中間過程,違背了 ACID 特性,存在臟讀的問題,所以基本不會用到,可以忽略。
(2)讀提交(RC,Read Committed)。它表示如果其他事務(wù)已經(jīng)提交,那么我們就可以看到,這也是一種最普遍適用的級別。但由于一些歷史原因,可能 RC 在生產(chǎn)環(huán)境中用的并不多。
(3)可重復(fù)讀(RR,Repeatable Read),是目前被使用得最多的一種級別。其特點(diǎn)是有 Gap 鎖、目前還是默認(rèn)的級別、在這種級別下會經(jīng)常發(fā)生死鎖、低并發(fā)等問題。
(4)可串行化,這種實(shí)現(xiàn)方式,其實(shí)已經(jīng)并不是多版本了,又回到了單版本的狀態(tài),因?yàn)樗械膶?shí)現(xiàn)都是通過鎖來實(shí)現(xiàn)的。
2、并發(fā)事務(wù)控制
2.1 單版本控制-鎖
先來看鎖,鎖用獨(dú)占的方式來保證在只有一個版本的情況下事務(wù)之間相互隔離,所以鎖可以理解為單版本控制。
在 MySQL 事務(wù)中,鎖的實(shí)現(xiàn)與隔離級別有關(guān)系,在 RR(Repeatable Read)隔離級別下,MySQL 為了解決幻讀的問題,以犧牲并行度為代價(jià),通過 Gap 鎖來防止數(shù)據(jù)的寫入,而這種鎖,因?yàn)槠洳⑿卸炔粔颍瑳_突很多,經(jīng)常會引起死鎖。現(xiàn)在流行的 Row 模式可以避免很多沖突甚至死鎖問題,所以推薦默認(rèn)使用 Row + RC(Read Committed)模式的隔離級別,可以很大程度上提高數(shù)據(jù)庫的讀寫并行度。
2.2 多版本控制-MVCC
多版本控制也叫作 MVCC,是指在數(shù)據(jù)庫中,為了實(shí)現(xiàn)高并發(fā)的數(shù)據(jù)訪問,對數(shù)據(jù)進(jìn)行多版本處理,并通過事務(wù)的可見性來保證事務(wù)能看到自己應(yīng)該看到的數(shù)據(jù)版本。
那個多版本是如何生成的呢?每一次對數(shù)據(jù)庫的修改,都會在 Undo 日志中記錄當(dāng)前修改記錄的事務(wù)號及修改前數(shù)據(jù)狀態(tài)的存儲地址(即 ROLL_PTR),以便在必要的時(shí)候可以回滾到老的數(shù)據(jù)版本。例如,一個讀事務(wù)查詢到當(dāng)前記錄,而最新的事務(wù)還未提交,根據(jù)原子性,讀事務(wù)看不到最新數(shù)據(jù),但可以去回滾段中找到老版本的數(shù)據(jù),這樣就生成了多個版本。
多版本控制很巧妙地將稀缺資源的獨(dú)占互斥轉(zhuǎn)換為并發(fā),大大提高了數(shù)據(jù)庫的吞吐量及讀寫性能。
2.3 MVCC 實(shí)現(xiàn)原理
MySQL InnoDB 存儲引擎,實(shí)現(xiàn)的是基于多版本的并發(fā)控制協(xié)議——MVCC,而不是基于鎖的并發(fā)控制。
MVCC 最大的好處是讀不加鎖,讀寫不沖突。在讀多寫少的 OLTP(On-Line Transaction Processing)應(yīng)用中,讀寫不沖突是非常重要的,極大的提高了系統(tǒng)的并發(fā)性能,這也是為什么現(xiàn)階段幾乎所有的 RDBMS(Relational Database Management System),都支持 MVCC 的原因。
2.4 快照讀與當(dāng)前讀
在 MVCC 并發(fā)控制中,讀操作可以分為兩類: 快照讀(Snapshot Read)與當(dāng)前讀 (Current Read)。
快照讀:讀取的是記錄的可見版本(有可能是歷史版本),不用加鎖。
當(dāng)前讀:讀取的是記錄的最新版本,并且當(dāng)前讀返回的記錄,都會加鎖,保證其他事務(wù)不會再并發(fā)修改這條記錄。
注意:MVCC 只在 Read Commited 和 Repeatable Read 兩種隔離級別下工作。
如何區(qū)分快照讀和當(dāng)前讀呢? 可以簡單的理解為:
快照讀:簡單的 select 操作,屬于快照讀,不需要加鎖。
當(dāng)前讀:特殊的讀操作,插入/更新/刪除操作,屬于當(dāng)前讀,需要加鎖。
3、并發(fā)事務(wù)問題及解決方案
并發(fā)事務(wù)處理也會帶來一些問題,如:臟讀、不可重復(fù)讀、幻讀。
臟讀:
一個事務(wù)正在對一條記錄做修改,在這個事務(wù)完成并提交前,這條記錄的數(shù)據(jù)就處于不一致狀態(tài);這時(shí),另一個事務(wù)也來讀取同一條記錄,如果不加控制,第二個事務(wù)讀取了這些“臟”數(shù)據(jù),并據(jù)此做進(jìn)一步的處理,就會產(chǎn)生未提交的數(shù)據(jù)依賴關(guān)系。這種現(xiàn)象被形象的叫作"臟讀"(Dirty Reads)。不可重復(fù)讀:
一個事務(wù)在讀取某些數(shù)據(jù)后的某個時(shí)間,再次讀取以前讀過的數(shù)據(jù),卻發(fā)現(xiàn)其讀出的數(shù)據(jù)已經(jīng)發(fā)生了改變、或某些記錄已經(jīng)被刪除了!這種現(xiàn)象就叫作“ 不可重復(fù)讀”(Non-Repeatable Reads)。幻讀:
一個事務(wù)按相同的查詢條件重新讀取以前檢索過的數(shù)據(jù),卻發(fā)現(xiàn)其他事務(wù)插入了滿足其查詢條件的新數(shù)據(jù),這種現(xiàn)象就稱為“幻讀”(Phantom Reads)。
解決方案:
產(chǎn)生的這些問題,MySQL 數(shù)據(jù)庫是通過事務(wù)隔離級別來解決的,如下圖所示:
4、MySQL 鎖分類
在 MySQL 中有三種級別的鎖:頁級鎖、表級鎖、行級鎖。
表級鎖:開銷小,加鎖快;不會出現(xiàn)死鎖;鎖定粒度大,發(fā)生鎖沖突的概率最高,并發(fā)度最低。 會發(fā)生在:MyISAM、memory、InnoDB、BDB 等存儲引擎中。
注意:MySQL 中的表鎖包括讀鎖和寫鎖。行級鎖:開銷大,加鎖慢;會出現(xiàn)死鎖;鎖定粒度最小,發(fā)生鎖沖突的概率最低,并發(fā)度最高。會發(fā)生在:InnoDB 存儲引擎。
頁級鎖:開銷和加鎖時(shí)間界于表鎖和行鎖之間;會出現(xiàn)死鎖;鎖定粒度界于表鎖和行鎖之間,并發(fā)度一般。會發(fā)生在:BDB 存儲引擎。
三種級別的鎖分別對應(yīng)存儲引擎關(guān)系如下圖所示:
4.1 InnoDB 鎖分類及問題
共享鎖(S),也叫讀鎖:允許一個事務(wù)去讀一行,阻止其他事務(wù)獲得相同數(shù)據(jù)集的排他鎖。
排他鎖(X),也叫寫鎖:允許獲得排他鎖的事務(wù)更新數(shù)據(jù),阻止其他事務(wù)取得相同數(shù)據(jù)集的共享讀鎖和排他寫鎖。
另外,為了允許行鎖和表鎖共存,實(shí)現(xiàn)多粒度鎖機(jī)制,InnoDB 還有兩種內(nèi)部使用的意向鎖(Intention Locks),這兩種意向鎖都是表鎖。表鎖又分為三種。
意向共享鎖(IS):事務(wù)計(jì)劃給數(shù)據(jù)行加行共享鎖,事務(wù)在給一個數(shù)據(jù)行加共享鎖前必須先取得該表的 IS 鎖。
意向排他鎖(IX):事務(wù)打算給數(shù)據(jù)行加行排他鎖,事務(wù)在給一個數(shù)據(jù)行加排他鎖前必須先取得該表的 IX 鎖。
自增鎖(AUTO-INC Locks):特殊表鎖,自增長計(jì)數(shù)器通過該“鎖”來獲得子增長計(jì)數(shù)器最大的計(jì)數(shù)值。
在加行鎖之前必須先獲得表級意向鎖,否則等待 innodb_lock_wait_timeout 超時(shí)后根據(jù)innodb_rollback_on_timeout 決定是否回滾事務(wù)。
(1)InnoDB 自增鎖:
在 MySQL InnoDB 存儲引擎中,在設(shè)計(jì)表結(jié)構(gòu)的時(shí)候,通常會建議添加一列作為自增主鍵。這里就會涉及一個特殊的鎖:自增鎖。
(2)InnoDB 行鎖實(shí)現(xiàn)算法:
InnoDB 行鎖是通過對索引數(shù)據(jù)頁上的記錄(record)加鎖實(shí)現(xiàn)的。主要實(shí)現(xiàn)算法有 3 種:Record Lock、Gap Lock 和 Next-key Lock。
(3)排查 InnoDB 鎖問題:
(4)InnoDB 加鎖行為:
下面舉一些例子分析 InnoDB 不同索引的加鎖行為。分析鎖時(shí)需要跟隔離級別聯(lián)系起來,以 RR 為例,主要是從四個場景分析。
- 主鍵 + RR:
- 唯一鍵 + RR:
- 非唯一鍵 + RR:
- 無索引 + RR:
(5)InnoDB 死鎖:
在 MySQL 中死鎖不會發(fā)生在 MyISAM 存儲引擎中,但會發(fā)生在 InnoDB 存儲引擎中,因?yàn)?InnoDB 是逐行加鎖的,極容易產(chǎn)生死鎖。那么死鎖產(chǎn)生的四個條件是什么呢?
在發(fā)生死鎖時(shí),InnoDB 存儲引擎會自動檢測,并且會自動回滾代價(jià)較小的事務(wù)來解決死鎖問題。但很多時(shí)候一旦發(fā)生死鎖,InnoDB 存儲引擎的處理的效率是很低下的或者有時(shí)候根本解決不了問題,需要人為手動去解決。
既然死鎖問題會導(dǎo)致嚴(yán)重的后果,那么在開發(fā)或者使用數(shù)據(jù)庫的過程中,如何避免死鎖的產(chǎn)生呢?這里給出一些建議:
給大家一些開發(fā)建議來避免線上業(yè)務(wù)因死鎖造成的不必要的影響。