面試知識點整理之儲存篇
面試時所必要,或者被問到的問題的記錄,如果有理解錯誤或者疏漏,煩請指出,萬分感謝:)
MyISAM
和InnoDB
的理解與區別
- 兩者採用的都是
B+
樹,MyISAM
是非聚集索引,即索引檔案和資料檔案是分離的,data
域儲存的是資料檔案的指標。索引表只儲存資料記錄的地址;InnoDB
是聚集索引,因此必須需要主鍵,data
域儲存完整的資料記錄。但是通過輔助索引需要兩次查詢,因此主鍵不應該過大,否則會導致其他索引也很大。 MyISAM
只支援表鎖,因此加鎖開銷小,不會出現死鎖,但併發效能差;InnoDB
可以支援行鎖與表鎖,但是僅僅在使用索引時會使用行鎖MyISAM
不支援事務和崩潰後的安全恢復,但是InnoDB
MyISAM
不支援外來鍵,InnoDB
支援InnoDB
不儲存表的具體行數,執行select count(*)
時需要全表掃描,而MyISAM
使用變數儲存了整錶行數,執行select count(*)
速度很快。
由於MySQL
預設認為寫優先順序高於讀優先順序,因此當大量讀寫場景時,MyISAM
的鎖排程總是寫程序先獲得鎖,因此它並不適合有大量更新操作和查詢操作應用的原因,可能會造成查詢操作很難獲得讀鎖,從而可能永遠阻塞。
Mysql
索引
MyISAM
是非聚集索引,InnoDB
是聚集索引,因此在使用InnoDB
作為引擎設計表時,不建議使用過長的欄位作為主鍵(以免輔助索引過大),也不建議使用非單調的欄位作為主鍵(造成樹的頻繁調整)。
因此索引會降低增刪改查的速度。
索引的優化與失效
- 最左匹配原則,
MySQL
會一直向右匹配直到遇到範圍查詢(>,<,BETWEEN,LIKE)
就停止匹配(>=
,<=
是可以的)。因此儘管沒有按照索引的順序來寫,MySql
仍然會自動優化,因此in
、=
的順序並不重要 - 選擇區分度高的列作為索引,即
COUNT(DISTINCT col)/COUNT(*)
- 索引列不能參與計算
- 儘可能擴充套件索引,而不是新建立索引。比如表中已經有了a的索引,現在要
(a,b)
的索引,那麼只需要修改原來的索引即可。 - 儘量使用覆蓋索引,而不是使用
select *
- 索引欄位不要使用
is null
或者is not null
- 索引欄位使用
like
以萬用字元開頭,即'%xxx'
時,會導致索引失效(以萬用字元結尾並不會導致失效),如果使用%xxx%
時,請使用覆蓋索引。 - 索引是字串,查詢時不加單引號,會導致索引失效。
- 索引欄位使用
or
時,會導致索引失效 - 表資料較少時,可以全表掃描,不需要建立索引
EXPLAIN
[MySQL 效能優化神器 Explain 使用分析](https://segmentfault.com/a/1190000008131735)
使用EXPLAIN
命令可以對select
語句進行分析,以實現針對性優化。
mysql> explain select * from user_info where id = 2\G
*************************** 1. row ***************************
id: 1
select_type: SIMPLE
table: user_info
partitions: NULL
type: const
possible_keys: PRIMARY
key: PRIMARY
key_len: 8
ref: const
rows: 1
filtered: 100.00
Extra: NULL
1 row in set, 1 warning (0.00 sec)
欄位包括有:
id
:SELECT
查詢的識別符號,每個SELECT
都會自動分配一個唯一的識別符號.select_type
:SELECT
查詢的型別,常用的是SIMPLE
table
: 查詢的是哪個表partitions
: 匹配的分割槽type
:join
型別,效能從高到低為:system
:表中只有一條資料const
:針對主鍵或唯一索引的等值查詢掃描,最多隻返回一條資料ef_ref
:多表join
查詢,前表每一個結果都只能匹配後表的一條記錄ref
:多表join
查詢,針對非主鍵或者非唯一索引,或者使用了最左字首規則range
:使用索引範圍查詢,通常出現在=
,<>
,>
,>=
,<
,<=
,IS NULL
,<=>
,BETWEEN
,IN()
操作,此時key_len
使用到的索引最長的那個index
:全部索引進行掃描all
:全表掃描
possible_keys
: 此次查詢中可能選用的索引key
: 此次查詢中確切使用到的索引key_len
:使用的索引位元組數ref
: 哪個欄位或常數與key
一起被使用rows
: 顯示此查詢一共掃描了多少行, 這個是一個估計值filtered
: 表示此查詢條件所過濾的資料的百分比extra
: 額外的資訊,例如using filesort
(需要優化)/using index
/using temporary
(需要優化)
group by
的本質是先分組後排序,因此會使用using filesort
,可以使用order by null
禁用排序進行優化。
鎖
- 對於
UPDATE、DELETE、INSERT
語句,**InnoDB**
會自動給涉及資料集加排他鎖(X)
**MyISAM**
在執行查詢語句SELECT
前,會自動給涉及的所有表加讀鎖,在執行更新操作(UPDATE、DELETE、INSERT
等)前,會自動給涉及的表加寫鎖,這個過程並不需要使用者干預
鎖根據粒度可以分為:
- 表鎖:開銷小,加鎖快,不會出現死鎖,發生衝突概率高,併發低。表鎖可以分為讀鎖與寫鎖
- 讀鎖:只能加讀鎖,不能加寫鎖
- 寫鎖:不能加讀鎖與寫鎖
- 行鎖:開銷大,加鎖慢,會出現死鎖,發生衝突概率低,併發高。
InnoDB
實現了兩種型別的行鎖:- 共享鎖:也稱為讀鎖,多個客戶可以讀取同一個資源,但是不允許修改
- 排他鎖:也稱為寫鎖,會阻塞其他的寫鎖和讀鎖。
InnoDB
支援表鎖與行鎖(基於索引的),但是MyISAM
支援表鎖。預設情況下,InnoDB
中select
是不加任何鎖的,可以通過顯示加鎖:
- 共享鎖:
SELECT * FROM table_name WHERE ... LOCK IN SHARE MODE
。 - 排他鎖:
SELECT * FROM table_name WHERE ... FOR UPDATE
。
為了實現多粒度鎖機制,InnoDB
還有兩種意向鎖,都是表鎖,也是自動新增的:
- 意向共享鎖(
IS
):事務在加共享鎖之前,必須先取得該表的IS
鎖 - 意向排他鎖(
IX
):事務在加排他鎖之前,必須先取得IX
鎖
MVCC
和事務隔離級別
MVCC
多版本控制,一般是讀寫不阻塞的,可以提升併發效能,它是通過資料快照的方式來實現的。隱含的增加兩列,一個儲存了行的建立時間,一個儲存行的過期時間。對資料庫的任何修改的提交都不會直接覆蓋之前的資料,而是產生一個新的版本與老版本共存,使得讀取時可以完全不加鎖。基於MVCC
的實現基本上免除了大部分的鎖等待問題。
事務隔離級別是通過鎖來實現的,只是隱藏了加鎖的細節。
事務具有四大特性(ACID
),即原子性,一致性,隔離性和永續性。
如果不考慮隔離性,可能會出現:
- 髒讀:讀到其他事務未提交的資料
- 不可重複讀:讀到其他事務提交的資料
- 幻讀:讀到其他事務提交的資料,但是針對的是一批資料整體,例如針對同一查詢條件,多出幾行。
因而MySQL
提供了四種隔離級別:
Serializable
(序列化):可避免髒讀、不可重複讀、幻讀的發生。Repeatable read
(可重複讀):可避免髒讀、不可重複讀的發生(Innodb
和Xtradb
通過MVCC
解決幻讀的問題)。Read committed
(讀已提交):可避免髒讀的發生(只能讀取其他事務已經提交的結果,因而會帶來不可重複讀)。Read uncommitted
(讀未提交):最低級別,任何情況都無法保證。(事務中的修改,即使沒有提交,對其他事務也是可見的,很少使用)
預設情況下,MySQL
採用的是RR
級別。MVCC
只和隔離級別RR
和RC
相相容:對於RR
,需要讀取建立版本小於等於當前版本的最新的資料記錄;對於RC
,只要讀取建立版本最新的資料記錄。
樂觀鎖和悲觀鎖
-
樂觀鎖是一種思想,具體實現是,表中有一個版本欄位,第一次讀的時候,獲取到這個欄位。處理完業務邏輯開始更新的時候,需要再次檢視該欄位的值是否和第一次的一樣。如果一樣更新,反之拒絕。之所以叫樂觀,因為這個模式沒有從資料庫加鎖,等到更新的時候再判斷是否可以更新。
-
悲觀鎖是資料庫層面加鎖,會阻塞等待鎖。
因此MVCC
可以看做是一種解決讀寫衝突的無鎖併發控制,樂觀併發控制是用來解決寫寫衝突的無鎖併發控制。
間隙鎖
當我們用範圍條件檢索資料而不是相等條件檢索資料,並請求共享或排他鎖時,InnoDB
會給符合範圍條件的已有資料記錄的索引項加鎖;對於鍵值在條件範圍內但並不存在的記錄,也會加間隙鎖。它可以保證,在事務未提交之前,其他事務不能插入滿足其鎖定條件的任何記錄。例如:
Select * from emp where empid > 100 for update;
會對大於100
的所有行加上鎖,即使實際上記錄只到101
。
死鎖的避免
一般Mysql
會通過回滾解決死鎖的問題,但是我們仍然可以避免:
- 以固定順序訪問表和行
- 大事務拆小
- 一個事務中做到一次鎖定所需要的資源
- 降低隔離級別
優化鎖效能
- 儘量使用較低的隔離級別
- 設計索引使得加鎖更精確
- 選擇合理的事務大小
- 要修改資料的話,最好直接申請排他鎖
- 不同程式訪問表時,儘量以相同的順序訪問各表
- 儘量用相等條件訪問資料,避免間隙鎖的影響
- 除非必要,查詢時不要顯式加鎖
- 特定的事務可以使用表鎖來減少死鎖的可能
資料庫大表優化
常見優化措施:
- 限定資料範圍,禁止不帶任何限制資料範圍的查詢
- 讀/寫分離
- 使用快取
- 垂直分割槽,根據功能分表分庫,可以使得行資料變小,減少
I/O
,簡化表的結構,易於維護,但可能會有冗餘列 - 水平分割槽,比如可以根據使用者的地域或者名字等將資料分散到不同表或者庫
redis
與memcached
的區別
redis
支援豐富的資料型別redis
支援持久化,釋出訂閱等附加功能redis
擁有叢集模式memcached
是多執行緒,非阻塞IO
;redis
使用單執行緒,IO
多路複用
redis
常見資料結構
String
Hash
List
Set
:可以提供交集、並集、差集等操作Sorted Set
:可以實現排行榜等功能
redis
過期刪除和記憶體淘汰
過期刪除採用定期刪除+惰性刪除。
記憶體淘汰有6種策略:
volatile-lru
:從已設定過期的keys
中挑選最少使用的資料淘汰volatile-ttl
:從已設定過期的keys
選擇馬上要過期的資料淘汰volatile-random
:從已設定過期的keys
中隨機選擇資料淘汰allkeys-lru
:從所有keys
中選擇最少使用的資料淘汰allkeys-random
:所有keys
中隨機釋放no-enviction
:不刪除,記憶體不足時直接報錯
redis
持久化
持久化有兩種方式:RDB
和AOF
。
RDB
是預設使用的方式,根據資料庫中的字典生成檔案;而如果打開了AOF
,則預設使用AOF
恢復資料庫,AOF
是將更改資料庫的命令寫入。它有三種方式:
appendfsync always
:每次有修改都寫入appendfsync everysec
:每秒鐘同步一次appendfsync no
:由作業系統決定
AOF
重寫是指,當AOF
檔案過大時,產生新的AOF
檔案,它是根據現有資料的鍵值對實現,無須對現有的AOF
檔案進行讀寫分析,而是將需要通過多條命令實現的某個鍵值對狀態,儘可能使用更短的命令實現。
快取雪崩和快取穿透、快取擊穿
快取雪崩是指同一時間快取大面積失效(或者快取宕機),導致請求落在資料庫上,使得資料庫短時間內接受大量請求崩掉。
解決方案:
- 使用叢集保證
redis
服務可用性 - 過期時間加上隨機值,避免快取集體失效
- 針對快取雪崩發生時,對資料庫採用限流,防止其崩掉
快取穿透:因為大量請求redis
中不存在的key
導致,快取不起作用所有請求落在資料庫上
解決方案:
- 布隆過濾器,攔截非法
key
- 即使空結果,也進行快取,但設定短的過期時間
快取擊穿:大量查詢一個正好失效的key
,導致瞬時DB
請求量大
解決方案:
- 互斥鎖
redis
的併發競爭問題
多個程序同時操作一個key
時,需要使用分散式鎖。可以使用redis
,也可以使用zookeeper
或者mysql
實現。
針對單機情況,可以使用事務機制。
如何保證快取與資料庫雙寫一致
看了知乎,有如下機制:
- 等待
redis
中的資料自動過期,但是會有過期時間這麼長一段時間的資料不一致 - 使用程式碼更新
DB
,然後刪除redis
中的key
,但是也會有一小段不一致的時間,可以滿足絕大部分場景 redis
中的資料不過期,只要DB
更新,就採用背景任務更新redis
,訪問者永遠只訪問redis
。不一致的時間取決於,如果是定時更新任務,那麼就是更新任務的時間間隔;如果是佇列模式,取決於佇列的產生和消費的延遲- 針對
3
的變體,使用者寫往DB
寫入,但是永遠只讀redis
,仍然由DB
更新redis
綜上,總是會有一段時間不一致。即CAP
問題,如果需要保證強一致問題CP
,那麼需要犧牲A
:
- 不使用
redis
,只使用單點DB
- 使用分散式協議,比如
2PC
、3PC
,或者分散式鎖。
所以,要麼接受最終一致,要麼要求強一致,在分散式情況下就要犧牲A
。
CHAR
和VARCHAR
- char:儲存定長資料,最大隻能儲存
255
個字元。CHAR
欄位上的索引效率級高,必須在括號裡定義長度,可以有預設值,比如定義char(10)
,那麼不論你儲存的資料是否達到了10
個字元,都要佔去10
個字元的空間(自動用空格填充),且在檢索的時候查詢條件後面的空格會被移除(左邊的會保留),儲存的資料也會移除右側的空格。 - varchar:儲存變長資料,但儲存效率沒有
CHAR
高,必須在括號裡定義長度,可以有預設值。儲存資料的時候,不進行空格自動填充,而且如果資料存在空格時,當值儲存和檢索時尾部的空格仍會保留。另外,varchar
型別的實際長度會長1-2個位元組(用來指示儲存值的位元組數),如果小於等於255個位元組,使用1個位元組,大於255個位元組,使用2個位元組。儲存時VARCHAR
並不會移除左右的空格,但是查詢時會移除查詢條件右邊的空格。 - text:儲存可變長度的非
Unicode
資料,最大長度為2^31-1
個字元。text
列不能有預設值,儲存或檢索過程中,不存在大小寫轉換,後面如果指定長度,不會報錯誤,但是這個長度是不起作用的,意思就是你插入資料的時候,超過你指定的長度還是可以正常插入。
MySQL
事務的實現
原子性:需要在異常發生時進行回滾,因此需要依靠回滾日誌(undo log
)。他有如下特點:
- 每條資料變更(
insert
/update
/delete
)操作都伴隨一條undo log
的生成,並且回滾日誌必須先於資料持久化到磁碟上 - 所謂的回滾就是根據回滾日誌做逆向操作,比如
delete
的逆向操作為insert
,insert
的逆向操作為delete
,update
的逆向為update
等
永續性:使用重做日誌(redo log
)實現的,它包括兩個部分:
- 記憶體中的重做日誌緩衝區
- 磁碟上的重做日誌檔案
這樣一來,嘗試對事務進行修改時,會經歷以下步驟:
- 資料從磁碟讀入記憶體
- 更新記憶體中快取的資料
- 生成重做日誌寫入重做日誌緩衝區
- 提交時,將緩衝區中的內容重新整理到磁碟的重做日誌檔案
- 將記憶體中的資料更新到磁碟
因此,重做日誌是用來記錄已經成功提交事務的修改資訊(在日誌重新整理到磁碟中之後,在資料從記憶體更新到磁碟中之前),即使宕機了,也可以在讀取redo log
之後恢復最新資料。
重做日誌是以512位元組塊的形式儲存的,塊大小與磁碟扇區大小相同。重做日誌可以保證原子性。
隔離性:由鎖+MVCC
進行實現。
drop
、delete
和truncate
drop
直接刪掉表,將表所佔有的空間全部釋放掉delete
每次從表中刪除一行,並且將操作作為事務記錄儲存在日誌中以便進行回滾,不會減少表或者索引所佔的空間truncate
刪除的是表中的資料,表和索引佔用的空間會恢復初始大小truncate
和drop
都無法回滾- 如果需要保留
id
的計數,使用delete
- 如果具有外來鍵約束,不能使用
truncate
區域性性原理
磁碟讀取過程:
- 磁頭尋找柱面,所耗費的時間即尋道時間
- 目標扇區旋轉到磁頭下,所耗費的時間即旋轉時間
- 資料在磁碟與記憶體之間的實際傳輸時間
遵循從上到下,從外到內,按照柱面進行的規則。
由於儲存介質的特性,磁碟本身存取就比主存慢很多,再加上機械運動耗費,磁碟的存取速度往往是主存的幾百萬分之一,因此為了提高效率,要儘量減少磁碟I/O
。為了達到這個目的,磁碟往往不是嚴格按需讀取,而是每次都會預讀,即使只需要一個位元組,磁碟也會從這個位置開始,順序向後讀取一定長度的資料放入記憶體。這樣做的理論依據是電腦科學中著名的區域性性原理,即``CPU訪問儲存器時,無論是存取指令還是存取資料,所訪問的儲存單元都趨於聚集在一個較小的連續區域中:
- 時間區域性性:如果一個資訊項正在被訪問,那麼在近期它很可能還會被再次訪問。
- 空間區域性性:在最近的將來將用到的資訊很可能與現在正在使用的資訊在空間地址上是臨近的。
預讀的長度一般為頁(page)
的整倍數。
資料庫正規化
一看就懂就懂的資料庫正規化介紹(1NF,2NF,3NF,BC NF,4NF,5NF)
如何解釋關係資料庫的第一第二第三正規化? - 王紅波的回答 - 知乎
第一正規化:原子性,每個欄位都不可再分割
第二正規化:唯一性,有主鍵標識其唯一性,非主屬性不能部分依賴碼
第三正規化:非主鍵欄位不能相互依賴,每一列只與主鍵有直接關係,不存在傳遞依賴
儲存過程與觸發器
只有呼叫的時候才會執行儲存過程,而觸發器是一種特殊的儲存過程,主要通過事件執行觸發而被執行。
內連線與外連線
內連線(inner join
,inner
可以省略):只有某個條件是左表與右表中相同,才會保留結果
外連線:
- 左連線:以左表為主,保留左表所有行,右表不能匹配的置為空
- 右連線:與左連線相反
Mysql
架構
Mysql
架構包括三層:
- 客戶端的連線管理與認證:每個客戶端在伺服器中擁有一個執行緒,伺服器會負責快取執行緒,並且基於使用者名稱和密碼對客戶端進行認證
- 伺服器層:解析查詢,分析、優化,快取(
select
查詢),內建函式。同時它還提供跨儲存引擎的功能,包括儲存過程、觸發器和檢視等。 - 引擎層:負責存取資料,伺服器層通過
API
與其通訊。