【從入門到入土】使人脫髮的數據庫底層設計

前言

說到數據庫這個詞,我只能用愛恨交加這個詞來形容它。兩年前在本身還單純懵懂的時候進了數據庫的課堂,聽完數據庫的課,以爲這是一門再簡單不過的課程,任何一門編程語言都比SQL要晦澀難懂,任何一門理論課程都比數據庫關係要複雜得多。直到從被面試官按在地上摩擦,到工做中那一條條使人髮指的慢查詢SQL,這就已經徹底顛覆了我對數據庫的見解。在有各類數據庫工具的今天,咱們是看不到那簡單到不能再簡單的一張表的背後,隱藏着多少數據結構的支撐,也看不到咱們隨手敲的一條SELECT,背後會有多少算法和數據結構在給咱們作優化,做爲一個有技術熱情的coder,應該須要對咱們每日在用的數據庫作一次深刻了解。面試

數據庫架構

  • 如何設計一個關係型數據庫

    這個問題很寬泛,須要咱們對於總體有一個掌控,對咱們平時所用的數據庫要有足夠的瞭解,對整個數據庫作模塊劃分是這道題的關鍵,這就更須要咱們足夠了解數據庫,對數據庫作一個合理的模塊設計。算法

  • 設計

    從開始,首先要明白,數據庫的最最根本的做用是什麼——存儲數據,因此咱們須要一個存儲模塊來存儲咱們的數據,它能夠是一個文件系統(機械硬盤?SSD固態硬盤?)。但光有存儲模塊是不夠的,咱們還須要一個程序實例,來組織或者獲取這些數據,在程序實例中咱們須要提供獲取和組織這些數據的方式,因此咱們須要在程序實例中實現存儲管理模塊。咱們還能夠在存儲管理模塊中作一些提高效能的工做,例如同時讀取多行分塊分頁存儲等。數據庫做爲一款對性能要求極高的軟件,咱們應該加入緩存機制,來提升其速度,當查詢到緩存中已存在的數據,咱們應該直接將其從緩存中讀取,這樣能夠減小硬盤IO次數,提升效能。到這裏爲止,咱們已經完成了對數據庫的存儲方面的功能設計,可是做爲數據庫,應該須要提供給用戶對數據進行增刪改查的接口,即平時所寫的SQL,因此咱們應該提供一個SQL解析模塊,來對平常用戶所寫的SQL語句進行解析,轉換成機器可識別的指令,咱們也能夠直接將編譯過的SQL加入緩存,下次再有一樣的SQL就直接從緩存中讀取,一樣能夠提升效能。做爲一款成熟的數據庫,須要應對各類複雜的環境,要時刻記錄數據庫的狀態,因此咱們還須要一個日誌管理模塊,對操做和錯誤信息進行記錄。數據庫中須要支持多用戶操做,但每一個用戶都能操做全部的數據,這是不現實的,因此還須要權限劃分模塊對數據庫用戶進行權限管理。固然數據庫說到底也只是一款軟件,是軟件就會有bug,就會出故障,故障不可怕,可怕的是在數據庫這種敏感軟件下對故障沒有特殊的處理方式,致使數據丟失,畢竟數據是無價的,因此數據庫應該引入容災機制,在數據庫掛了的時候,對數據進行恢復。還有做爲數據庫最重要的兩個模塊,也是現今任何一個數據庫都須要考慮的問題——併發和查找效率,因此還需引入索引這兩個模塊,這就是實現一個最基礎的數據庫所必需的幾大模塊。sql

  • 概括

    綜上對數據庫設計模塊作一個彙總:
    1.存儲模塊
    2.程序實例
    2.1存儲管理模塊
    2.2緩存機制
    2.3SQL解析模塊
    2.4日誌管理模塊
    2.5權限劃分模塊
    2.6容災機制
    2.7索引模塊
    2.8鎖模塊
    數據庫

    設計數據庫模塊.

