12道Mysql常見的面試題

photo-1589064090574-7be967916250.jpeg

原文: https://github.com/lvCmx/study

有關事務的面試題

(1) 事務的特性java

  • 原子性:是指事務包含全部操做要麼所有成功,要麼所有失敗回滾。
  • 一致性:指事務必須使數據庫從一個一致性狀態變換成另外一個一致性狀態,也就是說一個事務執行以前和執行以後都必須處於一致性狀態。
    拿轉帳來講,假設用戶 A 和用戶 B 二者的錢加起來一共是 5000,那麼無論 A 和 B 之間如何轉帳,轉幾回帳,事務結束後兩個用戶的錢相加起來應該還得是 5000,這就是事務的一致性。
  • 隔離性:是當多個用戶併發訪問數據庫時,好比操做同一張表時,數據表爲每一個用戶開啓的事務,不能被其餘事務所幹擾,多個併發事務之間要相互隔離。
  • 持久性:持久性是指一個事務一旦被提交,那麼對數據庫中的數據的改變就是永久的,即使是在數據庫系統遇到故障的性況下也不會丟失提交事務的操做。

(2) 併發操做問題mysql

  • 髒讀:髒讀是指在一個事務處理過程當中讀取到了另一個未提交事務中的數據。
  • 不可重複讀:不可重複讀是指在對於數據庫中的某個數據,一個事務範圍內屢次查詢卻返回了不一樣的數據值,這是因爲在查詢間隔,被另外一個事務修改並提交了。
  • 虛讀(幻讀):幻讀發生在當兩個徹底相同的查詢執行時,第二次查詢所返回的結果集跟第一個查詢不相同。
    好比兩個事務操做,A 事務查詢狀態爲 1 的記錄時,這時 B 事務插入了一條狀態爲 1 的記錄,A 事務再次查詢返回的結果不同。

(3) 事務的隔離級別git

  • Serializable(串行化):可避免髒讀、不可重複讀、幻讀。(就是串行化讀數據)
  • Repeatable read(可重複讀):可避免髒讀、不可重複讀的發生。
  • Read committed(讀已提交):可避免髒讀的發生。
  • Read uncommitted(讀未提交):最低級別,任何狀況都沒法保證。

在 MySQL 數據庫中,支持上面四種隔離級別,默認的爲 Repeatable read (可重複讀);而在 Oracle 數據庫中,只支持 Serializable (串行化)級別和 Read committed (讀已提交)這兩種級別,其中默認的爲 Read committed 級別。github

數據庫索引

索引是表的目錄,在查找內容以前能夠先在目錄中查找索引位置,以此快速定位查詢數據。對於索引,會保存在額外的文件中。
索引是數據庫中專門用於幫助用戶快速查詢數據的一種數據結構,相似於字典中的目錄,查找字典內容時能夠根據目錄查找到數據的存放位置,而後直接獲取便可。面試

  • 索引由數據庫中一列或多列組合而成,其做用是提升對錶中數據的查詢速度。
  • 索引的優勢是能夠提升檢索數據的速度。
  • 索引的缺點是建立和維護索引須要耗費時間。
  • 索引能夠提升查詢速度,會減慢寫入速度。

索引自己也很大,不可能所有存儲在內存中,所以索引每每索引文件的形式存儲在磁盤上。索引查找過程當中就要產生磁盤 IO 消耗,相對於內存存取,IO 存取的消耗要高几個數量級。因此評價一個數據結構做爲索引的優劣最重要的指標就是在查找過程當中磁盤 I/O 操做次數的漸進複雜度。(換句話說,索引的結構組織要儘可能減小查找過程當中磁盤 I/O 的存取次數。)redis

(1) 索引的種類算法

​ B-tree、Hash、R-Tree、全文索引、主鍵索引、普通索引等。在 mysql 中索引是在存儲引擎層而不是服務器層實現的,因此沒有統一的索引標準。sql

  • 普通索引:僅加速查詢
  • 惟一索引:加速查詢+列值惟一。(能夠有 null)
  • 主鍵索引:加速查詢+列值惟一+表中只有一個(不能夠有 null)
  • 組合索引:多列值組成一個索引。專門用於組合搜索,其效率大於索引合併。
  • 全文索引:對文本的內容進行分詞,進行搜索。

B-Tree 索引:數據庫

