起因
先說說事情的起因哪审,最近在分析數(shù)據(jù)時經(jīng)常遇到一種場景毫痕,代碼需要頻繁的讀某一張數(shù)據(jù)庫的表寒瓦,比如根據(jù)地區(qū)ID獲取地區(qū)名稱、根據(jù)網(wǎng)站分類ID獲取分類名稱柏锄、根據(jù)關(guān)鍵詞ID獲取關(guān)鍵詞等酿箭。雖然以上需求都可以在原始建表時,通過冗余數(shù)據(jù)來解決趾娃。但仍有部分業(yè)務(wù)存的只是關(guān)聯(lián)表的ID缭嫡,數(shù)據(jù)分析時需要頻繁的查表。
所讀的表存在共同的特點
- 數(shù)據(jù)幾乎不會變更
- 數(shù)據(jù)量適中茫舶,從一萬到100多萬械巡,如果全加載到內(nèi)存也不太合適。
糾結(jié)的地方
在做數(shù)據(jù)分析時,需要十分頻繁的讀這些表讥耗,每秒有可能需要讀上萬次有勾。其實內(nèi)部的數(shù)據(jù)庫集群完全可以勝任,但會對線上業(yè)務(wù)稍有影響古程。(你懂得蔼卡,小公司不可能為離線分析做一套完整的數(shù)據(jù)存儲服務(wù)。大部分?jǐn)?shù)據(jù)分析還要借助線上的數(shù)據(jù)集群)
優(yōu)化方案的思考
有沒有一種方式可以不增加線上的壓力挣磨,同時提供更高效的查詢方式雇逞?想過redis,但最終選擇用文本存儲茁裙。因為數(shù)據(jù)分析是一個獨立的需求塘砸,不希望與現(xiàn)有的redis集群或者其它存儲服務(wù)有交集。還有一個原因是每次分析的中間結(jié)果晤锥,對下一次分析并沒有很大的實質(zhì)作用掉蔬,并不需要把結(jié)果持久存儲,而且占的內(nèi)存也會較多矾瘾。最終使用文本存儲女轿,然后用二分來查找。特點壕翩,1蛉迹,存儲非常快放妈,雖然redis等nosql服務(wù)雖然已經(jīng)非潮本龋快,但仍無法與文本存儲相提并論大猛;2扭倾,查找的時候使用二分查找淀零,百萬條記錄查詢也可在0.1ms內(nèi)完成(使用線上的普通硬盤挽绩,如果是ssd盤會更快)。
實現(xiàn)步驟
-
將數(shù)據(jù)庫中需要的字段導(dǎo)出到文本
方法:使用mysql的phpmyadmin工具驾中,執(zhí)行sql語句查出主建id和相應(yīng)字段 如以上的關(guān)鍵詞表: select kid, keyword from keyword 然后使用phpmyadmin的導(dǎo)出工具唉堪,可以快速把結(jié)果導(dǎo)出到文本中 操作截圖:
- 將導(dǎo)出的文本(已經(jīng)按id進行過排序)轉(zhuǎn)換格式重新存儲
- 程序讀取轉(zhuǎn)換后的格式
文本存儲格式
說明 :需求中,文本每行有兩列肩民,第一列是主建ID(數(shù)字)唠亚,第二列為文本。整個文本已經(jīng)按第一列有序排列持痰,兩列之間用tab鍵分隔灶搜。
之前有看過ip.dat的存儲,本次仿照其存儲格式:將文本中的內(nèi)容每行轉(zhuǎn)換為固定長度后,存儲到新的文件割卖。搜索時前酿,使用文件操作函數(shù)fopen,fseek鹏溯,fgets等函數(shù)按字節(jié)讀取內(nèi)容罢维,并以二分查找法快速定位需要的內(nèi)容。
代碼實現(xiàn)部分
- 通用類丙挽,類似需求只需要提供符合標(biāo)準(zhǔn)的文本(每行兩列肺孵,第一列為查找的ID,第二列為文本颜阐。同時文本已經(jīng)按第一列有序排序)
- 生成以上所提到的存儲格式
- 提供根據(jù)id查詢接口
代碼片斷
-
重新生成新的存儲格式
//讀源文件平窘,寫入到新的索引文件 $readfd = fopen($this->filename, 'rb'); $writefd = fopen($this->formatFile.'_tmp', 'wb+'); if ($readfd === false || $writefd === false) { return false; } echo "\n start reformat file $this->filename .."; while (!feof($readfd)) { $line = fgets($readfd, 8192); fwrite($writefd, pack("a".$this->maxLength, $line)); } echo "\n reformat ok\n"; fclose($readfd); fclose($writefd); rename($this->formatFile.'_tmp', $this->formatFile);
-
二分查找的代碼片斷
/** * 在索引文件中進行二分查找 * @param int $id 進行二分查找的id * @return [type] [description] */ public function search($key) { $filesize = filesize($this->formatFile); $fd = fopen($this->formatFile, "rb"); $left = 0; //行號 $right = ($filesize / $this->maxLength) - 1; while ($left <= $right) { $middle = intval(($right + $left)/2); fseek($fd, ($middle) * $this->maxLength); $info = unpack("a*", fread($fd, $this->maxLength))['1']; $lineinfo = explode("\t", $info, 2); if ($lineinfo['0'] > $key) { $right = $middle - 1; } elseif ($lineinfo['0'] < $key) { $left = $middle + 1; } else { return $lineinfo['1']; } } return false; }
整個類庫代碼一共91行,具體可查看github的demo代碼
運行截圖
以上拿100萬的關(guān)鍵詞進行測試凳怨,根據(jù)關(guān)鍵詞id快速查找關(guān)鍵詞初婆,平均速度可以達到0.1毫秒。