索引

  • 爲何要使用索引

    要考慮這個問題,首先要從最基礎的查找表中數據的過程開始提及。一般咱們在查找一個序列中的某一個元素時,用到的最簡單的方式就是遍歷,數據庫也是同樣,在一張表中查找某一行數據時,若是不考慮索引的情況下,也會採用一個逐行掃描的方式,只不過數據庫一般以塊或者頁爲單位,因此它一般將整個塊或者頁加載進內存,而後逐塊輪詢查找到結果並返回。若是數據庫中只有少許數據,那麼進行全表掃描,速度仍是會很快,可是若是在數據量很大的表中,這種方法就再也不適用了,在數據量很大的表中,因爲逐行掃描代價變大,一般須要避免採用這種逐行掃描的方式進行數據查找,數據庫爲了使查詢變得高效,因此引入了索引這種方式對數據進行查找。編程

  • 什麼樣的信息能成爲索引

    1.主鍵、惟一鍵、普通鍵緩存

  • 索引的數據結構

    • 二叉查找樹

      衆所周知,二叉查找樹是每一個節點最多由兩個子樹的樹結構,而其還有一個特色是,在任意一顆樹中,根節點左孩子永遠小於根節點,根節點右孩子永遠大於根節點,用二叉查找樹做爲索引,確實能夠提升查找效率,其可使用二分查找將時間複雜度控制在O(lgn),可是二叉查找樹有一個顯而易見的缺陷,當某種特殊狀況(按照某個特定順序插入樹)發生時,二叉查找樹將變爲下圖右側(線性二叉樹)的情況: bash

      二叉查找樹.jpg
      此時二叉查找樹查找任意某個元素時,其查找順序與逐行查找無異,查詢時間複雜度又將回到O(n),查詢效率沒法保持。

    • B-Tree

      B-Tree,平衡多路查找樹,若是每一個節點,最多有N個孩子,那麼這樣的樹就叫N階B-Tree, 每一個節點中主要包含關鍵字指向孩子的指針,最多能有幾個孩子,取決於節點的容量和數據庫的相關配置,一般狀況下這個N是很大的。
      B-Tree做爲一種數據結構,有以下特徵:
      1.根節點至少包含兩個孩子
      2.樹中每一個節點至多含有N個孩子(N>=2)
      3.除根節點和葉節點外,其它每一個節點至少有ceil(N/2)個孩子。(ceil表示取上限,例如1.2的上限爲2,1.1的上限也爲2,非四捨五入
      4.全部葉子節點都位於同一層,即葉子節點的高度都是同樣的。
      5.假設每一個非終端節點包含n個關鍵字信息(P0,P1...Pn,k1...kn)
      網絡

      ( a )ki(i=1..n)爲關鍵字,且關鍵字按順序升序排序k(i-1)<ki。
      ( b )關鍵字的個數必須知足:[ceil(m/2)-1]<=n<=m-1]。
      ( c )非葉子節點的指針:P[1],P[2]...P[M];其中P[1]指向關鍵字小於K[1]的子樹,P[N]指向關鍵字大於K[N-1]的子樹,其它P[i]指向關鍵字屬於(K[i-1],K[i])的子樹。
      數據結構

      B-Tree
      遵照上述規則,其目的就是儘可能使每一個索引塊都儘量多的存儲數據,儘量減小查找次數以提高效率。 舉個例子,模擬一下查找過程,以便於理解:假設咱們要查詢關鍵字爲10的數據,則從根節點出發,10<17,因而經過P1進入其孩子節點,10>8且10<12,因而經過P2進入其孩子節點,最後尋找到10。若是不使用索引,而使用逐行掃描的方式進行查找,則從0開始至少掃描10次才能查找到10號數據,有了索引以後能夠看到,查找次數從10變爲3,大大提升了查找效率。
      若是這裏是二叉查找樹,會出現極端狀況,使得查找時間複雜度爲O(n),而若是是B-Tree,因爲上述五個約束,可讓節點經過合併、分裂、上移、下移等操做,使得樹高度較二叉查找樹小,查找效率顯然更高。

    • B+ -Tree(MySQL)

      B+ -Tree是B-Tree的一個變體,其定義基本與B樹相同,除了:
      1.非葉子節點的子樹指針與關鍵字個數相同,其代表B+樹能存儲更多的關鍵字
      2.非葉子節點的子樹指針P[i],指向關鍵字值[K[i],K[i+1])的子樹。
      3.非葉子節點僅用來作索引,數據到保存在葉子節點中。(B+樹的全部檢索都是從根部開始,直到搜索到葉子節點結束。)
      4.全部葉子節點均有一個鏈指針,指向下一個葉子節點。(方便直接在葉子節點直接作範圍統計)
      架構

      B+Tree.
      B+樹相較於B樹的優點:
      1.B+樹的磁盤讀寫代價更低。
      2.B+樹的查詢效率更加穩定。
      3.B+樹更有利於對數據庫的掃描。

    • Hash

      Hash索引是根據Hash結構的定義,只須要一次運算即可以找到數據所在位置,不像B+樹或者B樹須要從根結點出發尋找數據,因此Hash索引的查詢效率理論上要高於B+樹索引,可是MySQL中並無採用這一種索引,這是因爲這種索引除查詢效率以外的缺陷是十分明顯的。
      1.僅僅只能知足"=","IN",不能使用範圍查詢。因爲其是由Hash運算獲取的數據存放位置,每次Hash運算獲取的是一個肯定的值,且這個值並不與數據自己的大小有關係,因此其並不能知足範圍查詢。
      2.沒法被用來避免數據的排序操做。和1的意思差很少,Hash的索引值是由Hash運算獲取的,其索引值與數據自己的大小並沒有明顯關係。
      3.不能利用部分索引鍵查詢。
      4.不能避免表掃描。因爲Hash索引會產生Hash衝突,存在Hash衝突的數據會被鏈接到同一個鏈表上,當大量數據被鏈接到相同鏈表上時,查詢某條數據就變成了掃描該鏈表,時間複雜度並不能保證在O(1)。
      5.遇到大量Hash值相等的狀況後性能並不必定就會比B-Tree索引高。

    • BitMap索引

      位圖索引,當表中的某個字段只有幾種值的時候,例如:性別,此時用位圖索引是一個最佳的選擇。目前使用位圖索引的比較主流的數據庫有Oracle數據庫。

  • 密集索引和稀疏索引的區別

    1.密集索引文件中的每一個搜索碼都對應一個索引值,稀疏索引文件只爲索引碼的某些值創建索引項。
    2.密集索引將數據存儲與索引放到了一塊,找到索引也就找到了數據,稀疏索引將數據和索引分開存儲,索引結構的葉子節點指向數據的對應行。

    • 關於MySQL的MyISAM和InnoDB
      MyISAM不管是主鍵索引、惟一鍵索引、仍是普通索引,都採用的是稀疏索引,而InnoDB必須有且僅有一個密集索引。
      InnoDB密集索引規則:
      1.若一個主鍵被定義,該主鍵則做爲密集索引。
      2.若沒有主鍵被定義,該表的第一個惟一非空索引則做爲密集索引。
      3.若不知足以上條件,InnoDB內部會生成一個隱藏主鍵(密集索引)。
      4.非主鍵索引存儲相關鍵位和其對應的主鍵值,包含兩次查找。
      :InnoDB若是查詢條件爲主鍵索引,則只需查詢一次,可是輔助索引須要查詢兩次,先經過輔助索引查詢到主鍵索引,再查詢到數據。
      聚簇索引和非聚簇索引
      從上圖中能夠看到,若是一個索引是彙集索引,則其葉子節點上存放的便是數據自己,而若是一個索引是稀疏索引,葉子節點存放的僅是地址,指向將要查找的數據。
  • 如何定位並優化慢查詢SQL

    首先先創建一張表

    CREATE DATABASE sqltest;
    use sqltest;
    create table tb_test(
      test_id int primary key auto_increment,
      test_name varchar(1024),
      test_date datetime,
      test_desc varchar(1024)
    );
    
    複製代碼

    在這張表中灌入200w數據。

    灌入數據.
    1.根據慢日誌定位慢查詢SQL

    #查找慢日誌
    slow_query_log
    複製代碼

    慢日誌.
    記錄慢日誌SQL運行時間閾值
    記入慢日誌閾值.

    設置慢查詢閾值爲1秒,重連數據庫 set global long_query_time = 1;

    修改慢查詢閾值.

    製造慢查詢:

    製造慢查詢
    慢查詢條數.
    2.使用explain工具分析SQL explain select test_name from tb_test order by test_name desc

    explain工具
    type:找到數據的方式,根據效率從高到低排序有以下幾種 system>const>eq_ref>ref>fulltext>ref_or_null>index_merge>unique_subquery>index_subquery>range>index>all 若是type爲index或all,表示本次掃描爲全表掃描,說明這個語句是須要優化的。

    extra:能夠用來輔助type幫助咱們進行SQL優化,extra中出現如下兩項,意味着MySQL根本不能使用索引,效率會受到重大影響,應該儘量對此進行優化。
    Using filesort:表示MySQL會對結果使用一個外部索引排序,而不是從表裏按索引次序讀到相關內容,可能在內存或者磁盤上進行排序。MySQL中沒法例用索引完成的排序操做稱爲「文件排序」。
    Using temporary:表示MySQL在對查詢結果排序時使用臨時表,常見於排序 order by和分組查詢group by。
    3.修改SQL,儘可能讓SQL走索引
    咱們能夠知道,建立表時,咱們將id設爲主鍵,那麼id也就天然稱爲了索引,因此咱們只要修改排序字段爲id,便可以經過索引排序。
    explain select test_id from tb_test order by test_id desc

    使用索引
    key:PRIMARY 表明使用了主鍵索引
    索引效率
    另外一種狀況,在特定情況下必定須要使用name進行排序,那還有一種作法,就是將name字段加索引。

    #加索引
    ALTER TABLE tb_test add index index_name(test_name);
    #再次分析
    explain select test_name from tb_test order by test_name desc;
    複製代碼

    結果:

    添加索引
    添加索引效率.

  • 聯合索引的最左匹配原則的成因

    上文中只是用了單一索引對錶進行排序,若是使用聯合索引又會是什麼樣的一種情況? 最左匹配原則:假設數據表中有兩列,A and B,咱們將A、B設置爲聯合索引,而後在where語句中調用where A = ? AND B = ?,該查詢語句會使用AB聯合索引,調用where A = ?,該查詢語句也會使用AB聯合索引,但當調用where B = ?時,它將不會使用AB聯合索引。
    官方定義

    1.最左前綴匹配原則,MySQL會一直向右匹配直到遇到範圍查詢(>、<、between、like)就中止匹配,好比 a=3 and b=4 and c>5 and d=6,若是創建(a,b,c,d)順序的索引,d是沒法使用索引的,若是創建(a,b,d,c)的索引則均可以使用到,a、b、d的順序能夠任意調整。
    2.=和in能夠亂序,好比 a=1 and b=2 and c=3 創建(a,b,c)索引能夠任意順序,MySQL的查詢優化器會幫你優化成索引能夠識別的形式。

  • 索引是創建的越多越好嗎

    答:NO,數據量小的表不須要創建索引,創建會增長額外的索引開銷。另外數據變動須要維護索引,所以更多的索引意味着更多的維護成本。更多的索引還須要消耗更多的空間。