​ B-Tree 無論葉子節點仍是非葉子節點,都會保存數據,這樣致使在非葉子節點中能保存的指針數量變少,指針少的狀況下要保存大量數據,只能增長樹的高度,致使 IO 操做變多,查詢性能變低。而每個頁的存儲空間是有限的,若是 data 數據較大時將會致使每一個節點(一個頁)能存儲的 key 的數量很小,當存儲的數據量很大時一樣會致使 B-Tree 的深度較大,增大查詢時的磁盤 IO 次數,進而影響查詢效率。數組

​ 在 B+Tree 中,全部數據記錄節點都是按照鍵值大小順序存放在同一層的葉子節點上,而非葉子節點上只能存儲 key 值信息,這樣能夠大大加大每一個節點存儲的 key 值數量,下降 B+Tree 的高度。

​ 數據庫中的 B+Tree 索引能夠分爲彙集索引(clustered index)和輔助索引(secondary index)。上面的 B+Tree 示例圖在數據庫中的實現即爲彙集索引,彙集索引的 B+Tree 中的葉子節點存放的是整張表的行記錄數據。輔助索引與彙集索引的區別在於輔助索引的葉子節點並不包含行記錄的所有數據,而是存儲相應行數據的彙集索引鍵,即主鍵。當經過輔助索引來查詢數據時,InnoDB 存儲引擎會遍歷輔助索引找到主鍵,而後再經過主鍵在彙集索引中找到完整的行記錄數據。

hash 索引

在進行查詢操做時,使用 hash 索引效率很高。所以,當使用一個語句去比較字符中,而後返回結果集。這樣的操做使用 hash 索引是很快的。

在字符串上建立 Hash 索引很是好,列值將插入到 Hash 表中和一個鍵對應,並和實際的數據行有一個映射關係,也就是該鍵是一個指向表中數據行的指針。Hash 表實際是基於關聯數組,假若有一個這樣的語句:「boyce = 0×28936」其中 0×28936 是關聯到存儲在內存中的 boyce。在 hash 表索引中查找 boyce 的值並返回內存中的數據,要比檢索整個表的列值要快得多。

Hash 表不能進行排序的數據結構,Hash 表擅長的是鍵值對,也就是說,檢索語句檢查相等性。在 Hash 表中鍵值是沒有排序的,在存儲的時候也沒有任何的排序規則。由於 hash 索引不夠靈活,因此,hash 索引不是默認的索引的數據結構。

  • 不是按照索引順序存儲,不能用於排序。
  • 不支持部分索引列匹配查找。
  • 支持等值查詢,不支持範圍查詢。
  • 哈希值衝突多時,不適用。

    全文索引

​ 全文索引是一種特殊類型的索引,它查找的是文本中的關鍵詞,而不是直接比較索引中的值。全文索引和其餘幾種索引的匹配方式徹底不同,它更相似於搜索引擎作的事情,而不是簡單的 where 條件匹配。能夠在相同的列上,同時建立全文索引和 B-Tree 索引,全文索引適用於 Match Against 操做,而不是普通的 where 條件操做。

​ 索引能夠包含一個列(即字段)或多個列的值。若是索引包含多個列,通常會將其稱做複合索引,此時,列的順序就十分重要,由於 MySQL 只能高效的使用索引的最左前綴列。建立一個包含兩個列的索引,和建立兩個只包含一列的索引是大不相同的。

(2) B+Tree 索引

在 B+Tree 的每一個葉子節點增長一個指向相鄰葉子節點的指針,就造成了帶有順序訪問指針的 B+Tree。作這個優化的目的是爲了提升區間訪問的性能,例如圖中若是要查詢 key 爲從 18 到 49 的全部數據記錄,當找到 18 後,只需順着節點和指針順序遍歷就能夠一次性訪問到全部數據節點,極大提到了區間查詢效率。

  • B+Tree 它的非葉子節點不存儲數據,只存儲索引,而數據會存放在葉子節點中。
  • B+Tree 規定:全部的葉子結點包含了所有關鍵字信息,以及指向含這些關鍵字記錄的指針,且葉子結點自己依關鍵字的大小自小而大順序連接。

在 InnoDB 存儲引擎中,數據存放的方式是以頁的方式進行存放,計算機在存儲數據的時候,有最小存儲單元,就是最小數據扇區,一個扇區的大小是 512 字節,而文件系統(ext4)他的最小單元是塊,一個塊的大小是 4k,而對於咱們的 InnoDB 存儲引擎也有本身的最小存儲單元--頁。一個頁的大小是 16K。在 InnoDB 中默認的數據頁的大小是 16K。

