一.本文所涉及的內容(Contents)
二.背景(Contexts)
之前我寫過關於SQL Server的數據遷移自動化的文章:SQL Server 數據庫遷移偏方,在上篇文章中設計了一張臨時表,這個臨時表記錄搬遷的配置信息,用一個存儲過程讀取這張表進行數據的遷移,再由一個Job進行迭代調用這個存儲過程。
在這次MySQL的實戰中,我的數據庫已經做了4個分片,分布在不同的4台機器上,每台機器上的數據量有1.7億(1.7*4=6.8億),占用空間260G(260*4=1040G),這次遷移的目的就是刪除掉一些歷史記錄,減輕數據庫壓力,有人說這為什么不使用表分區呢?這跟我們的業務邏輯有關造成無法使用表分區,至於為什么,參考閱讀:MySQL表分區實戰,其中最重要就是唯一索引的問題,擴展閱讀:MySQL當批量插入遇上唯一索引,這篇文章需要了解MySQL的定時器的一些知識:MySQL定時器Events
本文與SQL Server 數據庫遷移偏方最大的不同就是MySQL的Events不是串行執行的,當作業調用的存儲過程還沒有執行完畢,但又到了調度的時間,MySQL不會等待上次作業完成之后再調度,所以會造成重復調用讀取到相同的數據;而SQL Server並不存在上面的問題。
三.設計思路(Design)
1. 創建一個臨時表TempBlog_Log,這個表用於保存每次轉移數據的ID起始值和結束值,以及搬遷的開始時間和結束時間;(這個ID是我們要遷移表的主鍵,自增字段,唯一標識)
2. 創建一個存儲過程InsertData(),這個存儲過程用於在TempBlog_Log表中插入記錄,創建這個存儲過程是因為MySQL跟SQL Server有些不同,MySQL不支持匿名存儲過程,SQL Server直接執行SQL就可以了,無需為這些SQL再創建一個存儲過程,這就是匿名存儲過程了;
3. 創建一個存儲過程MoveBlogData(),這個存儲過程用於在TempBlog_Log表中讀取記錄,再批量把BlogA數據轉移到BlogB中;這個是核心邏輯,解決了定時器重復調度的問題,詳情見代碼的解釋;
4. 創建一個定時器e_Blog, 這個定時器定時調用存儲過程MoveBlogData(),但是這里存在重復調度的問題,只能通過存儲過程MoveBlogData()進行控制。
四.遷移自動化特點(Points)
1. 該設計適應於大數據的遷移;
2. 可以最小化宕機時間(在轉移的過程中BlogA還是一直在進數據的,只是在最后一部分數據的時候需要短時間的停入庫操作);
3. 可以防止MySQL定時器重復執行所帶來的問題;
4. 可以實時監控數據轉移的進度;
5. 數據遷移可能需要持續好幾天的時間,它能保證BlogB的數據會無限的接近BlogA的數據;
五.實現代碼(SQL Codes)
(一) 創建臨時表TempBlog_Log
-- 創建表 CREATE TABLE TempBlog_Log( BeginId INT NOT NULL, EndId INT NOT NULL, IsDone BIT DEFAULT b'0' NOT NULL, BeginTime DATETIME DEFAULT NULL, EndTime DATETIME DEFAULT NULL, PRIMARY KEY(BeginId) );
下面就對表結構進行字段解釋:
1) BeginId、EndId都是ServerA遷移表的主鍵值,BeginId表示一次數據遷移的起始值,EndId表示一次數據遷移的結束值,兩個值的差就是這次數據轉移的數據量;
2) IsDone 表示是否已經成功轉移數據;
3) BeginTime表示轉移的開始時間,EndTime表示轉移的結束時間,這兩個字段設置缺省值為NULL很關鍵,是后面進行判斷是否重復執行的依據;
(二) 創建存儲過程InsertData()
-- 存儲過程 DELIMITER $$ USE `DataBaseName`$$ DROP PROCEDURE IF EXISTS `InsertData`$$ CREATE DEFINER=`root`@`%` PROCEDURE `InsertData`() BEGIN DECLARE ids_begin,ids_end,ids_increment INT; SET ids_begin=130000000;-- 需要轉移開始Id值 SET ids_end=210000000;-- 需要轉移結束Id值 SET ids_increment=200000;-- 每次轉移的Id量 WHILE ids_begin < ids_end DO INSERT INTO TempBlog_Log(BeginId,EndId) VALUES(ids_begin,ids_begin+ids_increment); SET ids_begin = ids_begin + ids_increment; END WHILE; END$$ DELIMITER ;
MySQL中不支持匿名存儲過程,所以為了在臨時表TempBlog_Log插入記錄,只能創建一個存儲過程了,如果你還沒寫過MySQL的存儲過程,那么這是一個很好的例子。
1) 為了能在存儲過程中使用MySQL的分隔符“;”,DELIMITER $$表示你以“$$”作為分隔符,你也可以使用“//”;
2) 定義變量時,你需要把所有的變量定義完了,之后再進行賦值,不然會報錯,這跟SQL Server是有區別的;
3) WHILE條件后面需要加DO,而且要以END WHILE;作為結束標記;
4) 作為存儲過程的結束,再次出現“$$”表示已經結束,跟上一個“$$”形成一個整體、過程,並重新設置“;”為分隔符;
5) 執行CALL InsertData();調用上面的存儲過程,插入數據,調用完畢的結果如下圖Figure1所示:
(Figure1:轉移前狀態)
(三) 創建保留數據的新表BlogB
做完上面的准備工作,接下來就是創建與BlogA相同結構的BlogB表了,有些不同的就是不需要在BlogB創建太多的索引,只需要存儲兩個索引就可以了,一個是ID的聚集索引,一個是唯一索引(在批量插入的時候需要判重);
上面索引是根據我業務上的需求決定的,你需要視情況而定;
(四) 創建存儲過程MoveBlogData()
DELIMITER $$ USE `DataBaseName`$$ DROP PROCEDURE IF EXISTS `MoveBlogData`$$ CREATE DEFINER=`root`@`localhost` PROCEDURE `MoveBlogData`() BEGIN DECLARE blog_ids_begin INT;-- Id起始值 DECLARE blog_ids_end INT;-- Id結束值 DECLARE blog_ids_max INT;-- BlogA表現在的最大值 DECLARE blog_begintime INT;-- 執行開始時間 DECLARE blog_endtime INT;-- 執行結束時間 -- 查詢TempBlog_Log表還沒有done的記錄 SELECT BeginId,EndId,BeginTime,EndTime INTO blog_ids_begin,blog_ids_end,blog_begintime,blog_endtime FROM TempBlog_Log WHERE IsDone = 0 ORDER BY BeginId LIMIT 0,1; -- 防止了定時器的重復執行 IF(blog_begintime IS NULL AND blog_endtime IS NULL) THEN -- 設置當前最大的Id值 SELECT MAX(ids) INTO blog_ids_max FROM BlogA; -- 防止轉移超過當前最大值的Id數據 IF(blog_ids_begin != 0 AND blog_ids_end != 0 AND blog_ids_max >= blog_ids_end) THEN -- 更新執行開始時間 UPDATE TempBlog_Log SET BeginTime = NOW() WHERE BeginId = blog_ids_begin; -- 插入Id段數據,忽略重復值 INSERT IGNORE INTO BlogB (ID,AuthorID,Content,QUOTE,QuoteID,Author,TIME,Url,ImageUrl,Transmits,Comments,HASH,Site,AuthorUID,TYPE,HotTopic,AddOn,QuoteAuthorID,IDs) SELECT ID,AuthorID,Content,QUOTE,QuoteID,Author,TIME,Url,ImageUrl,Transmits,Comments,HASH,Site,AuthorUID,TYPE,HotTopic,AddOn,QuoteAuthorID,IDs FROM BlogA WHERE IDs >= blog_ids_begin AND IDs < blog_ids_end; -- 更新執行結束時間 UPDATE TempBlog_Log SET IsDone = 1,EndTime = NOW() WHERE BeginId = blog_ids_begin; END IF; END IF; END$$ DELIMITER ;
這個存儲過程是整個搬遷數據的核心代碼,之所以說是核心,是因為它把比較多的細節考慮進去,基本上實現自動化的目的。
1) 代碼中IF(blog_begintime IS NULL AND blog_endtime IS NULL) 防止了定時器的重復執行,兩個值都為NULL的時候表示這個Id段的數據還沒有被轉移,這樣就可以跳過,不執行下面的邏輯;
2) 查詢BlogA的最大值可以防止轉移超過當前BlogA最大值的Id數據,只有當blog_ids_max>=blog_ids_end才符合轉移的條件;
3) 在MySQL中對唯一索引約束的數據操作有很多的關鍵字支持,INSERT IGNORE INTO就是在批量插入過程中只插入沒有的數據,忽略重復的數據;更多唯一索引的信息:MySQL當批量插入遇上唯一索引
4) 查詢中FROM BlogA WHERE IDs >= blog_ids_begin AND IDs < blog_ids_end;需要注意IDs值的閉合關系,不然造成重復數據或者丟失數據;
(五) 創建定時器e_Blog
DELIMITER $$ CREATE DEFINER=`root`@`localhost` EVENT `e_blog` ON SCHEDULE EVERY 30 SECOND STARTS '2012-12-07 14:58:53' ON COMPLETION PRESERVE DISABLE DO CALL MoveBlogData()$$ DELIMITER ;
這定時器e_Blog的作用是在每隔30 SECOND調用一次存儲過程MoveBlogData(),至於有沒轉移數據那就是存儲過程判斷了,跟定時器的調度頻率完全沒有關系,更多關於定時器的信息:MySQL定時器Events
(六) 監控數據轉移的狀態
當定時器啟動后,可以查看TempBlog_Log表監控調度的進度:
(Figure2:轉移中狀態)
Figure2表示正在轉移Id>=225200000到Id<225400000這20W的數據;
你也可以通過下面的SQL進行統計:
SELECT IsDone,COUNT(1) FROM tempblog_log GROUP BY IsDone ORDER BY IsDone DESC;
(七) 創建索引
創建保留數據的新表BlogB的時候不要創建不必要的索引,等轉移完數據之后再創建回相關的索引;這樣做的目的是在插入數據的時候不需要對索引進行維護,並且到轉移完之后再創建索引可以讓索引更加沒有索引碎片;
(八) 禁用定時器
當TempBlog_Log表不再更新的時候,我們就可以禁用定時器了。因為BlogA表是一直在進數據的,所以當TempBlog_Log不再更新就說明數據已經基本轉移完畢了(新增的數據量小於20W),這個時候就可以禁用定時器了。
(九) 轉移最后數據
首先停止對BlogA表的入庫操作,通過SQL轉移最后一部分的數據到BlogB中,轉移完之后修改表名就大功告成了。