鎖 and 鎖 and 鎖

  • MyISAM與InnoDB關於鎖方面的區別是什麼

    1.MyISAM默認使用表級鎖,不支持行級鎖。 2.InnoDB默認使用行級鎖,也支持表級鎖。 3.InnoDB在使用索引時,默認使用行級鎖,但當其沒有用到索引時,默認使用表級鎖。

    • 表級鎖

      當一條數據庫語句對某條數據進行操做時,默認將整張表進行加鎖,其他操做沒法對錶進行更改,需等到當前操做執行完畢釋放鎖後,才能對該表進行更改。
    • 行級鎖

      當一條數據庫語句對某條數據或多條數據進行操做時,默認將正在操做的這幾條數據進行加鎖,其他操做沒法對當前語句正在操做的這幾條數據進行操做,但能夠操做其餘數據。
    • 共享鎖和排他鎖

      共享鎖:當對某行或某張表上了共享鎖以後,其它任意語句依然支持上共享鎖,但不支持上排他鎖。(MySQL使用lock in share mode加共享鎖) 排他鎖:當對某行或某張表上了排他鎖以後,其它任意語句對這一行或者這一張表進行讀或寫都是不容許的。(MySQL使用 for update 加排他鎖)
    • 樂觀鎖和悲觀鎖

      悲觀鎖:老是假設最壞的狀況,認爲競爭老是存在,認爲每次對數據的修改總會產生衝突,所以每次都會先上鎖,其餘線程阻塞等待釋放鎖。 樂觀鎖:老是假設最好的狀況,認爲競爭老是不存在,認爲每次對數據的修改都不會產生衝突,所以不會先上鎖,再最後更新的時候,比較數據是否已經被更新,可用版本號或CAS實現。
    • 行級鎖必定比表級鎖優嗎?

      答:不必定,鎖的粒度越細,其消耗的資源代價越高。行級鎖在上鎖的時候須要掃描到某行再進行上鎖,這樣的代價是較大的。
    • MyISAM和InnoDB各自適合的場景

      MyISAM適合的場景:1.頻繁執行全表count語句。2.對數據進行增刪改的頻率不高,查詢很是頻繁。3.沒有事務 InnoDB適合的場景:1.數據增刪改查都至關頻繁。2.可靠性要求比較高,存在事務。
  • 數據庫事務四大特性ACID

    事務:做爲單個邏輯單元執行的一個操做,要麼所有完成,要麼所有失敗。

    • 原子性

      事務包含的全部操做,要麼所有執行,要麼所有失敗。
    • 隔離性

      多個事務併發執行時,一個事務的執行不該該影響其它事務的執行。
    • 一致性

      事務應保證數據庫狀態從一個一致性狀態轉換到另外一個一致性狀態。(一致性狀態:數據庫的數據應知足完整性約束)
    • 持久性

      一個事務一旦提交,它的數據應該永久地保存在數據庫中。
  • 事務隔離級別以及各級別下的併發訪問問題

    1.更新丟失 —— 一個事務的更新,引發另外一個事務提交的丟失,MySQL全部事務隔離級別在數據庫層面上都可避免。
    2.髒讀 —— 一個事務讀到另外一個事務未提交的數據,READ-COMMITTED事務隔離級別以上可避免。(ORACLE默認隔離級別爲READ-COMMITTED)
    3.不可重複讀——事務A屢次讀取同一數據時,事務B修改該數據,致使事務A每次讀取到的數據結果不一致,REPEATABLE-READ隔離級別下可避免。
    4.幻讀——事務A屢次讀取同一數據時,事務B對事務A的影響區間內進行增刪操做,致使事務A讀取到的數據一會多、一會少,就像產生幻覺了同樣。SERIALIZABLE事務隔離級別可避免。(但實際上MySQL的InnoDB在REPEATABLE-READ隔離級別下避免了幻讀的發生)

    事務隔離級別

  • InnoDB可重複讀隔離級別下如何避免幻讀

    • 表象

      快照讀(非阻塞讀)--僞MVCC。使用此種機制避免使咱們看到「幻行」。
      當前讀:select ...lock in share mode,select ...for update,update,delete,insert.即加了鎖的增刪改查語句。因爲其讀取的是記錄的最新版本,因此稱爲當前讀。
      快照讀:不加鎖的非阻塞讀(非SERIALIZABLE),select。基於提高併發性能的考慮,基於多版本併發控制。既然是基於多版本,也就是說快照讀有可能讀到非最新版本的數據。
    • 內在

      next-key鎖(行鎖+gap鎖):next-key鎖由兩部分組成,行鎖+gap鎖,行鎖即對單個行記錄上的鎖。Gap鎖,即對插入索引間的空隙上鎖,即鎖定一個範圍,但不包括記錄自己。Gap鎖的目的,是爲了防止一個記錄兩次當前讀出現幻讀的狀況。Gap鎖只存在與Read-Committed(不包括Read-committed)以上的隔離級別存在。若是查詢時,where條件所有命中(精確查詢時),則不會用Gap鎖,只會加記錄鎖。由於在精確查詢的情況下,即便在讀結果集的過程當中,另外一個事務增長一條數據,也不會增長到當前結果集下,只會在where條件的範圍以外,因此並不會產生幻讀現象,加行鎖就足夠了。若是where條件部分命中或者全不命中,則會加Gap鎖
  • RC、RR級別下的InnoDB的非阻塞讀(快照讀)如何實現

    1.數據行裏的DB_TRX_ID、DB_ROLL_PTR、DB_ROW_ID字段,DB_TRX_ID標識最近一次對本行記錄作修改的事務ID,DB_ROLL_PTR回滾指針,當事務回滾時,去往undo日誌尋找上一版本的數據,DB_ROW_ID行號(MySQL自動建立的隱藏自增主鍵)。
    2.undo日誌:存儲歷史版本的數據。當某行的某個字段進行修改時,首先用排他鎖鎖住該行,而後將該行數據拷貝一份放入undolog中,經過行中的DB_ROLL_PTR指針,指向undolog中的這條數據,而後修改當前行的值,並填寫DB_TRX_ID字段爲當前事務的ID。

    RCRR級別下非阻塞讀
    3.read view:可見性判斷。決定當前事務能看到的是哪一個版本的數據。

結語

使人頭禿。

本文圖片來自網絡,侵刪。

歡迎你們訪問個人我的博客:Object's Blog

相關文章
相關標籤/搜索