數據表中的數據都是存儲在頁中的,因此一個頁中能存儲多少行數據呢?假設一個數據的大小是 1K,那麼一個頁能夠存放 16 行這樣的數據。

  • InnoDB 存儲引擎的最小存儲單元是頁,頁能夠用於存放數據也能夠用於存放鍵值+指針,在 B+樹中葉子節點存放數據,非葉子節點存放鍵值+指針
  • 索引組織表經過非葉子節點的二分查找法以及指針肯定數據在哪一個頁中,進頁在雲數據頁中查找到須要的數據。

例如:
假設 B+樹高爲 2,即存在一個根節點和若干個葉子節點,那麼這棵 B+樹的存放總記錄數爲:根節點指針數*單個葉子節點記錄行數。
假設一行數據大小爲:1KB,那麼一頁(16KB)中能夠存放 16 行數據。
假設主鍵 ID 爲 bigint 類型,長度爲 8 字節,而指針大小在 InnoDB 源碼中設置爲 6 字節,這樣一共 14 個字節。(8+6 的意思是 B+Tree 有 key 和指針)
16KB * 1024(化成字節) / 14 =1170(一個節點能夠存放頁的指針數據)
那麼能夠算出一棵高度爲 2 的 B+樹,能存放 1170*16=18720 行 解釋:1170 表示一個節點可以建立的指針數,而 16 表示,一個存放數據的頁能夠存放 16 行數據。*
3 階 B+樹能夠存放 1170117016=2 千萬(左右)
所在在 InnoDb 中 B+樹高度通常爲 1-3 層,它就能知足千萬級的數據存儲,在查找數據時一次頁的查找表明一次 IO,因此經過主鍵索引查詢一般只須要 1-3 次 IO 操做便可查找到數據。

在沒有加數據記錄大小的狀況下:
InnoDB 存儲引擎中頁的大小爲 16KB,通常表的主鍵類型爲 INT(佔用 4 個字節)或 BIGINT(佔用 8 個字節),指針類型也通常爲 4 或 8 個字節,也就是說一個頁(B+Tree 中的一個節點)中大概存儲 16KB/(8B+8B)=1K 個鍵值(由於是估值,爲方便計算,這裏的 K 取值爲〖10〗^3)。也就是說一個深度爲 3 的 B+Tree 索引能夠維護 10^3 10^3 10^3 = 10 億 條記錄。

(3) InnoDB 索引實現

InnoDB 的數據文件自己就是索引文件,InnoDB 中,表數據文件自己就是按 B+Tree 組織的一個索引結構,這棵樹的葉節點 data 域保存了完整的數據記錄,這個索引的 key 是數據表的主鍵。InnoDB 表都必須有主鍵,若是沒有定義主鍵,會默認添加一個主鍵。

一級索引(主鍵):

  • 若是一個主鍵被定義了,那麼這個主鍵就是做爲聚焦索引。
  • 若是沒有主鍵被定義,那麼該表的第一個惟一非空索引被做爲聚焦索引。
  • 若是沒有主鍵也沒有合適的惟一索引,那麼 InnoDB 內部會生成一個隱藏的主鍵做爲聚焦索引,這個隱藏的主鍵是一個 6 個字節的列,該列的值會隨着數據的插入自增。

二級索引:

  • 以所在的列創建二級索引,而後二級索引的 data 域則爲主鍵。

(4) MyISAM 索引實現

MyISAM 索引文件和數據文件是分離的,索引文件僅保存記錄所在頁的指針,經過這些地址來讀取頁,進而讀取被索引的行。

(5) InnoDB 與 MyISAM 在 B+Tree 索引的區別

第一重大的區別是 InnoDB 的數據文件自己就是索引文件,MyISAM 索引文件和數據文件是分離的,索引文件僅僅保存數據記錄的地址,而在 InnoDB 中,表數據文件自己就是按 B+Tree 組織的一個索引結構。這棵樹的葉節點 data 域保存了完整的數據記錄,這個索引的 key 是數據表的主鍵,所以 InnoDB 表數據文件自己就是主索引。由於 InnoDB 的數據文件自己要按主鍵彙集,因此 InnoDB 要求表必須有主鍵(MyISAM 能夠沒有)若是沒有顯示指定,則 MySQL 系統會自動選擇一個能夠惟一標識數據的列做爲主鍵,若是不存在這種列,則 MySQL 自動爲 InnoDB 表生成一個隱含字段做爲主鍵,這個字段長度爲 6 個字節,類型爲長整型。

第二個與 MyISAM 索引的不一樣是 InnoDB 的輔助索引 DATA 域存儲相應記錄主鍵的值而不是地址,InnoDB 的全部輔助索引都引用主鍵做爲 data 域。

