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記錄下來,然後在啓動的時候重做這些記錄。具體的實現這裏就不再詳細
展開。