我在上一篇文章最後,給你留下的問題是怎麼在兩張表中拷貝數據。若是能夠控制對源表的掃描行數和加鎖範圍很小的話,咱們簡單地使用 insert … select 語句便可實現。mysql
固然,爲了不對源表加讀鎖,更穩妥的方案是先將數據寫到外部文本文件,而後再寫回目標表。這時,有兩種經常使用的方法。接下來的內容,我會和你詳細展開一下這兩種方法。sql
爲了便於說明,我仍是先建立一個表 db1.t,並插入 1000 行數據,同時建立一個相同結構的表 db2.t。數據庫
create database db1; use db1; create table t(id int primary key, a int, b int, index(a))engine=innodb; delimiter ;; create procedure idata() begin declare i int; set i=1; while(i<=1000)do insert into t values(i,i,i); set i=i+1; end while; end;; delimiter ; call idata(); create database db2; create table db2.t like db1.t
假設,咱們要把 db1.t 裏面 a>900 的數據行導出來,插入到 db2.t 中。安全
一種方法是,使用 mysqldump 命令將數據導出成一組 INSERT 語句。你可使用下面的命令bash
mysqldump -h$host -P$port -u$user --add-locks=0 --no-create-info --single-transaction --set-gtid-purged=OFF db1 t --where="a>900" --result-file=/client_tmp/t.sql
把結果輸出到臨時文件。服務器
這條命令中,主要參數含義以下:session
經過這條 mysqldump 命令生成的 t.sql 文件中就包含了如圖 1 所示的 INSERT 語句。app
圖 1 mysqldump 輸出文件的部分結果spa
能夠看到,一條 INSERT 語句裏面會包含多個 value 對,這是爲了後續用這個文件來寫入數據的時候,執行速度能夠更快。線程
若是你但願生成的文件中一條 INSERT 語句只插入一行數據的話,能夠在執行mysqldump 命令時,加上參數–skip-extended-insert。
而後,你能夠經過下面這條命令,將這些 INSERT 語句放到 db2 庫裏去執行。
mysql -h127.0.0.1 -P13000 -uroot db2 -e "source /client_tmp/t.sql"
須要說明的是,source 並非一條 SQL 語句,而是一個客戶端命令。mysql 客戶端執行這個命令的流程是這樣的:
1. 打開文件,默認以分號爲結尾讀取一條條的 SQL 語句;
2. 將 SQL 語句發送到服務端執行。
也就是說,服務端執行的並非這個「source t.sql"語句,而是 INSERT 語句。因此,不管是在慢查詢日誌(slow log),仍是在 binlog,記錄的都是這些要被真正執行的
INSERT 語句。
另外一種方法是直接將結果導出成.csv 文件。MySQL 提供了下面的語法,用來將查詢結果
導出到服務端本地目錄:
select * from db1.t where a>900 into outfile '/server_tmp/t.csv';
咱們在使用這條語句時,須要注意以下幾點。
1. 這條語句會將結果保存在服務端。若是你執行命令的客戶端和 MySQL 服務端不在同一個機器上,客戶端機器的臨時目錄下是不會生成 t.csv 文件的。
2. into outfile 指定了文件的生成位置(/server_tmp/),這個位置必須受參數
secure_file_priv 的限制。參數 secure_file_priv 的可選值和做用分別是:
3. 這條命令不會幫你覆蓋文件,所以你須要確保 /server_tmp/t.csv 這個文件不存在,不然執行語句時就會由於有同名文件的存在而報錯。
4. 這條命令生成的文本文件中,原則上一個數據行對應文本文件的一行。可是,若是字段中包含換行符,在生成的文本中也會有換行符。不過相似換行符、製表符這類符號,前
面都會跟上「\」這個轉義符,這樣就能夠跟字段之間、數據行之間的分隔符區分開。
獲得.csv 導出文件後,你就能夠用下面的 load data 命令將數據導入到目標表 db2.t 中。
load data infile '/server_tmp/t.csv' into table db2.t;
獲得.csv 導出文件後,你就能夠用下面的 load data 命令將數據導入到目標表 db2.t 中。
這條語句的執行流程以下所示。
1. 打開文件 /server_tmp/t.csv,以製表符 (\t) 做爲字段間的分隔符,以換行符(\n)做爲記錄之間的分隔符,進行數據讀取;
2. 啓動事務。
3. 判斷每一行的字段數與表 db2.t 是否相同:
4. 重複步驟 3,直到 /server_tmp/t.csv 整個文件讀入完成,提交事務。
你可能有一個疑問,若是 binlog_format=statement,這個 load 語句記錄到 binlog裏之後,怎麼在備庫重放呢?
因爲 /server_tmp/t.csv 文件只保存在主庫所在的主機上,若是隻是把這條語句原文寫到binlog 中,在備庫執行的時候,備庫的本地機器上沒有這個文件,就會致使主備同步中止步
因此,這條語句執行的完整流程,實際上是下面這樣的。
1. 主庫執行完成後,將 /server_tmp/t.csv 文件的內容直接寫到 binlog 文件中。
2. 往 binlog 文件中寫入語句 load data local infile ‘/tmp/SQL_LOAD_MB-1-0’INTO TABLE `db2`.`t`。
3. 把這個 binlog 日誌傳到備庫。
4. 備庫的 apply 線程在執行這個事務日誌時:
a. 先將 binlog 中 t.csv 文件的內容讀出來,寫入到本地臨時目錄/tmp/SQL_LOAD_MB-1-0 中;
b. 再執行 load data 語句,往備庫的 db2.t 表中插入跟主庫相同的數據。
執行流程如圖 2 所示:
圖 2 load data 的同步流程
注意,這裏備庫執行的 load data 語句裏面,多了一個「local」。它的意思是「將執行這條命令的客戶端所在機器的本地文件 /tmp/SQL_LOAD_MB-1-0 的內容,加載到目標表db2.t 中」。
1. 不加「local」,是讀取服務端的文件,這個文件必須在 secure_file_priv 指定的目錄或子目錄下;
2. 加上「local」,讀取的是客戶端的文件,只要 mysql 客戶端有訪問這個文件的權限便可。這時候,MySQL 客戶端會先把本地文件傳給服務端,而後執行上述的 load data流程。
另外須要注意的是,select …into outfile 方法不會生成表結構文件, 因此咱們導數據時還須要單獨的命令獲得表結構定義。mysqldump 提供了一個–tab 參數,能夠同時導出表結
構定義文件和 csv 數據文件。這條命令的使用方法以下:
mysqldump -h$host -P$port -u$user ---single-transaction --set-gtid-purged=OFF db1 t --where="a>900" --tab=$secure_file_priv
這條命令會在 $secure_file_priv 定義的目錄下,建立一個 t.sql 文件保存建表語句,同時建立一個 t.txt 文件保存 CSV 數據。
前面咱們提到的 mysqldump 方法和導出 CSV 文件的方法,都是邏輯導數據的方法,也就是將數據從表 db1.t 中讀出來,生成文本,而後再寫入目標表 db2.t 中。
你可能會問,有物理導數據的方法嗎?好比,直接把 db1.t 表的.frm 文件和.ibd 文件拷貝到 db2 目錄下,是否可行呢?
答案是不行的。
由於,一個 InnoDB 表,除了包含這兩個物理文件外,還須要在數據字典中註冊。直接拷貝這兩個文件的話,由於數據字典中沒有 db2.t 這個表,系統是不會識別和接受它們的
不過,在 MySQL 5.6 版本引入了可傳輸表空間(transportable tablespace) 的方法,能夠經過導出 + 導入表空間的方式,實現物理拷貝表的功能。
假設咱們如今的目標是在 db1 庫下,複製一個跟表 t 相同的表 r,具體的執行步驟以下:
1. 執行 create table r like t,建立一個相同表結構的空表;
2. 執行 alter table r discard tablespace,這時候 r.ibd 文件會被刪除;
3. 執行 flush table t for export,這時候 db1 目錄下會生成一個 t.cfg 文件;
4. 在 db1 目錄下執行 cp t.cfg r.cfg; cp t.ibd r.ibd;這兩個命令(這裏須要注意的是,拷貝獲得的兩個文件,MySQL 進程要有讀寫權限);
5. 執行 unlock tables,這時候 t.cfg 文件會被刪除;
6. 執行 alter table r import tablespace,將這個 r.ibd 文件做爲表 r 的新的表空間,因爲這個文件的數據內容和 t.ibd 是相同的,因此表 r 中就有了和表 t 相同的數據。
至此,拷貝表數據的操做就完成了。這個流程的執行過程圖以下:
圖 3 物理拷貝表
關於拷貝表的這個流程,有如下幾個注意點:
1. 在第 3 步執行完 flsuh table 命令以後,db1.t 整個表處於只讀狀態,直到執行 unlocktables 命令後才釋放讀鎖;
2. 在執行 import tablespace 的時候,爲了讓文件裏的表空間 id 和數據字典中的一致,會修改 r.ibd 的表空間 id。而這個表空間 id 存在於每個數據頁中。所以,若是是一
個很大的文件(好比 TB 級別),每一個數據頁都須要修改,因此你會看到這個 import語句的執行是須要一些時間的。固然,若是是相比於邏輯導入的方法,import 語句的耗時是很是短的。
今天這篇文章,我和你介紹了三種將一個表的數據導入到另一個表中的方法。
咱們來對比一下這三種方法的優缺點。
1. 物理拷貝的方式速度最快,尤爲對於大表拷貝來講是最快的方法。若是出現誤刪表的狀況,用備份恢復出誤刪以前的臨時庫,而後再把臨時庫中的表拷貝到生產庫上,是恢復
數據最快的方法。可是,這種方法的使用也有必定的侷限性:
2. 用 mysqldump 生成包含 INSERT 語句文件的方法,能夠在 where 參數增長過濾條件,來實現只導出部分數據。這個方式的不足之一是,不能使用 join 這種比較複雜的
where 條件寫法。
3. 用 select … into outfile 的方法是最靈活的,支持全部的 SQL 寫法。但,這個方法的缺點之一就是,每次只能導出一張表的數據,並且表結構也須要另外的語句單獨備份。
後兩種方式都是邏輯備份方式,是能夠跨引擎使用的。
最後,我給你留下一個思考題吧。
咱們前面介紹 binlog_format=statement 的時候,binlog 記錄的 load data 命令是帶local 的。既然這條命令是發送到備庫去執行的,那麼備庫執行的時候也是本地執行,爲什
麼須要這個 local 呢?若是寫到 binlog 中的命令不帶 local,又會出現什麼問題呢?
你能夠把你的分析寫在評論區,我會在下一篇文章的末尾和你討論這個問題。感謝你的收聽,也歡迎你把這篇文章分享給更多的朋友一塊兒閱讀。
我在上篇文章最後給你留下的思考題,已經在今天這篇文章的正文部分作了回答。上篇文章的評論區有幾個很是好的留言,我在這裏和你分享一下。
@huolang 同窗提了一個問題:若是 sessionA 拿到 c=5 的記錄鎖是寫鎖,那爲何sessionB 和 sessionC 還能加 c=5 的讀鎖呢?
這是由於 next-key lock 是先加間隙鎖,再加記錄鎖的。加間隙鎖成功了,加記錄鎖就會被堵住。若是你對這個過程有疑問的話,能夠再複習一下第 30 篇文章中的相關內容。
@一大隻 同窗作了一個實驗,驗證了主鍵衝突之後,insert 語句加間隙鎖的效果。比我在上篇文章正文中提的那個回滾致使死鎖的例子更直觀,體現了他對這個知識點很是好的理
解和思考,很贊。
@roaming 同窗驗證了在 MySQL 8.0 版本中,已經可以用臨時表處理 insert … select 寫入原表的語句了。
@老楊同志 的回答提到了咱們本文中說到的幾個方法。