彙集索引這種實現方式使得按主鍵的搜索十分高效,可是輔助索引搜索須要檢索兩遍索引:首先檢索輔助索引得到主鍵,而後用主鍵到主索引中檢索得到記錄。

(6) 有一道 MySQL 的面試題,爲何 MySQL 的索引要使用 B+樹而不是其它樹形結構?好比 B 樹?

由於 B 樹無論葉子節點仍是非葉子節點,都會保存數據,這樣致使在非葉子節點中能保存的指針數量變少(有些資料也稱爲扇出),指針少的狀況下要保存大量數據,只能增長樹的高度,致使 IO 操做變多,查詢性能變低;紅黑樹 BST 的時間複雜度是依賴於樹的高度,可是,紅黑樹的高度與 Btree 相比,高度更大。

(7) 彙集索引與非彙集索引

彙集索引:也叫聚簇索引,彙集索引表記錄的排列順序和索引的排列順序一致,因此查詢效率快,只要找到第一個索引值索引,其他就連續性記錄在物理也同樣連續存放,彙集索引對應的缺點就是修改慢,由於爲了保證表中記錄的物理和索引順序一致,在記錄插入的時候會對數據頁從新排序(InnoDB 的 B+樹)。

非彙集索引制定了表中記錄的邏輯順序,可是記錄的物理和索引不必定一致,兩種索引都採用 B+Tree 結構,非彙集索引的葉子層並不和實際數據頁相重疊,而採用葉子層包含一個指向表中的記錄在數據頁中的指針方式,非彙集索引層次多,不會形成數據重排。

Mysql 存儲引擎

在 Mysql 將每一個數據庫(Schema)保存爲數據目錄下的一個子目錄。建立表時,Mysql 會在數據庫子目錄下建立一個和表同名的.frm 文件保存表的定義。

有關:show table status like 'user'查看 user 表的相關信息:

(1) InnoDB 存儲引擎:Mysql 的默認事務型引擎

InnoDB 使用的是行級鎖,但實際是有限制的,只有在你增刪改查時匹配的條件字段帶有索引時,InnoDB 纔會使用行級鎖,在你增刪改查時匹配的條件字段不帶有索引時。InnoDB 使用的將是表級鎖。

InnoDB 是新版本 mysql 的默認引擎,支持事務處理和外鍵,可是其缺點就是慢了些,存儲方式分爲兩種

一、共享表空間存儲。這種方式建立的表的表結構保存在.frm 文件中,數據和索引保存在 innodb_data_home_dir 和 innodb_data_file_path 定義的表空間中,能夠是多個文件。

二、多表空間存儲。(.frm 表結構和 idb 數據。) 這種方式建立的表的表結構仍然保存在.frm 文件中,可是每一個表的數據和索引單獨保存在.ibd 中。若是是個分區表,則每一個分區對應單獨的.ibd 文件,文件名是「表名+分區名」,能夠在建立分區的時候指定每一個分區的數據文件的位置,以此來將表的 IO 均勻分佈在多個磁盤上。

使用 engine=innodb default charset=utf-8;

(2) MyISAM

MyISAM 存儲引擎是舊版本 mysql 的默認引擎,如今默認引擎是 InnoDB,MyISAM 引擎的主要特色就是快,沒有事務處理操做,也不支持外鍵操做。適合 insert 與 select 的操做表。MyISAM 存儲引擎的表在數據庫中,每個表都被存放爲三個以表名命名的物理文件。定義表結構.frm,存放表數據.myd 和索引數據.myi。使用方法:engine=myisam default charset=utf-8;

其中 MyISAM 支持如下三種類型的索引;

  • B-Tree 索引,就是全部的索引節點都按照 B-Tree 的數據結構來存儲,全部的索引數據節點都在葉節點。
  • R-Tree 索引
  • Full-Text 索引,全文索引,它的存儲結構也是 B-Tree。主要是爲了解決在咱們須要用 like 查詢的低效問題。

(3) MEMORY

存儲引擎使用存在內存中的內容來建立表,每一個 Memory 表只實現對應一個磁盤文件,格式是.frm(只保存表結構,不保存內容)。Memory 類型的表訪問很是快,由於它的數據是放在內存中的,而且默認使用 Hash 索引,可是一旦服務關閉,表中的數據就會丟失掉。

CREATE TABLE tab_memory ENGINE=MEMORY

給 Memory 表建立索引的時候,能夠指定使用 Hash 索引仍是 BTree 索引。

Create index mem_hash using HASH on table_memory(city_id);

