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萬,確實跑的很慢,但是最終確實全部插進去了。
以上是個人的一點小經歷,肯定還有很多不足之處,歡迎大家指正,多多交流。