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