MySQL實戰45講學習筆記:第三十八講

1、本節內容

我在上一篇文章末尾留給你的問題是:兩個 group by 語句都用了 order by null,爲何使用內存臨時表獲得的語句結果裏,0 這個值在最後一行;而使用磁盤臨時表獲得的結果
裏,0 這個值在第一行?數據庫

今天咱們就來看看,出現這個問題的緣由吧。數組

2、內存表的數據組織結構

一、兩個查詢結果 -0 的位置

爲了便於分析,我來把這個問題簡化一下,假設有如下的兩張表 t1 和 t2,其中表 t1 使用Memory 引擎, 表 t2 使用 InnoDB 引擎。緩存

create table t1(id int primary key, c int) engine=Memory;
create table t2(id int primary key, c int) engine=innodb;
insert into t1 values(1,1),(2,2),(3,3),(4,4),(5,5),(6,6),(7,7),(8,8),(9,9),(0,0);
insert into t2 values(1,1),(2,2),(3,3),(4,4),(5,5),(6,6),(7,7),(8,8),(9,9),(0,0);

而後,我分別執行 select * from t1 和 select * from t2。安全

圖 1 兩個查詢結果 -0 的位置bash

能夠看到,內存表 t1 的返回結果裏面 0 在最後一行,而 InnoDB 表 t2 的返回結果裏 0 在第一行。網絡

出現這個區別的緣由,要從這兩個引擎的主鍵索引的組織方式提及。session

二、表 t2 的數據組織

表 t2 用的是 InnoDB 引擎,它的主鍵索引 id 的組織方式,你已經很熟悉了:InnoDB 表的數據就放在主鍵索引樹上,主鍵索引是 B+ 樹。因此表 t2 的數據組織方式以下架構

圖 2 表 t2 的數據組織併發


主鍵索引上的值是有序存儲的。在執行 select * 的時候,就會按照葉子節點從左到右掃描,因此獲得的結果裏,0 就出如今第一行。性能

三、表 t1 的數據組織

與 InnoDB 引擎不一樣,Memory 引擎的數據和索引是分開的。咱們來看一下表 t1 中的數據內容。

圖 3 表 t1 的數據組織

能夠看到,內存表的數據部分以數組的方式單獨存放,而主鍵 id 索引裏,存的是每一個數據的位置。主鍵 id 是 hash 索引,能夠看到索引上的 key 並非有序的。

在內存表 t1 中,當我執行 select * 的時候,走的是全表掃描,也就是順序掃描這個數組。所以,0 就是最後一個被讀到,並放入結果集的數據。

四、InnoDB 和 Memory 引擎的數據組織方式是不一樣的

可見,InnoDB 和 Memory 引擎的數據組織方式是不一樣的:

InnoDB 引擎把數據放在主鍵索引上,其餘索引上保存的是主鍵 id。這種方式,咱們稱之爲索引組織表(Index Organizied Table)。

而 Memory 引擎採用的是把數據單獨存放,索引上保存數據位置的數據組織形式,咱們稱之爲堆組織表(Heap Organizied Table)

從中咱們能夠看出,這兩個引擎的一些典型不一樣:

1. InnoDB 表的數據老是有序存放的,而內存表的數據就是按照寫入順序存放的;
2. 當數據文件有空洞的時候,InnoDB 表在插入新數據的時候,爲了保證數據有序性,只能在固定的位置寫入新值,而內存表找到空位就能夠插入新值;
3. 數據位置發生變化的時候,InnoDB 表只須要修改主鍵索引,而內存表須要修改全部索引;
4. InnoDB 表用主鍵索引查詢時須要走一次索引查找,用普通索引查詢的時候,須要走兩
5. InnoDB 支持變長數據類型,不一樣記錄的長度可能不一樣;內存表不支持 Blob 和 Text 字段,而且即便定義了 varchar(N),實際也看成 char(N),也就是固定長度字符串來存儲,所以內存表的每行數據長度相同。

因爲內存表的這些特性,每一個數據行被刪除之後,空出的這個位置均可以被接下來要插入的數據複用。好比,若是要在表 t1 中執行:

delete from t1 where id=5;
insert into t1 values(10,10);
select * from t1;

就會看到返回結果裏,id=10 這一行出如今 id=4 以後,也就是原來 id=5 這行數據的位置。

須要指出的是,表 t1 的這個主鍵索引是哈希索引,所以若是執行範圍查詢,好比

select * from t1 where id<5;

是用不上主鍵索引的,須要走全表掃描。你能夠藉此再回顧下第 4 篇文章的內容。那若是要讓內存表支持範圍掃描,應該怎麼辦呢 ?

