數據庫結構(原文)
在 InnoDB 存儲引擎中,所有的數據都被邏輯地存放在表空間中,表空間(tablespace)是存儲引擎中最高的存儲邏輯單位,在表空間的下面又包括段(segment)、區(extent)、頁(page):
1、.frm
文件用來描述表的格式或者說定義;
2、.ibd 文件 數據索引、數據信息(默認所有表在一個文件中,打開 innodb_file_per_table設置按表區分
);
同一個數據庫實例的所有表空間都有相同的頁大小;默認情況下,表空間中的頁大小都為 16KB;
每個 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+ 樹索引並不能找到一個給定鍵對應的具體值,它只能找到數據行對應的頁,數據庫把整個頁讀入到內存中,並在內存中查找具體的數據行。
一、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碼作為比較准則。聚集索引這種實現方式使得按主鍵的搜索十分高效,但是輔助索引搜索需要檢索兩遍索引:首先檢索輔助索引獲得主鍵,然后用主鍵到主索引中檢索獲得記錄。
索引優化
1、組合索引比單個索引效果更好;
2、當搜索范圍超過30%,索引作用微乎其微;
3、索引最左有限原則;
4、SQL優化神器;(explain:id越大越先執行,id相同由上而下)
導致索引失效的可能情況
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 存儲引擎中使用的就是悲觀鎖,而按照鎖的粒度划分,也可以分成行鎖和表鎖。
並發控制機制
樂觀鎖和悲觀鎖其實都是並發控制的機制,同時它們在原理上就有着本質的差別;
- 樂觀鎖指樂觀的認為要訪問的數據不會被人修改。因此不對數據進行加鎖,如果操作的時候發現已經失敗了,則重新獲取數據進行更新(如CAS),或者直接返回操作失敗。
- 悲觀鎖就是一種真正的鎖了,它會在獲取資源前對資源進行加鎖,確保同一時刻只有有限的線程能夠訪問該資源,其他想要嘗試獲取資源的操作都會進入等待狀態,直到該線程完成了對資源的操作並且釋放了鎖后,其他線程才能重新操作資源;
雖然樂觀鎖和悲觀鎖在本質上並不是同一種東西,一個是一種思想,另一個是一種真正的鎖,但是它們都是一種並發控制機制。
樂觀鎖不會存在死鎖的問題,但是由於更新后驗證,所以當沖突頻率和重試成本較高時更推薦使用悲觀鎖,而需要非常高的響應速度並且並發量非常大的時候使用樂觀鎖就能較好的解決問題,在這時使用悲觀鎖就可能出現嚴重的性能問題;在選擇並發控制機制時,需要綜合考慮上面的四個方面(沖突頻率、重試成本、響應速度和並發量)進行選擇。
鎖的種類
對數據的操作其實只有兩種,也就是讀和寫,而數據庫在實現鎖時,也會對這兩種操作使用不同的鎖;InnoDB 實現了標准的行級鎖,也就是共享鎖(Shared Lock)和互斥鎖(Exclusive Lock);共享鎖和互斥鎖的作用其實非常好理解:
-
共享鎖:允許事務去讀一行,阻止其他事務對該數據進行修改
排它鎖:允許事務去讀取更新數據,阻止其他事務對數據進行查詢或者修改(SELECT FOR UPDATE)
而它們的名字也暗示着各自的另外一個特性,共享鎖之間是兼容的,而互斥鎖與其他任意鎖都不兼容:
稍微對它們的使用進行思考就能想明白它們為什么要這么設計,因為共享鎖代表了讀操作、互斥鎖代表了寫操作,所以我們可以在數據庫中並行讀,但是只能串行寫,只有這樣才能保證不會發生線程競爭,實現線程安全。
鎖的粒度
無論是共享鎖還是互斥鎖其實都只是對某一個數據行進行加鎖,InnoDB 支持多種粒度的鎖,也就是行鎖和表鎖;
表鎖
表鎖分為讀寫鎖有read和write兩種
Lock Tables......Read通常被稱為共享鎖或者讀鎖,讀鎖或者共享鎖,是互相不阻塞的,多個用戶可以同一時間使用共享鎖互相不阻塞。
Lock Table......write通常被稱為排他鎖或者寫鎖,寫鎖或者排他鎖會阻塞其他的讀鎖或者寫鎖,確保在給定時間里,只有一個用戶執行寫入,防止其他用戶讀取正在寫入的同一資源。
總結:
- Lock Tables....READ不會阻塞其他線程對表數據的讀取,會阻塞其他線程對數據變更
- Lock Tables....WRITE會阻塞其他線程對數據讀和寫
- Lock Tables....READ不允許對表進行更新操作(新增、刪除也不行),並且不允許訪問未被鎖住的表
- Lock Tables....WRITE允許對被鎖住的表進行增刪改查,但不允許對其他表進行訪
顯示調用:
LOCK TABLES oauth_code WRITE/READ
UNLOCK TABLES;
為了支持多粒度鎖定,InnoDB 存儲引擎引入了意向鎖(Intention Lock),意向鎖就是一種表級鎖。
與上一節中提到的兩種鎖的種類相似的是,意向鎖也分為兩種:
- 意向共享鎖:事務想要在獲得表中某些記錄的共享鎖,需要在表上先加意向共享鎖;
- 意向互斥鎖:事務想要在獲得表中某些記錄的互斥鎖,需要在表上先加意向互斥鎖;
- 意向鎖之間兼容,不會阻塞。但是會跟S鎖和X鎖沖突,沖突的方式跟讀寫鎖相同。例如當一張表上已經有一個排它鎖(X鎖),此時如果另外一個線程要對該表加意向鎖,不管意向共享鎖還是意向排他鎖都不會成功。
隨着意向鎖的加入,鎖類型之間的兼容矩陣也變得愈加復雜:
意向鎖其實不會阻塞全表掃描之外的任何請求,它們的主要目的是為了表示是否有人請求鎖定表中的某一行數據。
有的人可能會對意向鎖的目的並不是完全的理解,我們在這里可以舉一個例子:如果沒有意向鎖,當已經有人使用行鎖對表中的某一行進行修改時,如果另外一個請求要對全表進行修改,那么就需要對所有的行是否被鎖定進行掃描,在這種情況下,效率是非常低的;不過,在引入意向鎖之后,當有人使用行鎖對表中的某一行進行修改之前,會先為表添加意向互斥鎖(IX),再為行記錄添加互斥鎖(X),在這時如果有人嘗試對全表進行修改就不需要判斷表中的每一行數據是否被加鎖了,只需要通過等待意向互斥鎖被釋放就可以了。
死鎖的發生
既然 InnoDB 中實現的鎖是悲觀的,那么不同事務之間就可能會互相等待對方釋放鎖造成死鎖,最終導致事務發生錯誤;想要在 MySQL 中制造死鎖的問題其實非常容易:
兩個會話都持有一個鎖,並且嘗試獲取對方的鎖時就會發生死鎖,不過 MySQL 也能在發生死鎖時及時發現問題,並保證其中的一個事務能夠正常工作,這對我們來說也是一個好消息。
事務與隔離級別
事務還遵循 ACID 四大特性:原子性(Atomicity)、一致性(Consistency)、隔離性(Isolation)和持久性(Durability);
幾種隔離級別
隔離級別 | 臟讀(Dirty Read) | 不可重復讀(NonRepeatable Read) | 幻讀(Phantom Read) |
---|---|---|---|
未提交讀(Read uncommitted) | 可能 | 可能 | 可能 |
已提交讀(Read committed) | 不可能 | 可能 | 可能 |
可重復讀(Repeatable read) | 不可能 | 不可能 | 可能 |
可串行化(Serializable ) | 不可能 | 不可能 | 不可能 |
- 未提交讀(Read Uncommitted):允許臟讀,也就是可能讀取到其他會話中未提交事務修改的數據;
- 提交讀(Read Committed):只能讀取到已經提交的數據。Oracle等多數數據庫默認都是該級別 (不重復讀);
- 可重復讀(Repeated Read):可重復讀。在同一個事務內的查詢都是事務開始時刻一致的,InnoDB默認級別。在SQL標准中,該隔離級別消除了不可重復讀,但是還存在幻象讀;
- 串行讀(Serializable):完全串行化的讀,每次讀都需要獲得表級共享鎖,讀寫相互都會阻塞;
1、臟讀:事務A讀取了事務B更新的數據,然后B回滾操作,那么A讀取到的數據是臟數據
2、不可重復讀:事務 A 多次讀取同一數據,事務 B 在事務A多次讀取的過程中,對數據作了更新並提交,導致事務A多次讀取同一數據時,結果 不一致。
3、幻讀:系統管理員A將數據庫中所有學生的成績從具體分數改為ABCDE等級,但是系統管理員B就在這個時候插入了一條具體分數的記錄,當系統管理員A改結束后發現還有一條記錄沒有改過來,就好像發生了幻覺一樣,這就叫幻讀。
不可重復讀和幻讀的區別
小結:不可重復讀的和幻讀很容易混淆,不可重復讀側重於修改,幻讀側重於新增或刪除。解決不可重復讀的問題只需鎖住滿足條件的行,解決幻讀需要鎖表
如果使用鎖機制來實現這兩種隔離級別,在可重復讀中,該sql第一次讀取到數據后,就將這些數據加鎖,其它事務無法修改這些數據,就可以實現可重復讀了。但這種方法卻無法鎖住insert的數據,所以當事務A先前讀取了數據,或者修改了全部數據,事務B還是可以insert數據提交,這時事務A就會發現莫名其妙多了一條之前沒有的數據,這就是幻讀,不能通過行鎖來避免。需要Serializable隔離級別 ,讀用讀鎖,寫用寫鎖,讀鎖和寫鎖互斥,這么做可以有效的避免幻讀、不可重復讀、臟讀等問題,但會極大的降低數據庫的並發能力。
所以說不可重復讀和幻讀最大的區別,就在於如何通過鎖機制來解決他們產生的問題。
上文說的,是使用悲觀鎖機制來處理這兩種問題,但是MySQL、ORACLE、PostgreSQL等成熟的數據庫,出於性能考慮,都是使用了以樂觀鎖為理論基礎的MVCC(多版本並發控制)來避免這兩種問題。
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),它並不加鎖。
鎖的算法
MVCC:
多版本控制,InnoDB實現MVCC是通過在每行記錄后面保存兩個隱藏的列來實現,一個保存創建的事務版本號,一個保存的是刪除的事務版本號。MVCC只有在REPEATABLE READ 和 READ COMMITED兩個隔離級別下工作。另外兩個隔離級別與MVCC並不兼容,因為READ UNCOMMITED總是讀取最新數據,跟事務版本無關,而SERIALIZABLE會對讀取的所有行都進行加鎖。
MVCC實現原理:https://www.toutiao.com/a6868176598705635853/?tt_from=weixin&utm_campaign=client_share&app=news_article&utm_source=weixin&iid=1336623137845079&utm_medium=toutiao_android&wxshare_count=1
間隙鎖(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 的行鎖是實現在索引上的,而不是鎖在物理行記錄上。所以如果訪問沒有命中索引,也無法使用行鎖,將要退化為表鎖。
間隙鎖例子:https://blog.csdn.net/weixin_34553861/article/details/112380759