天天看點

MySQL 的 Memory存儲引擎還有意義嗎?

兩個group by 語句都用了order by null,為什麼使用記憶體臨時表得到的語句結果裡,0這個值在最後一行;而使用磁盤臨時表得到的結果裡,0這個值在第一行?

記憶體表的資料組織結構

  • 示例表 插入測試資料。分别查詢
mysql> select *
    -> from t1;
+----+------+
| id | c    |
+----+------+
|  1 |    1 |
|  2 |    2 |
|  3 |    3 |
|  4 |    4 |
|  5 |    5 |
|  6 |    6 |
|  7 |    7 |
|  8 |    8 |
|  9 |    9 |
|  0 |    0 |
+----+------+
10 rows in set (0.00 sec)

mysql> select *
    -> from t2;
+----+------+
| id | c    |
+----+------+
|  0 |    0 |
|  1 |    1 |
|  2 |    2 |
|  3 |    3 |
|  4 |    4 |
|  5 |    5 |
|  6 |    6 |
|  7 |    7 |
|  8 |    8 |
|  9 |    9 |
+----+------+
10 rows in set (0.00 sec)           

可見:

  • 記憶體表的傳回結果裡面0在最後一行
  • InnoDB表的傳回結果裡0在第一行

主鍵索引的組織方式

  • InnoDB表的資料就放在主鍵索引樹:t2的資料組織 主鍵索引上的值有序存儲。執行select *時,就會按照葉子節點從左到右掃描,是以0在第一行。
  • Memory引擎的資料和索引是分開的:t1中的資料組織 記憶體表的資料以數組存放,而主鍵id索引裡,存的是每個資料的位置。主鍵id是hash索引,是以索引的key并非有序。

在記憶體表中,執行select *時,是全表掃描:順序掃描該數組。是以,0就是最後一個被讀到,并放入結果集的資料。

是以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中執行:

MySQL 的 Memory存儲引擎還有意義嗎?

可見,id=10這行出現在id=4之後,即原來id=5這行資料位置。 t1的這個主鍵索引是哈希索引,是以若執行範圍查詢:

select * from t1 where id<5;           

用不上主鍵索引的,而走全表掃描。

若要讓記憶體表支援範圍掃描,應該怎麼辦呢 ?

hash索引和B-Tree索引

記憶體表其實也支援B-Tree索引。在id列上建立一個B-Tree索引,SQL語句可以這麼寫:

MySQL 的 Memory存儲引擎還有意義嗎?

此時t1的資料組織形式:

MySQL 的 Memory存儲引擎還有意義嗎?

這就類似InnoDB的b+樹索引了。

查詢對比

  • 優化器選擇B-Tree索引,傳回結果:0~4
  • force index 主鍵id索引,id=0這行在結果集末尾 我們都覺得記憶體表優勢是速度快,因為Memory引擎支援hash索引。更重要的原因是,記憶體表的所有資料都儲存在記憶體,記憶體讀寫速度肯定比磁盤快。

但仍然不推薦在生産環境上使用記憶體表,因為有如下嚴重問題:

記憶體表的鎖

記憶體表不支援行鎖,隻支援表鎖。是以,一張表隻要有更新,就會堵住其他所有在這個表上的讀寫。

這裡的表鎖和MDL鎖不同,但都是表級鎖。

模拟記憶體表的表級鎖

MySQL 的 Memory存儲引擎還有意義嗎?
  • sessionA的update語句要執行50s
  • 該語句執行期間sessionB的查詢會進入鎖等待狀态
  • session C的show processlist:
mysql> show processlist;
+----+-----------------+-----------------+-----------------+---------+--------+------------------------------+---------------------------------------+
| Id | User            | Host            | db              | Command | Time   | State                        | Info                                  |
+----+-----------------+-----------------+-----------------+---------+--------+------------------------------+---------------------------------------+
|  5 | event_scheduler | localhost       | NULL            | Daemon  | 390719 | Waiting on empty queue       | NULL                                  |
| 41 | root            | localhost       | common_mistakes | Query   |      8 | User sleep                   | update t1 set id=sleep(10) where id=1 |
| 47 | root            | localhost       | common_mistakes | Query   |      4 | Waiting for table level lock | select * from t1 where id=2           |
| 49 | root            | localhost:56378 | common_mistakes | Sleep   |    100 |                              | NULL                                  |
| 51 | root            | localhost       | NULL            | Query   |      0 | starting                     | show processlist                      |
+----+-----------------+-----------------+-----------------+---------+--------+------------------------------+---------------------------------------+
5 rows in set (0.00 sec)           

表鎖限制了并發通路。是以,記憶體表的鎖粒度問題,決定了它在處理并發事務時,性能也不好。

資料持久性

資料放在記憶體中,是記憶體表優勢,但也是劣勢。資料庫重新開機時,所有記憶體表會被清空。

若資料庫異常重新開機,記憶體表被清空也就清空了,好像也不會有啥問題呀!但在高可用架構下,記憶體表的這個特點就是個bug!

M-S架構下記憶體表的問題。

  • M-S基本架構
  1. 業務正常通路主庫
  2. 備庫由于xxx而重新開機,記憶體表t1内容被清空
  3. 備庫重新開機後,用戶端發送一條update語句,修改t1的資料行,這時備庫應用線程就會報錯“找不到要更新的行”

這就會導緻主備同步停止。當然了,若此時發生主備切換,用戶端會看到,t1的資料“丢失”了。 在有proxy的架構,預設主備切換的邏輯由資料庫系統自己維護。這樣對用戶端來說,就是“網絡斷開,重連之後,發現記憶體表資料丢失了”。

這也還好呀,畢竟主備發生切換,連接配接會斷開,業務端能夠感覺到異常! 但接下來記憶體表會讓現象更“詭異”。由于MySQL知道重新開機之後,會丢失記憶體表資料。是以,擔心主庫重新開機之後,出現主備不一緻,MySQL會在資料庫重新開機後,往binlog寫一行DELETE FROM t1。 此時若使用的雙M架構:

MySQL 的 Memory存儲引擎還有意義嗎?

備庫重新開機時,備庫binlog裡的delete語句就會傳到主庫,然後把主庫記憶體表删除。這樣你在使用時,就會發現主庫的記憶體表資料突然被清空。

綜上,記憶體表不适合在生産環境使用。

但記憶體表執行速度就是快呀?!

  • 若你的表更新量大,那麼并發度是個重要名額,InnoDB支援行鎖,并發度就是比記憶體表好
  • 能放到記憶體表的資料量都不大。若你考慮的是讀性能,一個讀QPS很高 && 資料量不大的表,即使用InnoDB,資料也都會緩存在 Buffer Pool,讀性能也不會差!

是以,推薦普通記憶體表都用InnoDB表替代。 but!有個場景是例外:使用者臨時表,在資料量可控,不會耗費過多記憶體的情況下,你可以考慮使用記憶體表。

記憶體臨時表剛好可以無視記憶體表的兩個不足,主要因為:

  1. 臨時表不會被其他線程通路,無并發問題
  2. 臨時表重新開機後也需要删除,不存在清空資料問題
  3. 備庫的臨時表也不會影響主庫的使用者線程

看看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);           

這裡使用記憶體臨時表的效果更好:

  • 使用記憶體表不需要寫磁盤,往表temp_t的寫資料的速度更快
  • 索引b使用hash索引,查找的速度比B-Tree索引快
  • 臨時表資料隻有2000行,占用的記憶體有限

是以,可以将臨時表temp_t改成記憶體臨時表,并且在字段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);           
  • 使用記憶體臨時表的執行效果 不論是導入資料的時間,還是執行join的時間,使用記憶體臨時表的速度都比使用InnoDB臨時表要快。

繼續閱讀