MySQL Temporary Table相關問題的探究

MySQL Temporary Table相關問題的探究


問題的引入

讓我們先來觀察幾條非常簡單的MySQL語句:

mysql> create temporary table tmp(id int, data char(20));
Query OK, 0 rows affected (0.00 sec)

mysql> create table tmp(id int, data char(20));
Query OK, 0 rows affected (0.00 sec)

mysql> drop table tmp;
Query OK, 0 rows affected (0.00 sec)

mysql> drop table tmp;
Query OK, 0 rows affected (0.00 sec)

mysql> drop table tmp;
ERROR 1051 (42S02): Unknown table 'tmp'

這是丁奇提出的引導性的問題,幾條語句看似簡單,不過接下來我們提出的一連串問題與進
行的研究可都是圍繞它們來的!

看到以上語句,你很容易會產生類似於以下的疑問:

1. 上述語句在一個session中先後創建了兩個名爲’tmp’的table,只不過一個是temporary
table,一個是normal table。問題來了:temporary table爲何可以與同名的normal table
共存?

2. 上述語句成功執行了兩條DROP TABLE語句,那麼每一條語句操作的對象是哪個table呢?
亦即同名的temporary table與normal table之間的優先級關係是如何的?

很好,有了問題就知道了前進的方向!接下來我們就從這兩個問題入手,由淺入深,開始我
們的探索之旅吧!

單機模式下的同名問題與優先級問題的探究

我們不妨從現象入手,先來驗證第二個問題的結果究竟如何,即哪個表擁有較高的優先級?
爲此我們設計如下的語句:

mysql> create temporary table tmp(id1 int, data1 char(20));
Query OK, 0 rows affected (0.00 sec)

mysql> describe tmp;
+-------+----------+------+-----+---------+-------+
| Field | Type     | Null | Key | Default | Extra |
+-------+----------+------+-----+---------+-------+
| id1   | int(11)  | YES  |     | NULL    |       |
| data1 | char(20) | YES  |     | NULL    |       |
+-------+----------+------+-----+---------+-------+
2 rows in set (0.00 sec)

mysql> insert into tmp values(1, "Some");
Query OK, 1 row affected (0.00 sec)

mysql> select * from tmp;
+------+-------+
| id1  | data1 |
+------+-------+
|    1 | Some  |
+------+-------+
1 row in set (0.00 sec)

mysql> create table tmp(id2 int, data2 char(20));
Query OK, 0 rows affected (0.00 sec)

mysql> describe tmp;
+-------+----------+------+-----+---------+-------+
| Field | Type     | Null | Key | Default | Extra |
+-------+----------+------+-----+---------+-------+
| id1   | int(11)  | YES  |     | NULL    |       |
| data1 | char(20) | YES  |     | NULL    |       |
+-------+----------+------+-----+---------+-------+
2 rows in set (0.00 sec)

mysql> insert into tmp values(2, "Some");
Query OK, 1 row affected (0.00 sec)

mysql> select * from tmp;
+------+-------+
| id1  | data1 |
+------+-------+
|    1 | Some  |
|    2 | Some  |
+------+-------+
2 rows in set (0.00 sec)

以上語句做的工作很簡單:先創建一個名爲’tmp’的temporary table,並insert一個值;
之後創建一個名爲’tmp’的normal table,也insert一個值。最終select時發現,兩次
insert操作均作用於temporary table。

至此我們可以得到初步的印象是,同名的temporary table與normal table共存時,
temporary table較高的優先級。但是別忘了還存在另一種情況:先創建的表總有着較
高的優先級。這個猜想是很容易來驗證它的對錯的,我們只需將剛纔的創建表的順序調
換一下即可。這裏就不再重複代碼,直接給出結果:即使temporary table在normal table
之後創建,諸如select,insert,update等操作仍然優先作用於temporary table之上。
於是我們可以進一步猜測drop表的時候,先drop的也是temporary table。
馬上來驗證一下:

/* 緊接着之前的代碼 */
mysql> drop table tmp;
Query OK, 0 rows affected (0.00 sec)

mysql> select * from tmp;
Empty set (0.01 sec)

mysql> describe tmp;
+-------+----------+------+-----+---------+-------+
| Field | Type     | Null | Key | Default | Extra |
+-------+----------+------+-----+---------+-------+
| id2   | int(11)  | YES  |     | NULL    |       |
| data2 | char(20) | YES  |     | NULL    |       |
+-------+----------+------+-----+---------+-------+
2 rows in set (0.00 sec)

mysql> drop table tmp;
Query OK, 0 rows affected (0.00 sec)

mysql> show tables;
Empty set (0.00 sec)

mysql> describe tmp;
ERROR 1146 (42S02): Table 'test.tmp' doesn't exist