(4) InnoDB 與 MyISAM 區別

InnoDB 和 MyISAM 是許多人在使用 MySQL 時最經常使用的兩個表類型,MyISAM 不支持事務處理等高級處理,而 InnoDB 類型支持。MyISAM 類型的強調的是性能。其執行速度比 InnoDB 類型更快,可是不提供事務類型支持。而 InnoDB 提供事務支持和外鍵。

聯合索引

把聯合索引單獨拿出來是由於去滴滴面試的時候被問到過。

(1) 聯合索引的規則

聯合索引能夠將多個字段組合建立一個索引,在查詢 where 條件中使用組合索引時,它符合最左匹配規則。例如爲 A,B,C 三列建立索引,則它支持 A/A,B/A,B,C 而 B,C 則沒法使用組合索引。

當一個列存在聯合索引和單列索引時,mysql 會根據查詢語句的成原本選擇走哪條索引。

(2) 查輔助索引時須要查幾回索引

須要兩次:在前面的索引一節咱們詳細的介紹過輔助索引的結構,輔助索引的 B+Tree 結點的葉子結點存放的是一級索引的值,因此查到一級索引時,它會再查詢一次一級索引。

Mysql 讀寫分離

(1)  應用場景

讀寫分離主要解決的是數據庫的寫操做是比較耗時的,而數據庫的讀取則是很是快的,因此讀寫分離來解決數據庫的寫入,影響了查詢的效率。

讀寫分離的好處

  1. 分攤服務器壓力,提升機器的系統處理效率。讀寫分離適用於讀遠比寫的場景,若是有一臺服務器,當 select 不少時,update 和 delete 會被這些 select 訪問中的數據堵塞,等待 select 結束,併發性能並不高,而主從只負責各自的寫和讀,極大程度的緩解 X 鎖和 S 鎖爭用;
  2. 增長冗餘,提升服務可用性,當一臺數據庫服務器宕機後能夠調整另一臺從庫以最快速度恢復服務

(2) 原理分析

主庫將插入、更新或刪除的記錄寫入到了 binlog 日誌,而後從庫鏈接到主庫以後,從庫有一個 IO 線程,將主庫的 binlog 日誌拷貝到本身本地,寫入一箇中繼日誌中。接着從庫中有一個 SQL 線程會從中繼日誌讀取 binlog,而後執行 binlog 日誌中的內容,也就是在本身本地再次執行一遍 SQL,這樣就能夠保證本身跟主庫的數據是同樣的。

這裏有一個很是重要的一點,就是從庫同步主庫數據的過程是串行化的,也就是說主庫上並行的操做,在從庫上會串行執行。因此這就是一個很是重要的點了,因爲從庫從主庫拷貝日誌以及串行執行 SQL 的特色,在高併發場景下,從庫的數據必定會比主庫慢一些,是有延時的。因此常常出現,剛寫入主庫的數據多是讀不到的,要過幾十毫秒,甚至幾百毫秒才能讀取到。

並且這裏還有另一個問題,就是若是主庫忽然宕機,而後剛好數據還沒同步到從庫,那麼有些數據可能在從庫上是沒有的,有些數據可能就丟失了。

mysql 半同步複製:

這個所謂半同步複製,semi-sync 複製,指的就是主庫寫入 binlog 日誌以後,就會將強制此時當即將數據同步到從庫,從庫將日誌寫入本身本地的 relay log 以後,接着會返回回一個 ack 給主庫,主庫接收到至少一個從庫的 ack 以後纔會認爲寫操做完成了。

並行複製

所謂並行複製,指的是從庫開啓多個線程,並行讀取 relay log 中不一樣庫的日誌,而後並行重放不一樣庫的日誌,這是庫級別的並行。

(3) 延遲問題解決方案

在 Master 上增長一個自增表,這個表僅含有 1 個字段,當 master 接收到任何數據更新的請求時,均會觸發這個觸發器,該觸發器更新自增表中的記錄。

MySQL_Poxy 解決方案:

寫數據時:因爲 Count_table 也參與 Mysq 的主從同步,所以在 Master 上做的 Update 更新也會同步到 Slave 上。當 Client 經過 Proxy 進行數據讀取時,Proxy 能夠先向 Master 和 Slave 的 Count_table 表發送查詢請求,當兩者的數據相同時,Proxy 能夠認定 Master 和 Slave 的數據狀態是一致的,而後把 select 請求發送到 Slave 服務器上,不然就發送到 Master 上。以下圖所示:

