關於海量數據插入Redis

Redis相信大家都不陌生,作爲一個緩存數據庫,對於讀取相對頻繁的數據用起來很方便。關於Redis的詳細介紹,請大家自行百度,廢話不多說,進入我們的正題。

少量數據存入Redis並不難,瞬間就能搞定,關鍵是數據量多了呢?上百萬,千萬,甚至上億的數據量呢?我測的是1000萬條,字段10個,其中包括中文。

首先,我們要保證Redis的容量能存下我們的數據,這個在Redis的配置文件中可以設置,這裏就不多說了。接下來就是各種往裏面放數據了。

一、傳統方法,直接從mysql中查詢,遍歷放入Redis

這種方法是最容易想到的,也是最直接的,當然,我最先使用的也是這種方法。關於數據庫查詢的類這裏就不寫了,以下是Redis鏈接及插入Redis部分。

public class ZjhddToRedis {
    private Logger logger = LoggerFactory.getLogger(ZjhddToRedis.class);

    public void getHddAdInfoData(){

        ServiceUtil<HddAdInfoService> service = new ServiceUtil<>();
        List<Map<String, Object>> AdInfoDataList = new ArrayList<>();

        //本地redis
       Jedis jedis = new Jedis("127.0.0.1",6379); // 默認端口
       jedis.auth("123456");// 指定密碼
        System.out.println("Connection to server sucessfully");
        logger.info("redis服務器連接成功");

        AdInfoDataList=service.getService(HddAdInfoService.class).getDatasList();
        
        for (int i=0;i<AdInfoDataList.size();i++){
            System.out.println(i+"-----"+AdInfoDataList.get(i));            jedis.set(AdInfoDataList.get(i).get("encode_ad").toString(),parseMapToJsonObjectStr(AdInfoDataList.get(i)));
        }
        jedis.close();
}        

    //把Map<String, Object>的字符串轉換成JsonObject
    public static String parseMapToJsonObjectStr(Map<String, Object> map) {
        String result = null;
        if(map != null && map.keySet().size() != 0) {
            Set<String> set = map.keySet();
            JSONObject jsonObject = new JSONObject();
            Object value = null;
            for(String key : set) {
                value = map.get(key);
                if(value != null) {
                    try {
                        jsonObject.put(key, value.toString());
                    } catch (JSONException e) {
                        e.printStackTrace();
                    }
                }
            }
            if(jsonObject.size() != 0) {
                result = jsonObject.toString();
            }
        }
        return result;
    }

}

是不是很簡單,不要高興的太早,數據量少的話這樣放一放還是可以的,一旦數據量達到千萬級別,就出問題嘍!

問題一:一千萬數據,一次性從mysql查出來,先不考慮時間多少,單是放在list裏面,就需要佔用很大java內存,也就是由-XMS和-Xmx這兩個參數控制的,除非你內存夠大,否則就會報out of memory異常。如果內存滿足,或許這種方法可行(並沒有實測)。

只可惜我的內存不夠,也懶得去加,還是換一種方法吧。

既然一次性查詢壓力太大,爲什麼不批量查詢呢?百度一下,可以用limit限制查詢量。改吧!

public class ZjhddToRedis {
    private Logger logger = LoggerFactory.getLogger(ZjhddToRedis.class);

