Innodb實踐總結(一)
1. MySQL鎖概述
相對其他資料庫而言,MySQL的鎖機制比較簡單,其最 顯著的特點是不同的儲存引擎支援不同的鎖機制。比如,MyISAM和MEMORY儲存引擎採用的是表級鎖(table-level locking);BDB儲存引擎採用的是頁面鎖(page-level locking),但也支援表級鎖;InnoDB儲存引擎既支援行級鎖(row-level locking),也支援表級鎖,但預設情況下是採用行級鎖。
表級鎖:開銷小,加鎖快;不會出現死鎖;鎖定粒度大,發生鎖衝突的概率最高,併發度最低。
行級鎖:開銷大,加鎖慢;會出現死鎖;鎖定粒度最小,發生鎖衝突的概率最低,併發度也最高。
頁面鎖:開銷和加鎖時間界於表鎖和行鎖之間;會出現死鎖;鎖定粒度界於表鎖和行鎖之間,併發度一般
從上述特點可見,很難籠統地說哪種鎖更好,只能就具體應用的特點來說哪種鎖更合適!僅從鎖的角度 來說:表級鎖更適合於以查詢為主,只有少量按索引條件更新資料的應用,如Web應用;而行級鎖則更適合於有大量按索引條件併發更新少量不同資料,同時又有 併發查詢的應用,如一些線上事務處理(OLTP)
InnoDB與MyISAM的最大不同有兩點:一是支援事務(TRANSACTION);二是採用了行級鎖。
2. Innodb的索引
2.1 Innodb索引
InnoDB 使用B+Tree作為索引結構,也別需要注意的是,對於主鍵索引,InnoDB 使用聚集索引,InnoDB的資料檔案本身就是就是索引檔案。而MyISAM,主鍵索引和資料檔案是分離的。
InnoDB資料檔案,要按主鍵聚集索引,這就要求InnoDB的表必須要有主鍵(MyISAM可以沒有)。如果沒有顯式指定主鍵,InnoDB會自動選擇一個可以唯一標識記錄的欄位作為主鍵,比如auto_increment的欄位,如果不存在這樣的列,InnoDB會自動生成一個隱含欄位作為主鍵,這個隱含欄位6個位元組,是長整形。
對於InnoDB的輔助索引,葉子節點的data存放的是主鍵的值。這就意味著,使用輔助索引定位記錄,需要使用兩次索引:首先使用輔助索引找到主鍵的值,根據主鍵的值,使用主鍵索引找到記錄。
InnoDB的輔助索引為什麼要設計成二級索引(包含主鍵值而非記錄地址)? 如果輔助索引data存放的行指標,當行移動或者資料頁分裂時,需要更新data域行指標的值,這就增加維護成本。data存在主鍵的值,就沒有這個問題。行移動和資料頁分裂,主鍵索引會自動更新。data關聯主鍵的值,不需要更新,相當於增加一個間接層。這個間接層對效能的影響也很小,因為通過主鍵定位記錄是非常快的。
2.2 為什麼建立索引
建立索引可以大大提高系統的效能。
通過建立唯一性索引,可以保證資料庫表中每一行資料的唯一性。
可以大大加快資料的檢索速度,這也是建立索引的最主要的原因。
可以加速表和表之間的連線,特別是在實現資料的參考完整性方面特別有意義。
在使用分組和排序子句進行資料檢索時,同樣可以顯著減少查詢中分組和排序的時間。
2.3 索引帶來的問題
建立索引和維護索引要耗費時間,這種時間隨著資料量的增加而增加。
索引需要佔物理空間,除了資料表佔資料空間之外,每一個索引還要佔一定的物理空間,如果要建立聚簇索引,那麼需要的空間就會更大。
2.4 哪些適合加索引
在經常需要搜尋的列上,可以加快搜索的速度;
在作為主鍵的列上,強制該列的唯一性和組織表中資料的排列結構;
在經常用在連線的列上,這些列主要是一些外來鍵, 可以加快連線的速度;
在經常需要根據範圍進行搜尋的列上建立索引,因為索引已經排序,其指定的範圍是連續的;
在經常需要排序的列上建立索引,因為索引已經排序,這樣查詢可以利用索引的排序,加快排序查詢時間;
2.5 哪些不適合加索引
對於那些在查詢中很少使用或者參考的列不應該建立索引。這是因為,既然這些列很少使用到,因此有索引或者無索引,並不能提高查詢速度。相反,由於增加了索引,反而降低了系統的維護速度和增大了空間需求。
對於那些只有很少資料值的列也不應該增加索引。這是因為,由於這些列的取值很少,例如人事表的性別列,在查詢的結果中,結果集的資料行佔了表中資料行的很大比例,即需要在表中搜索的資料行的比例很大。增加索引,並不能明顯加快檢索速度。
對於那些定義為text, image和bit資料型別的列不應該增加索引。這是因為,這些列的資料量要麼相當大,要麼取值很少。
3. 事務的4個特性
原子性(Atomic):事務必須是原子工作單元;對於其資料修改,要麼全都執行,要麼全都不執行。通常,與某個事務關聯的操作具有共同的目標,並且是相互依賴的。如果系統只執行這些操作的一個子集,則可能會破壞事務的總體目標。原子性消除了系統處理操作子集的可能性。
一致性(Consistency):事務的一致性指的是在一個事務執行之前和執行之後資料庫都必須處於一致性狀態。這種特性稱為事務的一致性。假如資料庫的狀態滿足所有的完整性約束,就說該資料庫是一致的。
隔離性(Isolation):由併發事務所作的修改必須與任何其它併發事務所作的修改隔離。事務檢視資料時資料所處的狀態,到底是另一個事務執行之前的狀態還是中間某個狀態,相互之間存在什麼影響,是可以通過隔離級別的設定來控制的。
4. 事務併發控制
第一類丟失更新(Update Lost):此種更新丟失是因為回滾的原因,所以也叫回滾丟失。此時兩個事務同時更新count,兩個事務都讀取到100,事務一更新成功並提交,count=100+1=101,事務二出於某種原因更新失敗了,然後回滾,事務二就把count還原為它一開始讀到的100,此時事務一的更新就這樣丟失了。
髒讀(Dirty Read):此種異常是因為一個事務讀取了另一個事務修改了但是未提交的資料。舉個例子,事務一更新了count=101,但是沒有提交,事務二此時讀取count,值為101而不是100,然後事務一出於某種原因回滾了,然後第二個事務讀取的這個值就是噩夢的開始。
不可重複讀(Not Repeatable Read):此種異常是一個事務對同一行資料執行了兩次或更多次查詢,但是卻得到了不同的結果,也就是在一個事務裡面你不能重複(即多次)讀取一行資料,如果你這麼做了,不能保證每次讀取的結果是一樣的,有可能一樣有可能不一樣。造成這個結果是在兩次查詢之間有別的事務對該行資料做了更新操作。舉個例子,事務一先查詢了count,值為100,此時事務二更新了count=101,事務一再次讀取count,值就會變成101,兩次讀取結果不一樣。
第二類丟失更新(Second Update Lost):此種更新丟失是因為更新被其他事務給覆蓋了,也可以叫覆蓋丟失。舉個例子,兩個事務同時更新count,都讀取100這個初始值,事務一先更新成功並提交,count=100+1=101,事務二後更新成功並提交,count=100+1=101,由於事務二count還是從100開始增加,事務一的更新就這樣丟失了。
5. 資料庫事務隔離級別
讀未提交(Read Uncommitted):該隔離級別指即使一個事務的更新語句沒有提交,但是別的事務可以讀到這個改變,幾種異常情況都可能出現。極易出錯,沒有安全性可言,基本不會使用。
讀已提交(Read Committed):該隔離級別指一個事務只能看到其他事務的已經提交的更新,看不到未提交的更新,消除了髒讀和第一類丟失更新,這是大多數資料庫的預設隔離級別,如Oracle,Sqlserver。
可重複讀(Repeatable Read):該隔離級別指一個事務中進行兩次或多次同樣的對於資料內容的查詢,得到的結果是一樣的,但不保證對於資料條數的查詢是一樣的,只要存在讀改行資料就禁止寫,消除了不可重複讀和第二類更新丟失,這是Mysql資料庫的預設隔離級別。
序列化(Serializable):意思是說這個事務執行的時候不允許別的事務併發執行.完全序列化的讀,只要存在讀就禁止寫,但可以同時讀,消除了幻讀。這是事務隔離的最高級別,雖然最安全最省心,但是效率太低,一般不會用。
“髒讀”、“不可重複讀”和“幻讀”,其實都是資料庫讀一致性問題,必須由資料庫提供一定的事務隔離機制來解決。資料庫實現事務隔離的方式,基本可以分為以下兩種。
一種是在讀取資料前,對其加鎖,阻止其他事務對資料進行修改。
在MVCC併發控制中,讀操作可以分成兩類:快照讀 (snapshot read)與當前讀 (current read)。快照讀,讀取的是記錄的可見版本 (有可能是歷史版本),不用加鎖。當前讀,讀取的是記錄的最新版本,並且,當前讀返回的記錄,都會加上鎖,保證其他事務不會再併發修改這條記錄。
在一個支援MVCC併發控制的系統中,哪些讀操作是快照讀?哪些操作又是當前讀呢?以MySQL InnoDB為例:
- 快照讀:簡單的select操作,屬於快照讀,不加鎖。(當然,也有例外)
select * from table where ?;
- 當前讀:特殊的讀操作,插入/更新/刪除操作,屬於當前讀,需要加鎖。 下面語句都屬於當前讀,讀取記錄的最新版本。並且,讀取之後,還需要保證其他併發事務不能修改當前記錄,對讀取記錄加鎖。其中,除了第一條語句,對讀取記錄加S鎖 (共享鎖)外,其他的操作,都加的是X鎖 (排它鎖)。
select * from table where ? lock in share mode;
select * from table where ? for update;
insert into table values (…);
update table set ? where ?;
delete from table where ?;
6. 資料庫鎖分類
一般可以分為兩類,一個是悲觀鎖,一個是樂觀鎖,悲觀鎖一般就是我們通常說的資料庫鎖機制,樂觀鎖一般是指使用者自己實現的一種鎖機制。
悲觀鎖:顧名思義,就是很悲觀,它對於資料被外界修改持保守態度,認為資料隨時會修改,所以整個資料處理中需要將資料加鎖。悲觀鎖一般都是依靠關係資料庫提供的鎖機制,事實上關係資料庫中的行鎖,表鎖不論是讀寫鎖都是悲觀鎖。
6.1 悲觀鎖按照鎖的型別
關於innodb的鎖型別總共可以分為四種,包含了行鎖和表鎖,分別是基本鎖(Shared Locks:S鎖和排它鎖(Exclusive Locks:X鎖))、意向鎖(intention lock,分為意向共享鎖(IS鎖)和意向排他鎖(IX鎖))、行鎖(record Locks、gap locks、next-key locks、Insert Intention Locks)、自增鎖(auto-inc locks)
共享鎖、意向鎖、排他鎖的相容矩陣如下:
X | IX | S | IS | |
---|---|---|---|---|
X | 衝突 | 衝突 | 衝突 | 衝突 |
IX | 衝突 | 相容 | 衝突 | 相容 |
S | 衝突 | 衝突 | 相容 | 相容 |
IS | 衝突 | 相容 | 相容 | 相容 |
間隙鎖
間隙鎖是在索引記錄之間的間隙上的鎖定,或在最後一個索引記錄之前或之後的間隙上的鎖定。
間隙可能跨越單個索引值,多個索引值,甚至為空。
間隙鎖是效能和併發性之間權衡的一部分,並且在一些事務隔離級別中使用。
SELECT * FROM child WHERE id = 100;
如果id沒有索引或具有非唯一索引,則該語句會鎖定上述間隙。
事務A可以在間隙上持有一個共享間隙鎖(gap S-lock),與此同時,事務B在同一間隙上持有一個排他間隙鎖(gap X-lock)。允許兩個間隙鎖的原因是,如果從索引中清除記錄,則必須合併不同事務對記錄持有的間隙鎖。
6.2 悲觀鎖按照作用範圍劃分:
行鎖:鎖的作用範圍是行級別,資料庫能夠確定那些行需要鎖的情況下使用行鎖,如果不知道會影響哪些行的時候就會使用表鎖。舉個例子,一個使用者表user,有主鍵id和使用者生日birthday當你使用update ... where id=?這樣的語句資料庫明確知道會影響哪一行,它就會使用行鎖,當你使用update ... where birthday=?這樣的的語句的時候因為事先不知道會影響哪些行就可能會使用表鎖。
InnoDB行鎖是通過給索引上的索引項加鎖來實現的,這一點MySQL與Oracle不同,後者是通過在資料塊中對相應資料行加鎖來實現的。InnoDB這種行鎖實現特點意味著:只有通過索引條件檢索資料,InnoDB才使用行級鎖,否則,InnoDB將使用表鎖!
由於MySQL的行鎖是針對索引加的鎖,不是針對記錄加的鎖,所以雖然是訪問不同行的記錄,但是如果是使用相同的索引鍵,是會出現鎖衝突的。
當表有多個索引的時候,不同的事務可以使用不同的索引鎖定不同的行,另外,不論是使用主鍵索引、唯一索引或普通索引,InnoDB都會使用行鎖來對資料加鎖。
即便在條件中使用了索引欄位,但是否使用索引來檢索資料是由MySQL通過判斷不同執行計劃的代價來決定的,如果MySQL認為全表掃描效率更高,比如對一些很小的表,它就不會使用索引,這種情況下InnoDB將使用表鎖,而不是行鎖。 因此,在分析鎖衝突時,別忘了檢查SQL的執行計劃,以確認是否真正使用了索引。
InnoDB行鎖是通過給索引上的索引項加鎖來實現的,如果沒有索引,InnoDB將通過隱藏的聚集索引來對記錄加鎖。InnoDB行鎖主要分為3種情形:
Record lock:對索引項加鎖
Gap lock:對索引項之間的“間隙”、第一條記錄前的“間隙”或最後一條記錄後的“間隙”加鎖
Next-key lock:前兩種的組合,對記錄及其前面的間隙加鎖。
InnoDB這種行鎖實現特點意味著:如果不通過索引條件檢索資料,那麼InnoDB將對錶中的所有記錄加鎖,實際效果和表鎖一樣。
- 表鎖:鎖的作用範圍是整張表。
樂觀鎖:顧名思義,就是很樂觀,每次自己操作資料的時候認為沒有人回來修改它,所以不去加鎖,但是在更新的時候會去判斷在此期間資料有沒有被修改,需要使用者自己去實現。既然都有資料庫提供的悲觀鎖可以方便使用為什麼要使用樂觀鎖呢?對於讀操作遠多於寫操作的時候,大多數都是讀取,這時候一個更新操作加鎖會阻塞所有讀取,降低了吞吐量。最後還要釋放鎖,鎖是需要一些開銷的,我們只要想辦法解決極少量的更新操作的同步問題。換句話說,如果是讀寫比例差距不是非常大或者你的系統沒有響應不及時,吞吐量瓶頸問題,那就不要去使用樂觀鎖,它增加了複雜度,也帶來了額外的風險。
6.3 樂觀鎖實現方式:
版本號(記為version):就是給資料增加一個版本標識,在資料庫上就是表中增加一個version欄位,每次更新把這個欄位加1,讀取資料的時候把version讀出來,更新的時候比較version,如果還是開始讀取的version就可以更新了,如果現在的version比老的version大,說明有其他事務更新了該資料,並增加了版本號,這時候得到一個無法更新的通知,使用者自行根據這個通知來決定怎麼處理,比如重新開始一遍。這裡的關鍵是判斷version和更新兩個動作需要作為一個原子單元執行,否則在你判斷可以更新以後正式更新之前有別的事務修改了version,這個時候你再去更新就可能會覆蓋前一個事務做的更新,造成第二類丟失更新,所以你可以使用update ... where ... and version="old version"這樣的語句,根據返回結果是0還是非0來得到通知,如果是0說明更新沒有成功,因為version被改了,如果返回非0說明更新成功。
時間戳(timestamp):和版本號基本一樣,只是通過時間戳來判斷而已,注意時間戳要使用資料庫伺服器的時間戳不能是業務系統的時間。
待更新欄位:和版本號方式相似,只是不增加額外欄位,直接使用有效資料欄位做版本控制資訊,因為有時候我們可能無法改變舊系統的資料庫表結構。假設有個待更新欄位叫count,先去讀取這個count,更新的時候去比較資料庫中count的值是不是我期望的值(即開始讀的值),如果是就把我修改的count的值更新到該欄位,否則更新失敗。java的基本型別的原子型別物件如AtomicInteger就是這種思想。
所有欄位:和待更新欄位類似,只是使用所有欄位做版本控制資訊,只有所有欄位都沒變化才會執行更新。
相關閱讀:
本文來自網易雲社群,經作者王志泳授權釋出。更多網易研發、產品、運營經驗分享請訪問網易雲社群。