讀數據時:經過這種方式,就能夠比較完美的結果 MySQL 的同步延遲不可控問題。之因此所「比較完美」,是由於這種方案 double 了查詢請求,對 Master 和 Slave 構成了額外的壓力。不過因爲 Proxy 與真實的 Mysql Server 採用鏈接池的方式鏈接,所以額外的壓力仍是能夠接受的

Mysql 分庫分表

(1)  爲何分庫分表

​ 好比一個項目單表都幾千萬數據了,mysql 數據庫已經抗不住了,單表數據量太大,會極大影響你的 sql 執行的性能,會發遭受 sql 可能就跑的愈來愈慢。
​ 分表就是把一個表的數據放到多個表中,而後項目查詢數據的時候只查詢一個表,好比按照用戶 id 來分表,將一個用戶的數據就放在一個用中。這樣能夠控制每一個表的數據量在可控的範圍內,好比每一個表固定在 200 萬之內。
​ 分庫就是你一個庫最多支撐併發 2000,並且一個健康的單庫併發值你最好保持在每秒 1000 左右,不要太大,那麼你能夠將一個庫的數據拆分到多個庫中,訪問的時候訪問一個庫就行了。

(2) 分庫分表中間件

數據庫分庫/分表中間件有兩類:一類是 client 層,也就是直接在系統中使用的,另外一類是 proxy 層,須要單獨部署這種中間件。

  • sharding-jdbc 這種 client 層方案的優勢在於不用部署,運維成本低,不須要代理層的二次轉發請求,性能很高,可是若是遇到升級啥的須要各個系統都從新升級版本再發布,各個系統都須要耦合 sharding-jdbc 的依賴;
  • mycat 這種 proxy 層方案的缺點在於須要部署,本身及運維一套中間件,運維成本高,可是好處在於對於各個項目是透明的,若是遇到升級之類的都是本身中間件那裏搞就好了。

(3)  垂直拆分與水平拆分

水平拆分:就是把一個表的數據給拆分到多個庫的多個表裏面去,可是每一個庫的表結構都同樣,只不過每一個庫下表放的數據是不一樣的,全部的不一樣庫的表數據加起來就是所有數據,水平拆分的意義,就是將數據均勻放更多的庫裏,而後用多個庫來抗更高的併發,還有就是用多個庫的存儲容量來進行擴容。
垂直拆分:就是把一個有不少字段的表給拆分紅多個表,或者是多個庫上去,每一個庫表的結構都不同,每一個庫都都包含部分字段。通常來講會將較少的訪問頻率很高的字段放到一個表裏去,而後將較多的訪問頻率很低的字段放到另一個表裏去。由於數據庫是有緩存的,你訪問頻率高的行字段越少,就能夠在緩存裏緩存更多的行,性能就越好。這個通常在表層面作的較多一些。

​ 你就得考慮一下,你的項目裏該如何分庫分表?通常來講,垂直拆分,你能夠在表層面來作,對一些字段特別多的表作一下拆分;水平拆分,你能夠說是併發承載不了,或者是數據量太大,容量承載不了,你給拆了,按什麼字段來拆,你本身想好;分表,你考慮一下,你若是哪怕是拆到每一個庫裏去,併發和容量都 ok 了,可是每一個庫的表仍是太大了,那麼你就分表,將這個表分開,保證每一個表的數據量並非很大。
​ 並且這兒還有兩種分庫分表的方式,一種是按照 range 來分,就是每一個庫一段連續的數據,這個通常是按好比時間範圍來的,可是這種通常較少用,由於很容易產生熱點問題,大量的流量都打在最新的數據上了;或者是按照某個字段 hash 一下均勻分散,這個較爲經常使用。
​ range 來分,好處在於說,後面擴容的時候,就很容易,由於你只要預備好,給每月都準備一個庫就能夠了,到了一個新的月份的時候,天然而然,就會寫新的庫了;缺點,可是大部分的請求,都是訪問最新的數據。實際生產用 range,要看場景,你的用戶不是僅僅訪問最新的數據,而是均勻的訪問如今的數據以及歷史的數據
​ hash 分法,好處在於說,能夠平均分配沒給庫的數據量和請求壓力;壞處在於說擴容起來比較麻煩,會有一個數據遷移的這麼一個過程。

把系統從未分庫分表動態切換到分庫分表上

(1)  停機遷移方案

停機遷移方案中,就是把系統在凌晨 12 點開始運維,系統停掉,而後提早寫好一個導數據的一次性工具,此時直接跑起來,而後將單庫單表的數據寫到分庫分表裏面去。 導入數據完成了以後,修改系統的數據庫鏈接配置,包括可能代碼和 SQL 也許有修改,那你就用最新的代碼,而後直接啓動連到新的分庫分表上去。

