38 - MySQL之Memory引擎

上文中,兩個 group by 語句都用了 order by null,為什么使用內存臨時表得到的語句結果里,0 這個值在最后一行;而使用磁盤臨時表得到的結果里,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);
  • 然后,分別執(zhí)行 select * from t1 和 select * from t2。
兩個查詢結果 0的位置
  • 可以看到,內存表 t1 的返回結果里面 0 在最后一行,而 InnoDB 表 t2 的返回結果里 0 在第一行。
  • 出現這個區(qū)別的原因,要從這兩個引擎的主鍵索引的組織方式說起。表 t2 用的是 InnoDB 引擎,它的主鍵索引 id 的組織方式,你已經很熟悉了:InnoDB 表的數據就放在主鍵索引樹上,主鍵索引是 B+ 樹。所以表 t2 的數據組織方式如下圖所示:
表t2組織形式
  • 主鍵索引上的值是有序存儲的。在執(zhí)行 select * 的時候,就會按照葉子節(jié)點從左到右掃描,所以得到的結果里,0 就出現在第一行。與 InnoDB 引擎不同,Memory 引擎的數據和索引是分開的。我們來看一下表 t1 中的數據內容。
表t1的組織形式
  • 可以看到,內存表的數據部分以數組的方式單獨存放,而主鍵 id 索引里,存的是每個數據的位置。主鍵 id 是 hash 索引,可以看到索引上的 key 并不是有序的。
  • 在內存表 t1 中,當我執(zhí)行 select * 的時候,走的是全表掃描,也就是順序掃描這個數組。因此,0 就是最后一個被讀到,并放入結果集的數據。
  • 可見,InnoDB 和 Memory 引擎的數據組織方式是不同的:
    • InnoDB 引擎把數據放在主鍵索引上,其他索引上保存的是主鍵 id。這種方式,我們稱之為索引組織表(Index Organizied Table)。
    • 而 Memory 引擎采用的是把數據單獨存放,索引上保存數據位置的數據組織形式,我們稱之為堆組織表(Heap Organizied Table)。
  • 從中我們可以看出,這兩個引擎的一些典型不同:
    1. InnoDB 表的數據總是有序存放的,而內存表的數據就是按照寫入順序存放的;
    2. 當數據文件有空洞的時候,InnoDB 表在插入新數據的時候,為了保證數據有序性,只能在固定的位置寫入新值,而內存表找到空位就可以插入新值;
    3. 數據位置發(fā)生變化的時候,InnoDB 表只需要修改主鍵索引,而內存表需要修改所有索引;
    4. InnoDB 表用主鍵索引查詢時需要走一次索引查找,用普通索引查詢的時候,需要走兩次索引查找。而內存表沒有這個區(qū)別,所有索引的“地位”都是相同的。
    5. InnoDB 支持變長數據類型,不同記錄的長度可能不同;內存表不支持 Blob 和 Text 字段,并且即使定義了 varchar(N),實際也當作 char(N),也就是固定長度字符串來存儲,因此內存表的每行數據長度相同。
  • 由于內存表的這些特性,每個數據行被刪除以后,空出的這個位置都可以被接下來要插入的數據復用。比如,如果要在表 t1 中執(zhí)行:
delete from t1 where id=5;
insert into t1 values(10,10);
select * from t1;
  • 就會看到返回結果里,id=10 這一行出現在 id=4 之后,也就是原來 id=5 這行數據的位置。需要指出的是,表 t1 的這個主鍵索引是哈希索引,因此如果執(zhí)行范圍查詢,比如:
select * from t1 where id<5;
  • 是用不上主鍵索引的,需要走全表掃描。那如果要讓內存表支持范圍掃描,應該怎么辦呢 ?

hash 索引和 B-Tree 索引

  • 實際上,內存表也是支 B-Tree 索引的。在 id 列上創(chuàng)建一個 B-Tree 索引,SQL 語句可以這么寫:
alter table t1 add index a_btree_index using btree (id);
  • 這時,表 t1 的數據組織形式就變成了這樣:
表t1的數據組織 -- 增加Btree索引
  • 這跟 InnoDB 的 b+ 樹索引組織形式類似。作為對比,你可以看一下這下面這兩個語句的輸出:
使用 B-Tree 和 hash 索引查詢返回結果對比
  • 可以看到,執(zhí)行 select * from t1 where id<5 的時候,優(yōu)化器會選擇 B-Tree 索引,所以返回結果是 0 到 4。 使用 force index 強行使用主鍵 id 這個索引,id=0 這一行就在結果集的最末尾了。
  • 其實,一般在我們的印象中,內存表的優(yōu)勢是速度快,其中的一個原因就是 Memory 引擎支持 hash 索引。當然,更重要的原因是,內存表的所有數據都保存在內存,而內存的讀寫速度總是比磁盤快。
  • 接下來我要跟你說明,為什么我不建議你在生產環(huán)境上使用內存表。這里的原因主要包括兩個方面:
    1. 鎖粒度問題;
    2. 數據持久化問題。

內存表的鎖

  • 內存表不支持行鎖,只支持表鎖。因此,一張表只要有更新,就會堵住其他所有在這個表上的讀寫操作。
  • 需要注意的是,這里的表鎖跟之前我們介紹過的 MDL 鎖不同,但都是表級的鎖。接下來,通過下面這個場景,模擬一下內存表的表級鎖。
內存表鎖
  • 在這個執(zhí)行序列里,session A 的 update 語句要執(zhí)行 50 秒,在這個語句執(zhí)行期間 session B 的查詢會進入鎖等待狀態(tài)。session C 的 show processlist 結果輸出如下:
內存表鎖結果
  • 跟行鎖比起來,表鎖對并發(fā)訪問的支持不夠好。所以,內存表的鎖粒度問題,決定了它在處理并發(fā)事務的時候,性能也不會太好。

數據持久性問題

  • 數據放在內存中,是內存表的優(yōu)勢,但也是一個劣勢。因為,數據庫重啟的時候,所有的內存表都會被清空。
  • 你可能會說,如果數據庫異常重啟,內存表被清空也就清空了,不會有什么問題啊。但是,在高可用架構下,內存表的這個特點簡直可以當做 bug 來看待了。為什么這么說呢?
  • 我們先看看 M-S 架構下,使用內存表存在的問題。
M-S 基本架構
  • 我們來看一下下面這個時序:
    1. 業(yè)務正常訪問主庫;
    2. 備庫硬件升級,備庫重啟,內存表 t1 內容被清空;
    3. 備庫重啟后,客戶端發(fā)送一條 update 語句,修改表 t1 的數據行,這時備庫應用線程就會報錯“找不到要更新的行”。
  • 這樣就會導致主備同步停止。當然,如果這時候發(fā)生主備切換的話,客戶端會看到,表 t1 的數據“丟失”了。
  • 在上圖中這種有 proxy 的架構里,大家默認主備切換的邏輯是由數據庫系統(tǒng)自己維護的。這樣對客戶端來說,就是“網絡斷開,重連之后,發(fā)現內存表數據丟失了”。
  • 這可能還好點,畢竟主備發(fā)生切換,連接會斷開,業(yè)務端能夠感知到異常。
    但是,接下來內存表的這個特性就會讓使用現象顯得更“詭異”了。由于 MySQL 知道重啟之后,內存表的數據會丟失。所以,擔心主庫重啟之后,出現主備不一致,MySQL 在實現上做了這樣一件事兒:在數據庫重啟之后,往 binlog 里面寫入一行 DELETE FROM t1。
  • 如果你使用是如圖所示的雙 M 結構的話:
雙 M 結構
  • 在備庫重啟的時候,備庫 binlog 里的 delete 語句就會傳到主庫,然后把主庫內存表的內容刪除。這樣你在使用的時候就會發(fā)現,主庫的內存表數據突然被清空了。
  • 基于上面的分析,你可以看到,內存表并不適合在生產環(huán)境上作為普通數據表使用。
  • 你可能會認為內存表執(zhí)行速度快。這個問題,其實你可以這么分析:
    1. 如果你的表更新量大,那么并發(fā)度是一個很重要的參考指標,InnoDB 支持行鎖,并發(fā)度比內存表好;
    2. 能放到內存表的數據量都不大。如果你考慮的是讀的性能,一個讀 QPS 很高并且數據量不大的表,即使是使用 InnoDB,數據也是都會緩存在 InnoDB Buffer Pool 里的。因此,使用 InnoDB 表的讀性能也不會差。
  • 所以,建議你把普通內存表都用 InnoDB 表來代替。但是,有一個場景卻是例外的。
  • 這個場景就是,在數據量可控,不會耗費過多內存的情況下,你可以考慮使用內存表。
    1. 內存臨時表剛好可以無視內存表的兩個不足,主要是下面的三個原因:
    2. 臨時表不會被其他線程訪問,沒有并發(fā)性的問題;
    3. 臨時表重啟后也是需要刪除的,清空數據這個問題不存在;
    4. 備庫的臨時表也不會影響主庫的用戶線程。
  • 現在,我們回過頭再看一下 join 語句優(yōu)化的例子,當時建議的是創(chuàng)建一個 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 行,占用的內存有限。
  • 因此可以對語句序列做一個改寫,將臨時表 temp_t 改成內存臨時表,并且在字段 b 上創(chuàng)建一個 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);
內存表執(zhí)行效果
  • 可以看到,不論是導入數據的時間,還是執(zhí)行 join 的時間,使用內存臨時表的速度都比使用 InnoDB 臨時表要更快一些。

小結

  • 本文和你介紹了 Memory 引擎的幾個特性。
  • 可以看到,由于重啟會丟數據,如果一個備庫重啟,會導致主備同步線程停止;如果主庫跟這個備庫是雙 M 架構,還可能導致主庫的內存表數據被刪掉。
    因此,在生產上,我不建議你使用普通內存表。
  • 如果你是 DBA,可以在建表的審核系統(tǒng)中增加這類規(guī)則,要求業(yè)務改用 InnoDB 表。我們在文中也分析了,其實 InnoDB 表性能還不錯,而且數據安全也有保障。而內存表由于不支持行鎖,更新語句會阻塞查詢,性能也未必就如想象中那么好。
  • 基于內存表的特性,我們還分析了它的一個適用場景,就是內存臨時表。內存表支持 hash 索引,這個特性利用起來,對復雜查詢的加速效果還是很不錯的。
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發(fā)布,文章內容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,443評論 6 532
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發(fā)現死者居然都...
    沈念sama閱讀 98,530評論 3 416
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,407評論 0 375
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,981評論 1 312
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,759評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 55,204評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,263評論 3 441
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,415評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發(fā)現了一具尸體,經...
    沈念sama閱讀 48,955評論 1 336
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,782評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,983評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,528評論 5 359
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 44,222評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,650評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,892評論 1 286
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,675評論 3 392
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,967評論 2 374

推薦閱讀更多精彩內容