最近做一個數據可視化項目時,需要 Mock 大量的數據(千萬級)來進行接口開發。本文將記錄探索實踐的全部過程。
1. 如何快速 Mock 大量數據
1.1 逐條插入數據
因為博主主業是搞前端開發的,對 MySQL 其實不是特別在行了。要 Mock 數據,第一想法當然是寫個程序或腳本來自動插入數據了。于是說干就干,很快一個基于 NodeJs 的 demo 就完成了。
建表 sql(為了演示方便,這里僅取4個字段,原測試 demo 有 21 個字段):
use test; CREATE TABLE test.data ( `id` INT NOT NULL, `nt` VARCHAR(100) NOT NULL, `imei` VARCHAR(100) NOT NULL, `model` VARCHAR(50) NOT NULL, PRIMARY KEY (id) )
逐條插入數據代碼:
let mysql = require('mysql'); const uuidv1 = require('uuid/v1'); let connection = mysql.createConnection({ host: 'localhost', port: '6666', user: 'root', password: '123456', database: 'test' }); const close = () => connection.end(); function network() { return ['"wifi"', '"g4"', '"g3"', '"g2"'][Math.floor(Math.random() * 4)] } function phone_model() { return ['"NEX"','"x23"','"x21"','"x20"'][Math.floor(Math.random() * 4)] } function imei() { return `"${uuidv1()}"`; } connection.connect(); let s = Date.now(); let i = 0; function insert() { connection.query(`insert into data(id, nt, imei, model) values (${i}, ${network()}, ${imei()}, ${phone_model()})`, function (error, results, fields) { if (error) throw error; if (i + 1 < 100000) { i++; insert(); } else { console.log('done:' + (Date.now() - s)); } }); } insert();
用 10 條數據測試了下,perfect!完美工作。清空表,數量加大到1萬條數據測試,勉強 perfect ,這次插入1萬數據耗時48192ms,也就是48s左右,還能勉強接受。
繼續擴量到10萬條數據,這下就尷尬的很明顯了,生成10萬條數據,一共耗時618983ms,618s,也就是10分鐘左右。大概心里估計了下,就算以線性遞增來算,那么100萬條數據大概就是100分鐘,1000萬數據大概是1000分鐘,也就是近17個小時。
很明顯,這效率太慢了,必須尋找效率更高的方式。
1.2 使用儲存過程批量插入數據
在網上搜索了一會兒,果然找到了一種新方式:使用 mysql 儲存過程來批量插入數據。所謂“儲存過程”,個人認為就是批處理。
建表 sql,因為數據量大,這里加上了分區:
use test; CREATE TABLE test.data ( `id` INT NOT NULL, `nt` VARCHAR(100) NOT NULL, `imei` VARCHAR(100) NOT NULL, `model` VARCHAR(50) NOT NULL, PRIMARY KEY (id) ) ENGINE = MyISAM ROW_FORMAT = DEFAULT partition BY RANGE (id) ( partition p0 VALUES LESS THAN (10000000), partition p1 VALUES LESS THAN (20000000), partition p2 VALUES LESS THAN (30000000), partition p3 VALUES LESS THAN (40000000), partition p4 VALUES LESS THAN (50000000), partition p5 VALUES LESS THAN (60000000), partition p6 VALUES LESS THAN (70000000), partition p7 VALUES LESS THAN (80000000), partition p8 VALUES LESS THAN (90000000), Partition p9 VALUES LESS THAN MAXVALUE );
接著是創建 mysql 儲存過程,不過在編寫儲存過程代碼時,遇到了一個問題,如何實現下面函數的功能,即隨機從 wigi, 4g, 3g, 2g 中返回一個網絡類型。
function network() { return ['"wifi"', '"g4"', '"g3"', '"g2"'][Math.floor(Math.random() * 4)] }
2. 如何在儲存過程中基于數組來生成隨機值
查了大量資料,發現 mysql 儲存過程不支持數組操作。一時間,似乎走到了死胡同。最后發現了這篇文章:MySQL函數和存儲過程生成電話號碼。作者生成電話號碼的思路給了我啟發,于是我參照他的思路,實現了隨機生成網絡類型的功能。
---------------------------- -- 生成網絡類型的函數 ---------------------------- DELIMITER $$ create function network() returns char(4) begin declare networks varchar(100) default "wifi4g 3g 2g "; -- 1,5,9,13 declare idx int; declare ret char(4); set idx = 1+floor(rand()*4)*4; set ret = trim(substring(networks,idx,4)); return ret; end $$ DELIMITER ;
大概思路就是:
- 找出數組中最長的字符串項,比如 wifi,4g,3g,2g 中最長的項是 'wifi',長度為4
- 將數組所有項用空格填充,讓其與最長項長度一樣,即wifi4g 3g 2g
- 隨機生成固定的字符截取起始點。這里的隨機,固定可能會不太好理解。看這個表達式1+floor(rand()*4)*4就清楚了,此表達式總是返回 1,5,9,13中某個值
- 截取字符串,同時去掉填充的空格,trim(substring(networks,idx,4)),就得到隨機值了
解決隨機生成值的問題后,儲存過程的代碼也就出來了:
---------------------------- -- 生成網絡類型的函數 ---------------------------- DELIMITER $$ create function network() returns char(4) begin declare networks varchar(100) default "wifi4g 3g 2g "; -- 1,5,9,13 declare idx int; declare ret char(4); set idx = 1+floor(rand()*4)*4; set ret = trim(substring(networks,idx,4)); return ret; end $$ DELIMITER ; ---------------------------- -- 生成機型的函數 ---------------------------- DELIMITER $$ create function phone_model() returns char(10) begin declare phone_types varchar(100) default "NEX x23 x21 x20 x9 x7 x6 x5 Z1 Z2 Z3 Y97 Y91 Y85 Y83 Y81 Y79 "; declare idx int; declare ret char(10); set idx = 1+floor(rand()*17)*4; set ret = trim(substring(phone_types,idx,4)); return ret; end $$ DELIMITER ; ---------------------------- -- 生成IMEI的函數 ---------------------------- DELIMITER $$ create function randchar() returns char(5) begin declare ret char(5); set ret = substring("ABCDEFGHIJKLMNOPQRSTUVWXYZ",floor(1+26*rand()),1); return ret; end $$ DELIMITER ; DELIMITER $$ create function imei() returns char(50) begin declare ret char(50) default ""; declare imeiLen int default 11; DECLARE idx INT default 0; WHILE idx < imeiLen DO SET idx = idx + 1; SET ret = CONCAT(ret, randchar()); END WHILE; return ret; end $$ DELIMITER ; ---------------------------- -- 創建儲存過程 ---------------------------- use test; DROP PROCEDURE IF EXISTS test.BatchInsertCustomer; delimiter // CREATE PROCEDURE BatchInsertCustomer(IN start INT,IN loop_time INT) BEGIN DECLARE Var INT; DECLARE ID INT; SET Var = 0; SET ID= start; WHILE Var < loop_time DO insert into data(`id`, `nt`, `imei`, `model`) values (ID, network(), imei(), phone_model()); SET Var = Var + 1; SET ID = ID + 1; END WHILE; END; // delimiter ;
調用儲存過程:
-- 調用 ALTER TABLE test DISABLE KEYS; CALL BatchInsertCustomer(1, 10); ALTER TABLE test ENABLE KEYS;
在測試時,使用儲存過程生成1000萬數據大概是140分鐘,不到2個半小時。相比逐條插入的17個小時,快了8,9倍,效率提升不少。
3. 小結
使用 mysql 儲存過程可以快速地生成 Mock 數據。同時本文還提供了一種“如何在儲存過程中基于數組來生成隨機值“的思路,希望對大家有些幫助。
3.1 參考
- MySQL批量插入大量數據方法
- MySQL函數和存儲過程生成電話號碼