(2)  雙寫遷移方案

此方案不用停機,比較經常使用。 簡單來講,就是在線上系統裏面,以前全部寫庫的地方,增刪改操做,都除了對老庫增刪改,都加上對新庫的增刪改,這就是所謂的雙寫。同時寫兩個庫,老庫和新庫。而後系統部署以後,新庫數據差太遠,用以前說的導數工具,路起來讀老庫數據寫新庫,寫的時候要根據 gmt_modified 這類判斷這條數據最後修改時間,除非是讀出來的數據在新庫裏沒有,或者是比新庫的數據新纔會寫。

8.分庫分表以後生成全局惟一 ID

數據庫通過分庫分表以後,系統在向數據庫中插入數據的時候,須要生成一個惟一的數據庫 id,它在分庫分表中必須是全局惟一的。經常使用的生成方法以下:

(1) 數據庫自增主鍵

在數據庫中單首創建一張表,這張表只包含一個字段 id,每次要向分庫分表中插入數據時,先從這張表中獲取一個 id。
缺點:這張生成 id 的表是單庫單表的,要是高併發的話,就會產生瓶頸。
適合的場景:你分庫分表就倆緣由,要不就是單庫併發過高,要不就是單庫數據量太大;除非是你併發不高,可是數據量太大致使的分庫分表擴容,你能夠用這個方案,由於可能每秒最高併發最多就幾百,那麼就走單獨的一個庫和表生成自增主鍵便可。

(2) UUID

好處就是本地生成,不須要基於數據庫,很差之處就是,UUID 太長了,而且是字符串,做爲主鍵性能太差了,不適合用於主鍵。
適合的場景:若是你是要隨機生成個什麼文件名了,編號之類的,你能夠用 uuid,可是做爲主鍵是不能用 uuid 的。

(3)  獲取系統當前時間

這個就是獲取當前時間便可,可是問題是,併發很高的時候,好比一秒併發幾千,會有重複的狀況,這個是確定不合適的,基本就不用考慮了。
適合的場景:通常若是用這個方案,是將當前時間跟不少其餘的業務字段拼接起來,做爲一 id,若是業務上你以爲能夠接受,那麼是能夠的,你能夠將別的業務字段值跟當前時間拼接起來,組成一個全局惟一的編號,訂單編號啊:時間戳 + 用戶 id + 業務含義編碼。

(4) redis 生成惟一 ID

redis 是部署在數據庫集羣以外,它不依賴於數據庫,使用 redis 可以生成惟一的 id 這主要依賴於 Redis 是單線程的,因此也能夠用生成全局惟一的 ID,能夠用 redis 的原子操做 incr 和 incrby 來實現。
也可使用 redis 集羣來獲取更高的吞吐量,假如一個集羣中有 5 臺 Redis,能夠初始化每臺 Redis 的值分別是 1,2,3,4,5。而後步長都是 5,各個 redis 生成的 ID 爲:
A:1,6,11,16,21
B:2,7,12,17,22
C:3,8,13,18,23
D:4,9,14,19,24
E:5,10,15,20,25

(5) snowflake 算法

twitter 開源的分佈式 id 生成算法,就是把一個 64 位的 long 型的 id,1 個 bit 是不用的,用其中的 41 bit 做爲毫秒數,用 10 bit 做爲工做機器 id,12 bit 做爲序列號

  • 1 bit:不用,爲啥呢?由於二進制裏第一個 bit 爲若是是 1,那麼都是負數,可是咱們生成的 id 都是正數,因此第一個 bit 統一都是 0
  • 41 bit:表示的是時間戳,單位是毫秒。41 bit 能夠表示的數字多達 2^41 - 1,也就是能夠標識 2 ^ 41 - 1 個毫秒值,換算成年就是表示 69 年的時間。
  • 10 bit:記錄工做機器 id,表明的是這個服務最多能夠部署在 2^10 臺機器上哪,也就是 1024 臺機器。可是 10 bit 裏 5 個 bit 表明機房 id,5 個 bit 表明機器 id。意思就是最多表明 2 ^ 5 個機房(32 個機房),每一個機房裏能夠表明 2 ^ 5 個機器(32 臺機器)。
  • 12 bit:這個是用來記錄同一個毫秒內產生的不一樣 id,12 bit 能夠表明的最大正整數是 2 ^ 12 - 1 = 4096,也就是說能夠用這個 12bit 表明的數字來區分同一個毫秒內的 4096 個不一樣的 id

