PHP實現文本快速查找 - 二分查找法

起因

先說說事情的起因,最近在分析數據時經常遇到一種場景,代碼需要頻繁的讀某一張數據庫的表,比如根據地區ID獲取地區名稱、根據網站分類ID獲取分類名稱、根據關鍵詞ID獲取關鍵詞等。雖然以上需求都可以在原始建表時,通過冗余數據來解決。但仍有部分業務存的只是關聯表的ID,數據分析時需要頻繁的查表。

所讀的表存在共同的特點

  • 數據幾乎不會變更
  • 數據量適中,從一萬到100多萬,如果全加載到內存也不太合適。

糾結的地方

在做數據分析時,需要十分頻繁的讀這些表,每秒有可能需要讀上萬次。其實內部的數據庫集群完全可以勝任,但會對線上業務稍有影響。(你懂得,小公司不可能為離線分析做一套完整的數據存儲服務。大部分數據分析還要借助線上的數據集群)

優化方案的思考

有沒有一種方式可以不增加線上的壓力,同時提供更高效的查詢方式?想過redis,但最終選擇用文本存儲。因為數據分析是一個獨立的需求,不希望與現有的redis集群或者其它存儲服務有交集。還有一個原因是每次分析的中間結果,對下一次分析并沒有很大的實質作用,并不需要把結果持久存儲,而且占的內存也會較多。最終使用文本存儲,然后用二分來查找。特點,1,存儲非常快,雖然redis等nosql服務雖然已經非常快,但仍無法與文本存儲相提并論;2,查找的時候使用二分查找,百萬條記錄查詢也可在0.1ms內完成(使用線上的普通硬盤,如果是ssd盤會更快)。

實現步驟

  • 將數據庫中需要的字段導出到文本

      方法:使用mysql的phpmyadmin工具,執行sql語句查出主建id和相應字段
      如以上的關鍵詞表: select kid, keyword from keyword
      然后使用phpmyadmin的導出工具,可以快速把結果導出到文本中
      操作截圖:
    
導出數據流程1

導出數據流程2
  • 將導出的文本(已經按id進行過排序)轉換格式重新存儲
  • 程序讀取轉換后的格式

文本存儲格式

說明 :需求中,文本每行有兩列,第一列是主建ID(數字),第二列為文本。整個文本已經按第一列有序排列,兩列之間用tab鍵分隔。
之前有看過ip.dat的存儲,本次仿照其存儲格式:將文本中的內容每行轉換為固定長度后,存儲到新的文件。搜索時,使用文件操作函數fopen,fseek,fgets等函數按字節讀取內容,并以二分查找法快速定位需要的內容。

代碼實現部分

  • 通用類,類似需求只需要提供符合標準的文本(每行兩列,第一列為查找的ID,第二列為文本。同時文本已經按第一列有序排序)
  • 生成以上所提到的存儲格式
  • 提供根據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萬的關鍵詞進行測試,根據關鍵詞id快速查找關鍵詞,平均速度可以達到0.1毫秒。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容

  • Spring Cloud為開發人員提供了快速構建分布式系統中一些常見模式的工具(例如配置管理,服務發現,斷路器,智...
    卡卡羅2017閱讀 134,837評論 18 139
  • 1. Java基礎部分 基礎部分的順序:基本語法,類相關的語法,內部類的語法,繼承相關的語法,異常的語法,線程的語...
    子非魚_t_閱讀 31,740評論 18 399
  • 一、溫故而知新 1. 內存不夠怎么辦 內存簡單分配策略的問題地址空間不隔離內存使用效率低程序運行的地址不確定 關于...
    SeanCST閱讀 7,865評論 0 27
  • ¥開啟¥ 【iAPP實現進入界面執行逐一顯】 〖2017-08-25 15:22:14〗 《//首先開一個線程,因...
    小菜c閱讀 6,497評論 0 17
  • 我抱怨了一整個月我絕不會去看擺渡人這樣的電影,但還是在生日拉了一小幫人去電影院“隨便看看”。當時的借口是什么竟然...
    濯塵的青泥閱讀 243評論 0 0