    public void getHddAdInfoData(){

        ServiceUtil<HddAdInfoService> service = new ServiceUtil<>();
        List<Map<String, Object>> AdInfoDataList = new ArrayList<>();

        Jedis jedis = new Jedis();
        jedis.auth("123456");
        System.out.println("成功地連接到服務器!");

int count=service.getService(HddAdInfoService.class).getDataCount();
        int size=10000;
        int pageSize = count%size==0?count/size:count/size+1;
        logger.info("寬帶信息總量count:"+count);
        System.out.println("count:"+count);
       logger.info("分批次總數:"+pageSize);
        System.out.println("pageSize:"+pageSize);
        int limit_b;
        int limit_e;
        logger.info("數據開始存入redis");
    for (int j=0;j<=pageSize;j++){
        limit_b=j*size;
        limit_e=(j+1)*size;
        logger.info("limit_b:"+limit_b+"------limit_e:"+limit_e);
        System.out.println("limit_b:"+limit_b+"------limit_e:"+limit_e);       AdInfoDataList=service.getService(HddAdInfoService.class).getDatasList(limit_b,limit_e);
        for (int i=0;i<AdInfoDataList.size();i++){
           System.out.println(i+"-----"+AdInfoDataList.get(i));            jedis.set(AdInfoDataList.get(i).get("encode_ad").toString(),parseMapToJsonObjectStr(AdInfoDataList.get(i)));
            
        }
        jedis.close();
    }
        logger.info("數據存入redis完成");
//把Map<String, Object>的字符串轉換成JsonObject
    public static String parseMapToJsonObjectStr(Map<String, Object> map) {
        String result = null;
        if(map != null && map.keySet().size() != 0) {
            Set<String> set = map.keySet();
            JSONObject jsonObject = new JSONObject();
            Object value = null;
            for(String key : set) {
                value = map.get(key);
                if(value != null) {
                    try {
                        jsonObject.put(key, value.toString());
                    } catch (JSONException e) {
                        e.printStackTrace();
                    }
                }
            }
            if(jsonObject.size() != 0) {
                result = jsonObject.toString();
            }
        }
        return result;
    }

}

不要忘記改數據查詢類哦!加了limit限制條件的。

按理說這樣不應該有問題了,大不了時間久一點,但是,事事多磨難啊!又有問題嘍!

問題二:數據庫連接超時,communications link failure

MySQL服務器默認的“wait_timeout”是28800秒即8小時,意味着如果一個連接的空閒時間超過8個小時,MySQL將自動斷開該連接,而連接池卻認爲該連接還是有效的(因爲並未校驗連接的有效性),當應用申請使用該連接時,就會導致上面的報錯。 
修改MySQL的參數,wait_timeout最大爲31536000即1年,在my.cnf中加入: 
[mysqld] 
wait_timeout=31536000 
interactive_timeout=31536000 
重啓生效,需要同時修改這兩個參數。

由於我們的分批查詢並插入Redis耗時太久,導致連接超時。並且衍生出另一個問題:limit的查詢機制。

mysql數據庫的所謂分頁(limit),設置開始索引及步長,其實每次查詢都會從頭開始進行檢索。所以即便你只要的是450萬到460萬之間的10萬條數據,但數據庫還是要將前面的450萬條數據檢索以便,所以會越來越慢,越來越慢。當即果斷停止導入,另尋他法。

二、java讀取數據文件導入

既然查詢大批量數據對數據庫有壓力,那我們就不從數據庫中取,直接讀取數據文件總可以吧。開搞!!!

首先,如果使用java導出數據文件的話,同樣需要查詢數據庫,這樣又會回到問題二,產生鏈接超時問題,當然這個是指個人推測,並未實際測試,有興趣的可以試一下。這裏我選擇用Navicat的導出功能,1000萬數據大概8分鐘就導出來了,導出的時候還要注意一點:字段分隔符根據需要自定,文本限定符建議選擇一個,因爲如果一條數據有字段空值的情況,導出來的文件會有字段缺失,有文本限定符的話就會給出一個佔位符,帶來的問題就是我們用字段值的時候要將首尾的限定符截掉。

導出的數據文件是這樣的:

有了文件,接下來就可以讀取導入了。

public class ReadFileToRedis {
    private Logger logger = LoggerFactory.getLogger(ReadFileToRedis.class);
    public void readHddDataFile() {
        String path = "";
        path = "D://waihuerror//";
        File files = new File(path); // 讀取的路徑下所有文件
        if (!files.exists()) {
            logger.info(path + " not exists");
            return;
        }
        File dataFiles[] = files.listFiles();
        for (int i = 0; i < dataFiles.length; i++) {
            File dataFile = dataFiles[i];

      if (!dataFile.isDirectory() && dataFile.getName().matches("hdd_ad_info.txt")) {
             String fileEncode = EncodingDetect.getJavaEncode(dataFile.getPath());
             BufferedReader bReader = null;
            try {
                bReader = new BufferedReader(new InputStreamReader(new FileInputStream(dataFile), fileEncode));
                    String line = "";
                    int count = 0;
                    while ((line = bReader.readLine()) != null) {
                        List<Map<String, Object>> AdInfoDataList = new ArrayList<>();
                        if (StringUtils.isNotBlank(line.trim())) {
                            String[] datas = line.split(",");
                            Map<String,Object> map  = new HashMap();   
//這裏就是前面多說的用substring截掉首尾的",因爲我要以json的形式塞到redis中,便於從redis中取具體字段,所以用到了map。   
                    map.put("encode_ad",datas[0].substring(1,datas[0].length()-1));                            
                    map.put("link_man_name",datas[1].substring(1,datas[1].length()-1));                            
                    map.put("link_phone_nbr",datas[2].substring(1,datas[2].length()-1));                            
                    map.put("addr_bunch_name",datas[3].substring(1,datas[3].length()-1));                            
                    map.put("addr_name",datas[4].substring(1,datas[4].length()-1));
                    map.put("id_nbr",datas[5].substring(1,datas[5].length()-1));
                    map.put("jindu",datas[6].substring(1,datas[6].length()-1));
                    map.put("weidu",datas[7].substring(1,datas[7].length()-1));
                    map.put("fj_area_id",datas[8].substring(1,datas[8].length()-1));
                    map.put("fj_area_name",datas[9].substring(1,datas[9].length()-1));
                    AdInfoDataList.add(map);
                        }
                        Jedis jedis = new Jedis("127.0.0.1",6379); // 默認端口
                        jedis.auth("123456");// 指定密碼
                        logger.info("redis服務器連接成功");
                        for (int j=0;j<AdInfoDataList.size();j++){
                            jedis.set(AdInfoDataList.get(j).get("encode_ad").toString(),parseMapToJsonObjectStr(AdInfoDataList.get(j)));
                            logger.info("記錄"+j+"-------加載成功!");
                        }
                        logger.info("寬帶數據加載完成!");
                        AdInfoDataList.clear();
                    }
                    
                    bReader.close();
                } catch (FileNotFoundException e) {
                    e.printStackTrace();
                } catch (IOException e) {
                    e.printStackTrace();
                } finally {
                    if (bReader != null) {
                        try {
                            bReader.close();
                        } catch (IOException e) {
                            e.printStackTrace();
                        }
                    }
                }
            }
        }
    }
    //把Map<String, Object>的字符串轉換成JsonObject
    public static String parseMapToJsonObjectStr(Map<String, Object> map) {
        String result = null;
        if(map != null && map.keySet().size() != 0) {
            Set<String> set = map.keySet();
            JSONObject jsonObject = new JSONObject();
            Object value = null;
            for(String key : set) {
                value = map.get(key);
                if(value != null) {
                    try {
                        jsonObject.put(key, value.toString());
                    } catch (JSONException e) {
                        e.printStackTrace();
                    }
                }
            }
            if(jsonObject.size() != 0) {
                result = jsonObject.toString();
            }
        }
        return result;
    }

}

搞定,跑起來!!!。。。。。。瞬間又慫了!!!報錯!!!內存溢出。。。當然會內存溢出啦,那麼多數據一下子讀到java內存,扛不住啊!!!沒辦法,繼續奔波吧。。。

再來一招,文件流,一條一條讀,low吧,我也覺得low,但是沒辦法啊,就想到這個,試試吧!

public class ReadFileOnebyone {
    private Logger logger = LoggerFactory.getLogger(ReadFileOnebyone.class);
    public void ReadDataFileOneByOne() throws IOException {
        String path = "D://waihuerror//hdd_ad_info.txt";
        File files = new File(path);
        if (!files.exists()) {
            logger.info(path + " not exists");
            return;
        }
        FileInputStream inputStream = null;
        Scanner sc = null;
        try {
            inputStream = new FileInputStream(path);
            sc = new Scanner(inputStream, "UTF-8");
            List<Map<String, Object>> AdInfoDataList = new ArrayList<>();
            Map<String,Object> map  = new HashMap();
            while (sc.hasNextLine()) {
                String line = sc.nextLine();
                if (StringUtils.isNotBlank(line.trim())) {
                    String[] datas = line.split(",");
//這裏就是前面多說的用substring截掉首尾的",因爲我要以json的形式塞到redis中,便於從redis中取具體字段,所以用到了map。  
                    map.put("encode_ad",datas[0].substring(1,datas[0].length()-1));
                    map.put("link_man_name",datas[1].substring(1,datas[1].length()-1));
                    map.put("link_phone_nbr",datas[2].substring(1,datas[2].length()-1));
                    map.put("addr_bunch_name",datas[3].substring(1,datas[3].length()-1));
                    map.put("addr_name",datas[4].substring(1,datas[4].length()-1));
                    map.put("id_nbr",datas[5].substring(1,datas[5].length()-1));
                    map.put("jindu",datas[6].substring(1,datas[6].length()-1));
                    map.put("weidu",datas[7].substring(1,datas[7].length()-1));
                    map.put("fj_area_id",datas[8].substring(1,datas[8].length()-1));
                    map.put("fj_area_name",datas[9].substring(1,datas[9].length()-1));
                    AdInfoDataList.add(map);
                }
                Jedis jedis = new Jedis("127.0.0.1",6379); // 默認端口
                jedis.auth("123456");// 指定密碼
                logger.info("成功地連接到服務器!");
                jedis.set(AdInfoDataList.get(0).get("encode_ad").toString(),parseMapToJsonObjectStr(AdInfoDataList.get(0)));
//用完後關閉連接,避免產生過多的客戶端連接
                jedis.close();
                logger.info("-------加載成功!");
//清空list和map,避免產生java內存堆積
                AdInfoDataList.clear();
                map.clear();
            }
            logger.info("寬帶數據加載完成!");
            if (sc.ioException() != null) {
                throw sc.ioException();
            }
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            if (inputStream != null) {
                inputStream.close();
            }
            if (sc != null) {
                sc.close();
            }
        }
    }

    //把Map<String, Object>的字符串轉換成JsonObject
    public static String parseMapToJsonObjectStr(Map<String, Object> map) {
        String result = null;
        if(map != null && map.keySet().size() != 0) {
            Set<String> set = map.keySet();
            JSONObject jsonObject = new JSONObject();
            Object value = null;
            for(String key : set) {
                value = map.get(key);
                if(value != null) {
                    try {
                        jsonObject.put(key, value.toString());
                    } catch (JSONException e) {
                        e.printStackTrace();
                    }
                }
            }
            if(jsonObject.size() != 0) {
                result = jsonObject.toString();
            }
        }
        return result;
    }

}

list和map用完後記得clear,redis創建連接後記得關閉,或許可以在開頭循環外創建一次連接,這個沒去深究,感興趣的可以試下。

好了,試一下,保險點,先試個300萬,確實跑的很慢,但是最終確實全部插進去了。

以上是個人的一點小經歷,肯定還有很多不足之處,歡迎大家指正,多多交流。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章