例如:0 1011100111101101000110100010111 10001 00001 000000000001
第 1 位 0:表示正數
接着 41 位表示:1559661847--> 2019-6-4 23:24:7
接着 5 位表示:17 機房
接着 5 位表示:17 機房下的第 1 臺機器
最後 12 位表示:當前時間戳下併發的自增編號。

參考代碼:

public class IdWorker{
    private long workerId;
    private long datacenterId;
    private long sequence;

    private long twepoch = 1288834974657L;

    private long workerIdBits = 5L;
    private long datacenterIdBits = 5L;
    private long maxWorkerId = -1L ^ (-1L << workerIdBits); // 這個是二進制運算,就是5 bit最多隻能有31個數字,也就是說機器id最多隻能是32之內
    private long maxDatacenterId = -1L ^ (-1L << datacenterIdBits); // 這個是一個意思,就是5 bit最多隻能有31個數字,機房id最多隻能是32之內
    private long sequenceBits = 12L;

    private long workerIdShift = sequenceBits;
    private long datacenterIdShift = sequenceBits + workerIdBits;
    private long timestampLeftShift = sequenceBits + workerIdBits + datacenterIdBits;
    private long sequenceMask = -1L ^ (-1L << sequenceBits);

    private long lastTimestamp = -1L;

    public IdWorker(long workerId, long datacenterId, long sequence){
        // 這兒不就檢查了一下,要求就是你傳遞進來的機房id和機器id不能超過32,不能小於0
        if (workerId > maxWorkerId || workerId < 0) {
            throw new IllegalArgumentException(String.format("worker Id can't be greater than %d or less than 0",maxWorkerId));
        }
        if (datacenterId > maxDatacenterId || datacenterId < 0) {
            throw new IllegalArgumentException(String.format("datacenter Id can't be greater than %d or less than 0",maxDatacenterId));
        }
        System.out.printf("worker starting. timestamp left shift %d, datacenter id bits %d, worker id bits %d, sequence bits %d, workerid %d",
                timestampLeftShift, datacenterIdBits, workerIdBits, sequenceBits, workerId);

        this.workerId = workerId;
        this.datacenterId = datacenterId;
        this.sequence = sequence;
    }

    public synchronized long nextId() {
    // 這兒就是獲取當前時間戳,單位是毫秒
        long timestamp = timeGen();

        if (timestamp < lastTimestamp) {
            System.err.printf("clock is moving backwards.  Rejecting requests until %d.", lastTimestamp);
            throw new RuntimeException(String.format("Clock moved backwards.  Refusing to generate id for %d milliseconds",
                    lastTimestamp - timestamp));
        }

        // 在同一個毫秒內,又發送了一個請求生成一個id,0 -> 1
        if (lastTimestamp == timestamp) {
            sequence = (sequence + 1) & sequenceMask; // 這個意思是說一個毫秒內最多隻能有4096個數字,不管你傳遞多少進來,這個位運算保證始終就是在4096這個範圍內,避免你本身傳遞個sequence超過了4096這個範圍
            if (sequence == 0) {
                timestamp = tilNextMillis(lastTimestamp);
            }
        } else {
            sequence = 0;
        }
        // 這兒記錄一下最近一次生成id的時間戳,單位是毫秒
        lastTimestamp = timestamp;
         // 這兒就是將時間戳左移,放到41 bit那兒;將機房id左移放到5 bit那兒;將機器id左移放到5 bit那兒;將序號放最後10 bit;最後拼接起來成一個64 bit的二進制數字,轉換成10進制就是個long型
        return ((timestamp - twepoch) << timestampLeftShift) |
                (datacenterId << datacenterIdShift) |
                (workerId << workerIdShift) |
                sequence;
    }
    private long tilNextMillis(long lastTimestamp) {
        long timestamp = timeGen();
        while (timestamp <= lastTimestamp) {
            timestamp = timeGen();
        }
        return timestamp;
    }
    private long timeGen(){
        return System.currentTimeMillis();
    }
    public long getWorkerId(){
        return workerId;
    }
    public long getDatacenterId(){
        return datacenterId;
    }
    public long getTimestamp(){
        return System.currentTimeMillis();
    }
    //---------------測試---------------
    public static void main(String[] args) {
        IdWorker worker = new IdWorker(1,1,1);
        for (int i = 0; i < 30; i++) {
            System.out.println(worker.nextId());
        }
    }
}
相關文章
相關標籤/搜索