沒問題吧!到這裏我們已經從現象得出了初步的結論:在同一個session下同名的
temporary table與normal table共存時,temporary table總是優先被操作的。那麼
我們可以更進一步提問:爲什麼temporary table的優先級會高於normal table呢?
而且別忘了在本段開始時我們還提出了一個問題:爲什麼在同一session下同名的
temporary table與normaltable可以共存?衆所周知兩個同名的temporary table或
normal table都是不被允許的。我們可以先做出猜想:temporary table與normal table
是存儲在不同的位置的。這個猜想對嗎?要回答這些問題,我們必須到MySQL的源碼中
一探究竟,找尋答案了!

(我插幾句:作爲一個不折不扣的MySQL菜鳥,剛拿到MySQL源碼時我就像拿到了天書,
除了膜拜之外根本不知道從何入手。經過一段時間的摸爬滾打,我高興的發現我終於
窺得了其中的端倪,並深感“任務驅動+gdb”是上手的好方法。MySQL完整源碼可以從
以下地址下載:http://dev.mysql.com/downloads/)

我們可以從創建一張表的流程入手,來探究這個過程(以下代碼中,如果沒有特別註明,
其註釋均爲原碼註釋。)

對於語句

create temporary table tmp(id int, data char(20));
create table tmp(id int, data char(20));

定位到./sql/sql_parse.cc中的mysql_execute_command()函數。

  case SQLCOM_CREATE_TABLE:
  {
    ...

    if ((res= create_table_precheck(thd, select_tables, create_table)))
      goto end_with_restore_list;

      ...

      /* regular create */
      if (create_info.options & HA_LEX_CREATE_TABLE_LIKE)
        res= mysql_create_like_table(thd, create_table, select_tables,
                                     &create_info);
      else
      {
        res= mysql_create_table(thd, create_table->db,
                                create_table->table_name, &create_info,
                                &alter_info, 0, 0);
      }

      ...
  }

首先我們查看同文件中create_table_precheck()函數的實現:

...

      /*
        For temporary tables we don't have to check if the created table exists
      */
      if (!(lex->create_info.options & HA_LEX_CREATE_TMP_TABLE) &&
          find_table_in_global_list(tables, create_table->db,
                                    create_table->table_name))
      {
	error= FALSE;
        goto err;
      }

...

而find_table_in_global_list()函數實質上調用了./sql/sql_base.cc文件中的
find_table_in_list()函數。這個函數的功能就是去內存中的全局table list中遍歷,
確認是否已有同名的normal table存在。注意,對於temporary table,到這裏爲止是不做
重名檢查的。

繼續跟蹤到./sql/sql_talbe.cc中的mysql_create_table()函數。
開頭的註釋說的很清楚:

/*
  Database and name-locking aware wrapper for mysql_create_table_no_lock(),
*/

這個函數實際上是對mysql_create_table_no_lock()的一個封裝,並且處理了一些加鎖
機制。我們繼續跟蹤到同文件的mysql_create_table_no_lock()函數。

...

      /* Check if table exists */
  if (create_info->options & HA_LEX_CREATE_TMP_TABLE)
  {
    path_length= build_tmptable_filename(thd, path, sizeof(path));
    create_info->table_options|=HA_CREATE_DELAY_KEY_WRITE;
  }
  else
  {
    path_length= build_table_filename(path, sizeof(path) - 1, db, alias, reg_ext,
                                      internal_tmp_table ? FN_IS_TMP : 0);
  }

...

這裏我們看到了一個關鍵函數build_tmptable_filename(),它位於./sql/sql_table.cc文件
中,這個函數是爲temporary table命名的。在該函數內部我們又看到如下一段關鍵代碼:

...

  my_snprintf(p, bufflen - (p - buff), "/%s%lx_%lx_%x%s",
              tmp_file_prefix, current_pid,
              thd->thread_id, thd->tmp_table++, reg_ext);

...

有了以上這段代碼,temporary table的命名規則就非常清楚了,其中current_pid爲16進制
形式,thd->thread_id是Client的線程序號,thd->tmp_table就是臨時表序號了,而reg_ext
就是形如*.frm這樣的後綴。

現在我們回到函數mysql_create_table_no_lock(),緊接着剛纔的代碼:

  /* Check if table already exists */
  if ((create_info->options & HA_LEX_CREATE_TMP_TABLE) &&
      find_temporary_table(thd, db, table_name))
  {
    // 如果找到重名的表,那麼執行這裏的錯誤處理代碼(非原註釋)
  }

...

在上面這段代碼中我們又看到了一個關鍵函數find_temporary_table(),這個函數內部是大
有文章的,它會去tmp_table list中去遍歷並檢查temporary table是否已經存在。如果一切
沒有問題,那麼繼續往下執行:

...

  if (rea_create_table(thd, path, db, table_name,
                       create_info, alter_info->create_list,
                       key_count, key_info_buffer, file))

...

這裏我們可以看到rea_create_table()函數的功能是創建normal table的實際數據文件。

...

  if (create_info->options & HA_LEX_CREATE_TMP_TABLE)
  {
    /* Open table and put in temporary table list */
    if (!(open_temporary_table(thd, path, db, table_name, 1)))
    {
      (void) rm_temporary_table(create_info->db_type, path);
      goto unlock_and_end;
    }
    thd->thread_specific_used= TRUE;
  }

...

上面這段代碼是對temporary table操作的,其中open_temporary_table()函數打開一個
temporary table並將其加入thd->temporary_table隊列。繼續往下,在函數末尾看到一
句代碼:

  error= write_create_table_bin_log(thd, create_info, internal_tmp_table);

進入write_create_table_bin_log()函數,上來就是一段非常清晰的註釋:

  /*
    Don't write statement if:
    - It is an internal temporary table,
    - Row-based logging is used and it we are creating a temporary table, or
    - The binary log is not open.
    Otherwise, the statement shall be binlogged.
   */

已經說得很明白了,如果是內部創建的temporary table或者Row-based binlog模式下
創建temporary table或者binlog功能未開啓,那麼不寫binlog,其他情況下都會寫。

至此,MySQL一個典型的創建表的流程就走完了。總結上述代碼,我們可以回答第一個問題,
也就是同名normal table與temporary table共存問題。現在我們知道,normal table與
temporary table保存的位置是不同的,temporary table保存在thd->temporary_table隊列
中,而normal table是保存在全局的隊列中的,這樣同名的normal table與temporary table
就可以共存。並且,temporary table是相對於session的,因爲session結束後相應的線程就
被回收了,那麼對應於該線程的temporary table也就被釋放了。更進一步,從temporary
table的命名規則我們可以看到,每個temporary table都對應着獨特的客戶端線程id,那麼
顯然各個Client之間同名的temporary table是允許共存的。而normal table顯然是在任何情
況下都不允許同。

爲了回答第二個問題,即優先級問題,我們只需要看一下drop一個表的過程即可,其他操作
的原理也是類似的。這裏我們就不再像剛纔那麼詳細的一步步分析源碼,直接給出關鍵代碼
(位於函數mysql_rm_table_part2()中,該函數位於./sql/sql_table.cc)

...

 error= drop_temporary_table(thd, table); // 這裏刪除臨時表(非原註釋)
...
      error= ha_delete_table(thd, table_type, path, db, table->table_name,
                             !dont_log_query); // 這裏刪除表的內容和索引(非原註釋)
...
	/* Delete the table definition file */
	strmov(end,reg_ext);
        // 以下刪除表的定義文件(非原註釋)
	if (!(new_error=my_delete(path,MYF(MY_WME))))
        {
	  some_tables_deleted=1;
          new_error= Table_triggers_list::drop_all_triggers(thd, db,
                                                            table->table_name);
        }
...

從以上代碼我們不難看出,drop表的過程總是先走temporary table,再走normal table的。
這也就解釋了爲何temporary table有着比normal table更高的優先權。

好了,到目前爲止我們已經從本質上回答了文章開頭提出的兩個問題,這樣看起來問題已經
解決的比較圓滿了。但是且慢,我們以上所做的探究全部基於同一臺服務器下,如果是分佈
式的系統,即主從模式下,又會出現什麼樣的狀況呢?下面一節我們繼續探究。

主從模式下temporary table機制的探究

首先我們要說明的是MySQL主從備份的實現機制。我們知道MySQL的衆多日誌類型中有一種爲
binlog日誌類型,凡是涉及到修改數據庫的操作都會被記錄到binlog日誌中。binlog日誌本
身又分爲兩種記錄方式:Statement-based方式,Row-based方式(Mixed方式可以視爲這兩種
方式的混合)。在主從模式下,某個特定的分佈式服務器羣中有兩種服務器:Master(主服務
器)與Slave(從服務器)。Master方將自己的數據修改痕跡以某種方式記錄在本機的binlog文
件中,當有Slave連接到Master時,Master會啓動Binlog dump線程來將本地的binlog內容發
送給Slave方。此時Slave方會啓動兩個線程:Slave I/O線程和Slave SQL線程。Slave I/O
線程讀取從Master的Binlog dump線程發送過來的binlog內容,並將其寫入本機的Relay log
中。Slave SQL線程則從本地的Relay log讀取並且執行需要更新的事件。更具體的實現與配
置細節可以參考官方文檔:http://dev.mysql.com/doc/refman/5.1/en/replication.html

注意到Slave方執行事件的線程只有一個,那就是Slave SQL線程。想一想按照我們目前的理
解,會出現怎樣的問題?回憶剛纔的MySQL temporary table命名規則,其中有一項是線程
id。再回憶剛纔我們說到,由於temporary table是相對於session的,於是不同的Client可
以創建同名的temporary table。問題來了:將這個情景移到主從模式下,Master方同時連
接了兩個Client,每一個Client各自創建了一個名爲a的temporary table。我們假設此時
Master的binlog模式被設置爲Statement-based,那麼這兩個建表事件都會被寫入binlog。
現在Slave I/O線程檢測並讀取了這兩個事件,Slave SQL線程要執行這兩個事件了。按照
我們的想法,此時Slave是不能區分這兩個temporary table的,因爲線程id相同!

但是經過實際驗證,MySQL能處理這個問題,而並沒有像我們預想的那樣會報錯。那麼MySQL
內部是如何處理的呢?讓我們再仔細讀一下建表函數mysql_create_table_no_lock()中的檢
查temporary table名字衝突的函數find_temporary_table()的實現代碼。

...

  key_length= create_table_def_key(thd, key, table_list, 1);

...

顯然create_table_def_key()函數是區分每個temporary table的關鍵,我們繼續看這個函數
內部的細節:

...

    int4store(key + key_length + 4, thd->variables.pseudo_thread_id);

...

這裏我們看到一個關鍵信息:thd->variables.pseudo_thread_id。如果使用gdb調試,我們發
現在find_temporary_table()函數中thd->variables.pseudo_thread_id的值等於Relay-log中
的線程id,也就是Master的binlog中記錄Client的線程id的值。然而注意到Slave SQL線程初
始化函數handle_slave_sql()中調用的 init_slave_thread()函數中有這樣一句代碼:

...

  thd->thread_id= thd->variables.pseudo_thread_id= thread_id++;

...

在這裏,thd->variable.pseudo_thread_id是被初始化爲Slave當前線程id的。那麼它是何時被
修改的呢?繼續看代碼:

...

  while (!sql_slave_killed(thd,rli))
  {
    ...

    if (exec_relay_log_event(thd,rli))
    {

      ...

    }
  }

...

以上代碼進入了執行relay log的循環。exec_relay_log_event()中調用了函數
apply_event_and_update_pos(),而這個函數中調用了ev->apply_event(),最終調用了
Query_log_event::do_apply_event()。在該函數中我們看到:

...

    thd->variables.pseudo_thread_id= thread_id;  // for temp tables

...

就是在這裏,thd->variables.pseudo_thread_id已經被置爲我們想要看到的值了。很神奇吧!

主從模式下temporary table可能造成的不同步問題

現在我們來考慮另外一個問題,即主從模式下temporary table可能引起的主從間不同步問
題。

回憶MySQL創建temporary table過程。該過程除了將temporary table信息加入當前線程所
擁有的temporary table隊列之外,還做了一項工作,即在/tmp目錄下創建了臨時數據文件,
如:

#sql64d6_18_0.frm  #sql64d6_18_0.ibd (InnoDB下)

考慮以下情形:Master機上創建了一個temporary table,並且此時binlog模式爲
Statement-based。於是Slave上讀到了這個事件,並且在Slave上也同步了這個操作,即同樣
建立了一個temporary table。此時由於某種原因,Slave突然意外重啓。我們知道服務器
重啓會導致所有/tmp文件夾下的數據文件被清空,那麼在Slave上,原先的temporary table
不復存在。但是此時Master上的原始的temporary table還是好好的!這樣,如果我們在
Master上做任何對該temporary table上的修改操作都會引起Slave端報錯,產生類似以下信息:

Error 'Table 'test.tmp' doesn't exist' on query. Default database: 'test'.
Query: 'insert into tmp values(SomeValue)'

我們知道在Slave Server關閉後直到重啓前,/tmp目錄下的數據文件都是存在的。問題的本質
在於:Slave Server關閉後,內存中的temporary table鏈表被回收,導致/tmp下的數據文件
沒有對應的數據結構,那麼我們也就無從知曉對應的創建該表的Client到底是哪一個。

解決這個問題的基本思路就是在Slave重啓時以某種方式恢復原先內存中的相關信息。其中一種
思路是,在Slave創建temporary table時,我們額外寫一個文件來記錄與維護數據文件與客戶
端線程id、表名、數據庫名的對應關係。另外一種思路是,在Slave創建temporary table時,
我們將相應的binlog記錄下來,然後在啓動的時候重做這些記錄。具體的實現這裏就不再詳細
展開。



發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章