一亿条数据的排序处理
假設場景:
某大型網站,活躍用戶上億個。(當然不是指同時在線人數,這里指的是再一段時間內有訪問操作的用戶數量,比如一個小時內)。
現在要每隔1小時,統計一次活躍用戶排行榜(用戶點擊本網站的一個連接,活躍度就加1,按活躍度進行排名)。
首先,在此場景下,解決此問題不涉及數據庫操作(也不可能用戶點擊一下,就更新一下數據庫!),訪問記錄就是記錄在日志文件中,例如:
zhangsan, http://a.com/b/
zhangsan, http://a.com/c/
lisi, http://a.com/b/
lisi, http://a.com/e/
lisi, http://a.com/x/
然后,我們不考慮用戶訪問量的統計過程,假設根據日志文件已經得出了這樣的文件:
zhangsan 2
lisi 3
其中,2、3分別表示對應用戶的活躍度,我們要按此進行排序,但是用戶總量有一億個!
接著,我們繼續抽象、簡化。既然活躍度用整數表示,我們就單獨來考慮整數排序的問題,即,用戶名也先不考慮了,就處理一億個整數的排序。
先嘗試直接使用TreeSet來排序。
TreeSet底層是紅黑樹實現的,排序是很高效的,這里不深究,我們就用它來完成排序:
1. 生產測試數據
package com.bebebird.data.handler;import java.io.File; import java.io.PrintWriter; import java.util.Random;/*** * @author sundeveloper* * 創建測試數據**/ public class DataProducer {/*** 創建數據* @param count 數據量* @param out 輸出文件路徑*/public static void produce(int count, String out) {long t1 = System.currentTimeMillis();File file = new File(out);if(file.exists())file.delete();try (PrintWriter writer = new PrintWriter(file, "UTF-8");) {Random random = new Random();for(int i=0; i<count; i++){writer.write(random.nextInt(count) + "\n");}}catch (Exception e){e.printStackTrace();}long t2 = System.currentTimeMillis();System.out.println("創建成功!耗時:" + (t2 - t1) + "毫秒。");}}調用produce()方法,指定數據量和數據輸出路徑,來生產測試數據。
2. 利用TreeSet對數據進行排序:
package com.bebebird.data.handler;import java.io.BufferedReader; import java.io.File; import java.io.FileInputStream; import java.io.InputStreamReader; import java.util.Arrays; import java.util.TreeSet;/*** * @author sundeveloper* * 使用TreeSet自動將數據排序* * 處理數據量能達到千萬級,一千萬數據排序大約用時20秒。**/ public class SimpleTreeSetHandler {private Integer[] datas = null;/*** 排序* @param in 數據文件路徑*/public void sort(String in){long t1 = System.currentTimeMillis();File file = new File(in);if(!file.exists())return;try(BufferedReader reader = new BufferedReader(new InputStreamReader(new FileInputStream(file),"UTF-8"));){TreeSet<Integer> set = new TreeSet<>();String line = null;while((line = reader.readLine()) != null && !"".equals(line)){set.add(new Integer(line));}this.datas = set.toArray(new Integer[set.size()]);}catch(Exception e){e.printStackTrace();}long t2 = System.currentTimeMillis();System.out.println("排序完成!耗時:" + (t2 - t1) + "毫秒。");}/*** 從pos開始,獲取count個數* @param pos* @param count* @return*/public Integer[] limit(int pos, int count){long t1 = System.currentTimeMillis();if(pos < 0 || count <= 0){return null;}Integer[] result = new Integer[count];for (int i = 0; i < count && pos + i < this.datas.length; i++) {result[i] = this.datas[pos + i];}long t2 = System.currentTimeMillis();System.out.println("取數成功!耗時:" + (t2 - t1) + "毫秒。");return result;}// 測試:// 創建1千萬隨機數,進行排序public static void main(String[] args) {DataProducer.produce(10000000, "data");SimpleTreeSetHandler handler = new SimpleTreeSetHandler();handler.sort("data");Integer[] limit = handler.limit(10, 10);System.out.println(Arrays.asList(limit));} }調用SimpleTreeSetHandler的sort()方法,指定數據文件路徑,對其排序。
經測試,直接使用TreeSet來處理,一千萬數據量很輕松就能處理,大概排序耗時20秒左右。
但是,一億數據量時就廢了!CPU滿,內存占用上2.5G左右,并且N多分鐘后不出結果,只能結束進程!(有條件的話,可以試試,具體多久能排出來)
機器配置簡要說明:2.5 GHz Intel Core i5,系統內存10G。
3. 既然用TreeSet處理一千萬數據很容易,那么把一億條分成10個一千萬不就能夠處理了?每個一千萬用時20秒,10個一千萬大概200秒,三分鐘拍出來還是可以接受的!(當然,這么算不準確,但大概是這個數量級的!)
package com.bebebird.data.handler;import java.io.BufferedReader; import java.io.File; import java.io.FileInputStream; import java.io.InputStreamReader; import java.io.PrintWriter; import java.util.ArrayList; import java.util.Iterator; import java.util.List; import java.util.Set; import java.util.TreeSet;/*** * @author sundeveloper** 將數據進行分成若干片段;* 分別對每個片段進行排序,存入臨時文件;* 將臨時文件進行合并**/ public class DivideTreeSetHandler {/*** 排序* @param in 數據文件路徑* @param size 每個數據文件的大小(行數)*/public List<String> divide(String in, int size){long t1 = System.currentTimeMillis();File file = new File(in);if(!file.exists())return null;List<String> outs = new ArrayList<String>();try(BufferedReader reader = new BufferedReader(new InputStreamReader(new FileInputStream(file),"UTF-8"));){int fileNo = 0; // 臨時文件編號Set<Integer> set = new TreeSet<Integer>();while(true){String line = reader.readLine();// 讀取結束!if(line == null){writeSetToTmpFile(set, fileNo, outs);break;}// 空行,跳過if("".equals(line.trim())){continue;}set.add(new Integer(line));// 數據量達到:if(set.size() >= size){writeSetToTmpFile(set, fileNo, outs);fileNo ++;}}}catch(Exception e){e.printStackTrace();}long t2 = System.currentTimeMillis();System.out.println("拆分完成!耗時:" + (t2 - t1) + "毫秒。");return outs;}// set數據寫入到文件中:private void writeSetToTmpFile(Set<Integer> set, int fileNo, List<String> outs) {long t1 = System.currentTimeMillis();File file = new File("tmp_" + fileNo);if(file.exists())file.delete();try (PrintWriter writer = new PrintWriter(file, "UTF-8");) {Iterator<Integer> iterator = set.iterator();while(iterator.hasNext()){writer.write(iterator.next() + "\n");}set.clear();}catch (Exception e){e.printStackTrace();}long t2 = System.currentTimeMillis();System.out.println("生成臨時文件:" + file.getAbsolutePath() + "!耗時:" + (t2 - t1) + "毫秒。");outs.add(file.getAbsolutePath());}/*** 合并數據* @param ins*/public String combine(List<String> ins) {long t1 = System.currentTimeMillis();if(ins == null || ins.size() <= 1)return null;File file = new File("tmp");if(file.exists())file.delete();try(PrintWriter writer = new PrintWriter(file, "UTF-8");){List<BufferedReader> readers = new ArrayList<>();for (String in : ins) {readers.add(new BufferedReader(new InputStreamReader(new FileInputStream(in),"UTF-8")));}while(readers.size() > 0){BufferedReader reader0 = readers.get(0);while(true){String line = reader0.readLine();if(line == null){readers.remove(0);break;}if("".equals(line.trim()))continue;// 用個set記錄從多個文件中取出的數據,這些數據需要繼續排序:Set<Integer> set = new TreeSet<Integer>();int data = new Integer(line);// 先把data放入set:set.add(data);for(int i = readers.size() - 1; i > 0; i--){BufferedReader readeri = readers.get(i);while(true){// 設置一個標記,如果后邊datai大于data了,需要reset到此處!readeri.mark(1024); String linei = readeri.readLine();if(linei == null){readers.remove(i);break;}if("".equals(linei.trim()))continue;int datai = new Integer(linei);// datai小于data,則把datai放入set,會自動排序if(datai < data){set.add(datai);}// datai等于data,則暫時退出,停止讀取else if(datai == data){break;}// datai大于data,則往回退一行(退到標記處),停止讀取else{readeri.reset();break;}}}// 按data查找,小于data的值,都已經存入set了,此時把set輸出到文件中:Iterator<Integer> iterator = set.iterator();while(iterator.hasNext()){writer.write(iterator.next() + "\n");}set.clear();}}}catch(Exception e){e.printStackTrace();}long t2 = System.currentTimeMillis();System.out.println("合并完成!耗時:" + (t2 - t1) + "毫秒。");return file.getAbsolutePath();}/*** 從pos開始,獲取count個數* @param pos* @param count* @return*/public Integer[] limit(int pos, int count, String in){// TODO : 從排序后的文件中讀取數據即可!不寫了!return null;}// 測試:public static void main(String[] args) {// 數據量:int dataCount = 100000000;// 分頁數(拆分文件數):int pageCount = 10;// 每頁數據量:int perPageCount = dataCount / pageCount;// 生成一億數據:DataProducer.produce(dataCount, "data");DivideTreeSetHandler handler = new DivideTreeSetHandler();// 拆分排序:List<String> tmps = handler.divide("data", perPageCount);// 合并排序:String tmp = handler.combine(tmps);// 獲取數據:Integer[] limit = handler.limit(10, 10, tmp);}}調用DivideTreeSetHandler的divide()方法,指定數據文件、拆分的每頁放多少數據,將數據拆分。當然,拆分的時候就已經分別使用TreeSet排序了!
調用DivideTreeSetHandler的combine()方法,將拆分后的若干個文件進行合并,合并的過程中同樣也會排序!
最終,輸出一個完全排序了的文件。
經測試,一億數據量,拆分加合并共用時約3.6分鐘(包含各種IO操作的用時),可以接受。
到這里,核心問題解決了,剩余的就是對象排序了,把用戶、活躍度封裝成對象,用TreeSet將對象進行排序,對象實現compareTo,重寫hashcode、equals等等,就不再多說了。
當然,DivideTreeSetHandler的還有很多優化空間,比如,可以把拆分、合并用多線程來處理。這里就先不搞了,有空再說。
說明:
寫代碼時,并不知道這種排序算法已經有名字了(叫“歸并排序”),還想著為其命名呢~
實際上,是受到hadoop的map-reduce思想的啟發,想到用這個方法來處理。
思想都是想通的:一個人搞不了了,就要分而治之!
總結
以上是生活随笔為你收集整理的一亿条数据的排序处理的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 自由幻想系统不能提供服务器,系统指南-自
- 下一篇: iphone ios7.1.1完美越狱(