3、hash 索引和 B-Tree 索引

一、t1 的數據組織 -- 增長 B-Tree 索引

實際上,內存表也是支 B-Tree 索引的。在 id 列上建立一個 B-Tree 索引,SQL 語句能夠這麼寫:

alter table t1 add index a_btree_index using btree (id);

這時,表 t1 的數據組織形式就變成了這樣:

圖 4 表 t1 的數據組織 -- 增長 B-Tree 索引

 

新增的這個 B-Tree 索引你看着就眼熟了,這跟 InnoDB 的 b+ 樹索引組織形式相似。

二、B-Tree 和 hash 索引查詢返回結果對比

做爲對比,你能夠看一下這下面這兩個語句的輸出:

圖 5 使用 B-Tree 和 hash 索引查詢返回結果對比

能夠看到,執行 select * from t1 where id<5 的時候,優化器會選擇 B-Tree 索引,因此返回結果是 0 到 4。 使用 force index 強行使用主鍵 id 這個索引,id=0 這一行就在結果集的最末尾了。

其實,通常在咱們的印象中,內存表的優點是速度快,其中的一個緣由就是 Memory 引擎支持 hash 索引。固然,更重要的緣由是,內存表的全部數據都保存在內存,而內存的
讀寫速度老是比磁盤快。

可是,接下來我要跟你說明,爲何我不建議你在生產環境上使用內存表。這裏的緣由主要包括兩個方面:

  • 1. 鎖粒度問題;
  • 2. 數據持久化問題。

4、內存表的鎖

咱們先來講說內存表的鎖粒度問題。

內存表不支持行鎖,只支持表鎖。所以,一張表只要有更新,就會堵住其餘全部在這個表上的讀寫操做。

須要注意的是,這裏的表鎖跟以前咱們介紹過的 MDL 鎖不一樣,但都是表級的鎖。接下來,我經過下面這個場景,跟你模擬一下內存表的表級鎖。

圖 6 內存表的表鎖 -- 復現步驟

在這個執行序列裏,session A 的 update 語句要執行 50 秒,在這個語句執行期間session B 的查詢會進入鎖等待狀態。session C 的 show processlist 結果輸出以下:

圖 7 內存表的表鎖 -- 結果

跟行鎖比起來,表鎖對併發訪問的支持不夠好。因此,內存表的鎖粒度問題,決定了它在處理併發事務的時候,性能也不會太好。

5、數據持久性問題

接下來,咱們再看看數據持久性的問題。

數據放在內存中,是內存表的優點,但也是一個劣勢。由於,數據庫重啓的時候,全部的內存表都會被清空。

你可能會說,若是數據庫異常重啓,內存表被清空也就清空了,不會有什麼問題啊。可是,在高可用架構下,內存表的這個特色簡直能夠當作 bug 來看待了。爲何這麼說呢?

一、 M-S 基本架構

咱們先看看 M-S 架構下,使用內存表存在的問題。

圖 8 M-S 基本架構

咱們來看一下下面這個時序:

1. 業務正常訪問主庫;
2. 備庫硬件升級,備庫重啓,內存表 t1 內容被清空;
3. 備庫重啓後,客戶端發送一條 update 語句,修改表 t1 的數據行,這時備庫應用線程就會報錯「找不到要更新的行」。

這樣就會致使主備同步中止。固然,若是這時候發生主備切換的話,客戶端會看到,表 t1的數據「丟失」了。

在圖 8 中這種有 proxy 的架構裏,你們默認主備切換的邏輯是由數據庫系統本身維護的。這樣對客戶端來講,就是「網絡斷開,重連以後,發現內存表數據丟失了」。

你可能說這還好啊,畢竟主備發生切換,鏈接會斷開,業務端可以感知到異常。

二、雙 M 結構

可是,接下來內存表的這個特性就會讓使用現象顯得更「詭異」了。因爲 MySQL 知道重啓以後,內存表的數據會丟失。因此,擔憂主庫重啓以後,出現主備不一致,MySQL 在
實現上作了這樣一件事兒:在數據庫重啓以後,往 binlog 裏面寫入一行 DELETE FROMt1。

若是你使用是如圖 9 所示的雙 M 結構的話:

圖 9 雙 M 結構

在備庫重啓的時候,備庫 binlog 裏的 delete 語句就會傳到主庫,而後把主庫內存表的內容刪除。這樣你在使用的時候就會發現,主庫的內存表數據忽然被清空了。

