數據庫結構(原文)html
在 InnoDB 存儲引擎中,全部的數據都被邏輯地存放在表空間中,表空間(tablespace)是存儲引擎中最高的存儲邏輯單位,在表空間的下面又包括段(segment)、區(extent)、頁(page):算法
一、.frm
文件用來描述表的格式或者說定義;sql
二、.ibd 文件 數據索引、數據信息(默認全部表在一個文件中,打開 innodb_file_per_table設置按表區分
);數據庫
同一個數據庫實例的全部表空間都有相同的頁大小;默認狀況下,表空間中的頁大小都爲 16KB;segmentfault
每一個 16KB 大小的頁中能夠存放 2-200 行的記錄。安全
索引數據結構
CREATE TABLE users(併發
id INT NOT NULL, 函數
first_name VARCHAR(20) NOT NULL, 性能
last_name VARCHAR(20) NOT NULL,
age INT NOT NULL,
PRIMARY KEY(id),
KEY(last_name, first_name, age)
KEY(first_name)
);
彙集索引
B+ 樹經過 B+ 樹實現的就會使用 id
做爲索引的鍵,並在葉子節點中存儲一條記錄中的全部信息。
全部正常的表應該有且僅有一個彙集索引(絕大多數狀況下都是主鍵),表中的全部行記錄數據都是按照彙集索引的順序存放的。
輔助索引
輔助索引也是經過 B+ 樹實現的,可是它的葉節點並不包含行記錄的所有數據,僅包含索引中的全部鍵和一個用於查找對應行記錄的『書籤』,在 InnoDB 中這個書籤就是當前記錄的主鍵。
InnoDB的索引實現後,就很容易明白爲何不建議使用過長的字段做爲主鍵,由於全部輔助索引都引用主索引,過長的主索引會令輔助索引變得過大。
用非單調的字段做爲主鍵在InnoDB中不是個好主意,由於InnoDB數據文件自己是一顆B+Tree,非單調的主鍵會形成在插入新記錄時數據文件爲了維持B+Tree的特性而頻繁的分裂調整,十分低效,而使用自增字段做爲主鍵則是一個很好的選擇。
索引的數據結構
InnoDB 存儲引擎在絕大多數狀況下使用 B+ 樹創建索引,可是 B+ 樹索引並不能找到一個給定鍵對應的具體值,它只能找到數據行對應的頁,數據庫把整個頁讀入到內存中,並在內存中查找具體的數據行。
1、myisam和innodb索引實現的不一樣
MyISAM類型不支持事務處理等高級處理,而InnoDB類型支持。
MyISAM類型的表強調的是性能,其執行數度比InnoDB類型更快,可是不提供事務支持,而InnoDB提供事務支持以及外部鍵等高級數據庫功能。
實現方式:
MyISAM引擎使用B+Tree做爲索引結構,葉節點的data域存放的是數據記錄的地址。下圖是MyISAM索引的原理圖:
這裏設表一共有三列,假設咱們以Col1爲主鍵,則上圖是一個MyISAM表的主索引(Primary key)示意。能夠看出MyISAM的索引文件僅僅保存數據記錄的地址。在MyISAM中,主索引和輔助索引(Secondary key)在結構上沒有任何區別,只是主索引要求key是惟一的,而輔助索引的key能夠重複。若是咱們在Col2上創建一個輔助索引,則此索引的結構以下圖所示:
一樣也是一顆B+Tree,data域保存數據記錄的地址。所以,MyISAM中索引檢索的算法爲首先按照B+Tree搜索算法搜索索引,若是指定的Key存在,則取出其data域的值,而後以data域的值爲地址,讀取相應數據記錄。
MyISAM的索引方式也叫作「非彙集」的,之因此這麼稱呼是爲了與InnoDB的彙集索引區分。
InnoDB索引實現
雖然InnoDB也使用B+Tree做爲索引結構,但具體實現方式卻與MyISAM大相徑庭。
第一個重大區別是InnoDB的數據文件自己就是索引文件。從上文知道,MyISAM索引文件和數據文件是分離的,索引文件僅保存數據記錄的地址。而在InnoDB中,表數據文件自己就是按B+Tree組織的一個索引結構,這棵樹的葉節點data域保存了完整的數據記錄。這個索引的key是數據表的主鍵,所以InnoDB表數據文件自己就是主索引。
上圖是InnoDB主索引(同時也是數據文件)的示意圖,能夠看到葉節點包含了完整的數據記錄。這種索引叫作彙集索引。由於InnoDB的數據文件自己要按主鍵彙集,因此InnoDB要求表必須有主鍵(MyISAM能夠沒有),若是沒有顯式指定,則MySQL系統會自動選擇一個能夠惟一標識數據記錄的列做爲主鍵,若是不存在這種列,則MySQL自動爲InnoDB表生成一個隱含字段做爲主鍵,這個字段長度爲6個字節,類型爲長整形。
第二個與MyISAM索引的不一樣是InnoDB的輔助索引data域存儲相應記錄主鍵的值而不是地址。換句話說,InnoDB的全部輔助索引都引用主鍵做爲data域。例如,下圖爲定義在Col3上的一個輔助索引:
這裏以英文字符的ASCII碼做爲比較準則。彙集索引這種實現方式使得按主鍵的搜索十分高效,可是輔助索引搜索須要檢索兩遍索引:首先檢索輔助索引得到主鍵,而後用主鍵到主索引中檢索得到記錄。
索引優化
一、組合索引比單個索引效果更好;
二、當搜索範圍超過30%,索引做用微乎其微;
三、索引最左有限原則;
四、SQL優化神器;
致使索引失效的可能狀況
2.對索引列進行運算致使索引失效,我所指的對索引列進行運算包括(+,-,*,/,! 等)
create index ind_test3_c1234 on test3(c1,c2,c3,c4);
explain select * from test3 where c1='a1' and c5='a5' order by c2,c3; 只用了c1這個字段索引,可是c2,c3用於排序,無filesort explain select * from test3 where c1='a1' and c5='a5' order by c3,c2; 只用了c1這個字段索引,可是因爲c3,c2順序顛倒了,因此沒法使用索引排序,出現filesort
鎖
咱們都知道鎖的種類通常分爲樂觀鎖和悲觀鎖兩種,InnoDB 存儲引擎中使用的就是悲觀鎖,而按照鎖的粒度劃分,也能夠分紅行鎖和表鎖。
併發控制機制
樂觀鎖和悲觀鎖其實都是併發控制的機制,同時它們在原理上就有着本質的差異;
- 樂觀鎖是一種思想,它其實並非一種真正的『鎖』,它會先嚐試對資源進行修改,在寫回時判斷資源是否進行了改變,若是沒有發生改變就會寫回,不然就會進行重試,在整個的執行過程當中其實都沒有對數據庫進行加鎖;
- 悲觀鎖就是一種真正的鎖了,它會在獲取資源前對資源進行加鎖,確保同一時刻只有有限的線程可以訪問該資源,其餘想要嘗試獲取資源的操做都會進入等待狀態,直到該線程完成了對資源的操做而且釋放了鎖後,其餘線程才能從新操做資源;
雖然樂觀鎖和悲觀鎖在本質上並非同一種東西,一個是一種思想,另外一個是一種真正的鎖,可是它們都是一種併發控制機制。
樂觀鎖不會存在死鎖的問題,可是因爲更新後驗證,因此當衝突頻率和重試成本較高時更推薦使用悲觀鎖,而須要很是高的響應速度而且併發量很是大的時候使用樂觀鎖就能較好的解決問題,在這時使用悲觀鎖就可能出現嚴重的性能問題;在選擇併發控制機制時,須要綜合考慮上面的四個方面(衝突頻率、重試成本、響應速度和併發量)進行選擇。
鎖的種類
對數據的操做其實只有兩種,也就是讀和寫,而數據庫在實現鎖時,也會對這兩種操做使用不一樣的鎖;InnoDB 實現了標準的行級鎖,也就是共享鎖(Shared Lock)和互斥鎖(Exclusive Lock);共享鎖和互斥鎖的做用其實很是好理解:
- 共享鎖(讀鎖):容許事務對一條行數據進行讀取;
- 互斥鎖(寫鎖):容許事務對一條行數據進行刪除或更新;
而它們的名字也暗示着各自的另一個特性,共享鎖之間是兼容的,而互斥鎖與其餘任意鎖都不兼容:
稍微對它們的使用進行思考就能想明白它們爲何要這麼設計,由於共享鎖表明了讀操做、互斥鎖表明了寫操做,因此咱們能夠在數據庫中並行讀,可是隻能串行寫,只有這樣才能保證不會發生線程競爭,實現線程安全。
鎖的粒度
不管是共享鎖仍是互斥鎖其實都只是對某一個數據行進行加鎖,InnoDB 支持多種粒度的鎖,也就是行鎖和表鎖;爲了支持多粒度鎖定,InnoDB 存儲引擎引入了意向鎖(Intention Lock),意向鎖就是一種表級鎖。
與上一節中提到的兩種鎖的種類類似的是,意向鎖也分爲兩種:
- 意向共享鎖:事務想要在得到表中某些記錄的共享鎖,須要在表上先加意向共享鎖;
- 意向互斥鎖:事務想要在得到表中某些記錄的互斥鎖,須要在表上先加意向互斥鎖;
- 意向鎖之間兼容,不會阻塞。可是會跟S鎖和X鎖衝突,衝突的方式跟讀寫鎖相同。例如當一張表上已經有一個排它鎖(X鎖),此時若是另一個線程要對該表加意向鎖,無論意向共享鎖仍是意向排他鎖都不會成功。
隨着意向鎖的加入,鎖類型之間的兼容矩陣也變得越發複雜:
意向鎖其實不會阻塞全表掃描以外的任何請求,它們的主要目的是爲了表示是否有人請求鎖定表中的某一行數據。
有的人可能會對意向鎖的目的並非徹底的理解,咱們在這裏能夠舉一個例子:若是沒有意向鎖,當已經有人使用行鎖對錶中的某一行進行修改時,若是另一個請求要對全表進行修改,那麼就須要對全部的行是否被鎖定進行掃描,在這種狀況下,效率是很是低的;不過,在引入意向鎖以後,當有人使用行鎖對錶中的某一行進行修改以前,會先爲表添加意向互斥鎖(IX),再爲行記錄添加互斥鎖(X),在這時若是有人嘗試對全表進行修改就不須要判斷表中的每一行數據是否被加鎖了,只須要經過等待意向互斥鎖被釋放就能夠了。
鎖的算法
到目前爲止已經對 InnoDB 中鎖的粒度有必定的瞭解,也清楚了在對數據庫進行讀寫時會獲取不一樣的鎖,在這一小節將介紹鎖是如何添加到對應的數據行上的,咱們會分別介紹三種鎖的算法:Record Lock、Gap Lock 和 Next-Key Lock。
Record Lock
記錄鎖(Record Lock)是加到索引記錄上的鎖,假設咱們存在下面的一張表 users
:
CREATE TABLE users( id INT NOT NULL AUTO_INCREMENT, last_name VARCHAR(255) NOT NULL, first_name VARCHAR(255), age INT, PRIMARY KEY(id), KEY(last_name), KEY(age) );
若是咱們使用 id
或者 last_name
做爲 SQL 中 WHERE
語句的過濾條件,那麼 InnoDB 就能夠經過索引創建的 B+ 樹找到行記錄並添加索引,可是若是使用 first_name
做爲過濾條件時,因爲 InnoDB 不知道待修改的記錄具體存放的位置,也沒法對將要修改哪條記錄提早作出判斷就會鎖定整個表。
Gap Lock
記錄鎖是在存儲引擎中最爲常見的鎖,除了記錄鎖以外,InnoDB 中還存在間隙鎖(Gap Lock),間隙鎖是對索引記錄中的一段連續區域的鎖;當使用相似 SELECT * FROM users WHERE id BETWEEN 10 AND 20 FOR UPDATE;
的 SQL 語句時,就會阻止其餘事務向表中插入 id = 15
的記錄,由於整個範圍都被間隙鎖鎖定了。
間隙鎖是存儲引擎對於性能和併發作出的權衡,而且只用於某些事務隔離級別。
雖然間隙鎖中也分爲共享鎖和互斥鎖,不過它們之間並非互斥的,也就是不一樣的事務能夠同時持有一段相同範圍的共享鎖和互斥鎖,它惟一阻止的就是其餘事務向這個範圍中添加新的記錄。
Next-Key Lock
Next-Key 鎖相比前二者就稍微有一些複雜,它是記錄鎖和記錄前的間隙鎖的結合,在 users
表中有如下記錄:
+------+-------------+--------------+-------+ | id | last_name | first_name | age | |------+-------------+--------------+-------| | 4 | stark | tony | 21 | | 1 | tom | hiddleston | 30 | | 3 | morgan | freeman | 40 | | 5 | jeff | dean | 50 | | 2 | donald | trump | 80 | +------+-------------+--------------+-------+
若是使用 Next-Key 鎖,那麼 Next-Key 鎖就能夠在須要的時候鎖定如下的範圍:
(-∞, 21] (21, 30] (30, 40] (40, 50] (50, 80] (80, ∞)
既然叫 Next-Key 鎖,鎖定的應該是當前值和後面的範圍,可是實際上卻不是,Next-Key 鎖鎖定的是當前值和前面的範圍。
當咱們更新一條記錄,好比 SELECT * FROM users WHERE age = 30 FOR UPDATE;
,InnoDB 不只會在範圍 (21, 30]
上加 Next-Key 鎖,還會在這條記錄後面的範圍 (30, 40]
加間隙鎖,因此插入 (21, 40]
範圍內的記錄都會被鎖定。
Next-Key 鎖的做用實際上是爲了解決幻讀的問題,咱們會在下一節談事務的時候具體介紹。
死鎖的發生
既然 InnoDB 中實現的鎖是悲觀的,那麼不一樣事務之間就可能會互相等待對方釋放鎖形成死鎖,最終致使事務發生錯誤;想要在 MySQL 中製造死鎖的問題其實很是容易:
兩個會話都持有一個鎖,而且嘗試獲取對方的鎖時就會發生死鎖,不過 MySQL 也能在發生死鎖時及時發現問題,並保證其中的一個事務可以正常工做,這對咱們來講也是一個好消息。
事務與隔離級別
在介紹了鎖以後,咱們再來談談數據庫中一個很是重要的概念 —— 事務;相信只要是一個合格的軟件工程師就對事務的特性有所瞭解,其中被人常常提起的就是事務的原子性,在數據提交工做時,要麼保證全部的修改都可以提交,要麼就全部的修改所有回滾。
可是事務還遵循包括原子性在內的 ACID 四大特性:原子性(Atomicity)、一致性(Consistency)、隔離性(Isolation)和持久性(Durability);文章不會對這四大特性所有展開進行介紹,相信你可以經過 Google 和數據庫相關的書籍輕鬆得到有關它們的概念,本文最後要介紹的就是事務的四種隔離級別。
幾種隔離級別
事務的隔離性是數據庫處理數據的幾大基礎之一,而隔離級別其實就是提供給用戶用於在性能和可靠性作出選擇和權衡的配置項。
ISO 和 ANIS SQL 標準制定了四種事務隔離級別,而 InnoDB 遵循了 SQL:1992 標準中的四種隔離級別:READ UNCOMMITED、READ COMMITED、REPEATABLE READ 和 SERIALIZABLE;每一個事務的隔離級別其實都比上一級多解決了一個問題:
RAED UNCOMMITED:使用查詢語句不會加鎖,可能會讀到未提交的行(Dirty Read);READ COMMITED:只對記錄加記錄鎖,而不會在記錄之間加間隙鎖,因此容許新的記錄插入到被鎖定記錄的附近,因此再屢次使用查詢語句時,可能獲得不一樣的結果(Non-Repeatable Read);REPEATABLE READ:屢次讀取同一範圍的數據會返回第一次查詢的快照,不會返回不一樣的數據行,可是可能發生幻讀(Phantom Read);SERIALIZABLE:InnoDB 隱式地將所有的查詢語句加上共享鎖,解決了幻讀的問題;MySQL 中默認的事務隔離級別就是 REPEATABLE READ,可是它經過 Next-Key 鎖也可以在某種程度上解決幻讀的問題。
接下來,咱們將數據庫中建立以下的表並經過個例子來展現在不一樣的事務隔離級別之下,會發生什麼樣的問題:
CREATETABLEtest(
idINTNOTNULL,
UNIQUE(id)
);
髒讀
當事務的隔離級別爲 READ UNCOMMITED 時,咱們在 SESSION 2 中插入的未提交數據在 SESSION 1 中是能夠訪問的。
不可重複讀
當事務的隔離級別爲 READ COMMITED 時,雖然解決了髒讀的問題,可是若是在 SESSION 1 先查詢了一個範圍的數據,在這以後 SESSION 2 中插入一條數據而且提交了修改,在這時,若是 SESSION 1 中再次使用相同的查詢語句,就會發現兩次查詢的結果不同。
不可重複讀的緣由就是,在 READ COMMITED 的隔離級別下,存儲引擎不會在查詢記錄時添加間隙鎖,鎖定 id < 5 這個範圍。
幻讀
從新開啓了兩個會話 SESSION 1 和 SESSION 2,在 SESSION 1 中咱們查詢全表的信息,沒有獲得任何記錄;在 SESSION 2 中向表中插入一條數據並提交;因爲 REPEATABLE READ 的緣由,再次查詢全表的數據時,咱們得到到的仍然是空集,可是在向表中插入一樣的數據卻出現了錯誤。
這種現象在數據庫中就被稱做幻讀,雖然咱們使用查詢語句獲得了一個空的集合,可是插入數據時卻獲得了錯誤,好像以前的查詢是幻覺同樣。
在標準的事務隔離級別中,幻讀是由更高的隔離級別 SERIALIZABLE 解決的,可是它也能夠經過 MySQL 提供的 Next-Key 鎖解決:
REPERATABLE READ 和 READ UNCOMMITED 實際上是矛盾的,若是保證了前者就看不到已經提交的事務,若是保證了後者,就會致使兩次查詢的結果不一樣,MySQL 爲咱們提供了一種折中的方式,可以在 REPERATABLE READ 模式下加鎖訪問已經提交的數據,其自己並不能解決幻讀的問題,而是經過文章前面提到的 Next-Key 鎖來解決。
select 鎖表問題。
MySQL 最多見的坑就是 InnoDB 是行鎖,這是你們都知道的事,可是有時候它卻會鎖表,你說奇怪不奇怪。
其實只要你懂它了以後,一點也不會以爲奇怪。只有你不懂,纔會以爲它奇怪。InnoDB 的行鎖是實如今索引上的,而不是鎖在物理行記錄上。潛臺詞是,若是訪問沒有命中索引,也沒法使用行鎖,將要退化爲表鎖。這一點和 Oracle 的行鎖實現機制略有不一樣。
例以下面的表:
1
|
xttblog(id, title, url, text) innodb;
|
id PK(主鍵),無其餘索引,即其餘列都沒有索引。
1
|
update
xttblog
set
text=
'業餘草'
where
id=1;
|
命中索引,行鎖。
1
|
update
xttblog
set
text=
'業餘草'
where
id != 1;
|
未命中索引,表鎖。
1
|
update
xttblog
set
text=
'業餘草'
where
title=
'業餘草'
;
|
無索引,表鎖。
啓示:InnoDB 務必建好索引,不然鎖粒度較大,會影響併發。
再說一下 select,若是查詢沒有命中索引,也將退化爲表鎖。下面咱們結合 InnoDB 的三種鎖(記錄鎖(Record Locks)、間隙鎖(Gap Locks)、臨鍵鎖(Next-Key Locks))來講明它。再講這三種鎖的前提條件是默認的事務隔離級別爲可重複讀(Repeated Read, RR)。
記錄鎖(Record Locks)
記錄鎖,它封鎖索引記錄,例以下面的查詢語句:
1
|
select
*
from
xttblog
where
id=1
for
update
;
|
它會在 id=1 的索引記錄上加鎖,以阻止其餘事務插入,更新,刪除 id=1 的這一行。
須要說明的是,若是是下面的查詢語句:
1
|
select
*
from
xttblog
where
id=1;
|
則是快照讀(SnapShot Read),它並不加鎖。
間隙鎖(Gap Locks)
間隙鎖,它封鎖索引記錄中的間隔,或者第一條索引記錄以前的範圍,又或者最後一條索引記錄以後的範圍。例以下面的 SQL 語句:
1
|
select
*
from
xttblog
where
id
between
8
and
15
for
update
;
|
會封鎖區間 id 爲 8 到 15 的記錄。以阻止其餘事務,如:id=10 的記錄插入。
若是不阻止 id=10 的記錄插入,則會產生幻讀。若是可以插入成功,同一個事務執行相同的 SQL 語句,會發現結果集多出了一條記錄,即幻讀數據。
間隙鎖的主要目的,就是爲了防止其餘事務在間隔中插入數據,以致使「不可重複讀」。
若是把事務的隔離級別降級爲讀提交(Read Committed, RC),間隙鎖則會自動失效。
臨鍵鎖(Next-Key Locks)
臨鍵鎖,是記錄鎖與間隙鎖的組合,它的封鎖範圍,既包含索引記錄,又包含索引區間。臨鍵鎖會封鎖索引記錄自己,以及索引記錄以前的區間。
若是一個會話佔有了索引記錄 Record 的共享/排他鎖,其餘會話不能馬上在 Record 以前的區間插入新的索引記錄。臨鍵鎖的主要目的,也是爲了不幻讀(Phantom Read)。若是把事務的隔離級別降級爲RC,臨鍵鎖則也會失效。
最後說一下,怎麼測試當前的查詢究竟是行鎖仍是表鎖呢?
以咱們以前發生事故來講,首先是 select 查詢不能有索引。而後 dev 環境和 sit 環境鏈接同一個數據庫,dev 對某個事務中的查詢取斷點,讓它停在查詢操做上;sit 環境則對同一個表進行插入、更新、刪除操做。查看日誌,就會發現有 time out 日誌。具體爲事務沒法提交,超時結束,由於這個表已經被鎖住了,獲取不到鎖,就會發生超時。
總結:InnoDB 的鎖,與索引類型,事務的隔離級別相關。InnoDB 究竟是行鎖仍是表鎖取決於你的 SQL 語句。若是查詢沒有命中索引,也將退化爲表鎖。InnoDB 的行鎖是實如今索引上的,而不是鎖在物理行記錄上。因此若是訪問沒有命中索引,也沒法使用行鎖,將要退化爲表鎖。