三、建議你把普通內存表都用 InnoDB 表來代替

基於上面的分析,你能夠看到,內存表並不適合在生產環境上做爲普通數據表使用。

有同窗會說,可是內存表執行速度快呀。這個問題,其實你能夠這麼分析:

1. 若是你的表更新量大,那麼併發度是一個很重要的參考指標,InnoDB 支持行鎖,併發度比內存表好;

2. 能放到內存表的數據量都不大。若是你考慮的是讀的性能,一個讀 QPS 很高而且數據量不大的表,即便是使用 InnoDB,數據也是都會緩存在 InnoDB Buffer Pool 裏的

所以,使用 InnoDB 表的讀性能也不會差。

因此,我建議你把普通內存表都用 InnoDB 表來代替。可是,有一個場景倒是例外的。

四、內存表的應用場景

這個場景就是,咱們在第 35 和 36 篇說到的用戶臨時表。在數據量可控,不會耗費過多內存的狀況下,你能夠考慮使用內存表。

內存臨時表恰好能夠無視內存表的兩個不足,主要是下面的三個緣由:

1. 臨時表不會被其餘線程訪問,沒有併發性的問題;
2. 臨時表重啓後也是須要刪除的,清空數據這個問題不存在;
3. 備庫的臨時表也不會影響主庫的用戶線程。

如今,咱們回過頭再看一下第 35 篇 join 語句優化的例子,當時我建議的是建立一個InnoDB 臨時表,使用的語句序列是:

create temporary table temp_t(id int primary key, a int, b int, index(b))engine=innodb;
insert into temp_t select * from t2 where b>=1 and b<=2000;
select * from t1 join temp_t on (t1.b=temp_t.b);

四、使用內存臨時表的執行效果

瞭解了內存表的特性,你就知道了, 其實這裏使用內存臨時表的效果更好,緣由有三個:

1. 相比於 InnoDB 表,使用內存表不須要寫磁盤,往表 temp_t 的寫數據的速度更快;
2. 索引 b 使用 hash 索引,查找的速度比 B-Tree 索引快;
3. 臨時表數據只有 2000 行,佔用的內存有限。

所以,你能夠對第 35 篇文章的語句序列作一個改寫,將臨時表 t1 改爲內存臨時表,而且在字段 b 上建立一個 hash 索引。

create temporary table temp_t(id int primary key, a int, b int, index (b))engine=memory;
insert into temp_t select * from t2 where b>=1 and b<=2000;
select * from t1 join temp_t on (t1.b=temp_t.b);

圖 10 使用內存臨時表的執行效果

能夠看到,不管是導入數據的時間,仍是執行 join 的時間,使用內存臨時表的速度都比使用 InnoDB 臨時表要更快一些。

6、小結

今天這篇文章,我從「要不要使用內存表」這個問題展開,和你介紹了 Memory 引擎的幾個特性。

能夠看到,因爲重啓會丟數據,若是一個備庫重啓,會致使主備同步線程中止;若是主庫跟這個備庫是雙 M 架構,還可能致使主庫的內存表數據被刪掉。

所以,在生產上,我不建議你使用普通內存表。

若是你是 DBA,能夠在建表的審覈系統中增長這類規則,要求業務改用 InnoDB 表。咱們在文中也分析了,其實 InnoDB 表性能還不錯,並且數據安全也有保障。而內存表因爲不
支持行鎖,更新語句會阻塞查詢,性能也未必就如想象中那麼好。

基於內存表的特性,咱們還分析了它的一個適用場景,就是內存臨時表。內存表支持 hash索引,這個特性利用起來,對複雜查詢的加速效果仍是很不錯的。

最後,我給你留一個問題吧。

假設你剛剛接手的一個數據庫上,真的發現了一個內存表。備庫重啓以後確定是會致使備庫的內存表數據被清空,進而致使主備同步中止。這時,最好的作法是將它修改爲
InnoDB 引擎表

假設當時的業務場景暫時不容許你修改引擎,你能夠加上什麼自動化邏輯,來避免主備同步中止呢?

你能夠把你的思考和分析寫在評論區,我會在下一篇文章的末尾跟你討論這個問題。感謝你的收聽,也歡迎你把這篇文章分享給更多的朋友一塊兒閱讀。

7、上期問題時間

今天文章的正文內容,已經回答了咱們上期的問題,這裏就再也不贅述了。

評論區留言點贊板

@老楊同志、@poppy、@長傑 這三位同窗給出了正確答案,春節期間還持續保持跟進學習,給大家點贊。

相關文章
相關標籤/搜索