redis 使用经验总结
①、定时更新天气网数据文件解析并存储到redis
②、通过接口访问形式提供给高德
1、定时更新天气网数据文件解析并存储到redis
- 下载GridForecast、GridHourly、GridNow、RainMinute四类数据文件 同时在服务器本地备份
date = TqwUtil.formatDate(TqwUtil.parse(date, dateFormat), dateFormat); //设置当前时间 RedisUtil.set("{" + TqwConstant.GRID_NOW_TIME + "}", date);
map.put("{" + TqwConstant.RedisKeyPrefix.GRID_NOW + lonlatString + "}", sb.toString()); //todo 以后删除,3*3 造出 1*1 的数据, 用中心点填充周围 BigDecimal lonBigdecimal = new BigDecimal(lon); BigDecimal latBigdecimal = new BigDecimal(lat); BigDecimal lonIncr = lonBigdecimal.add(new BigDecimal("0.01")); BigDecimal latIncr = latBigdecimal.add(new BigDecimal("0.01")); BigDecimal lonDecr = lonBigdecimal.subtract(new BigDecimal("0.01")); BigDecimal latDecr = latBigdecimal.subtract(new BigDecimal("0.01")); map.put("{" + TqwConstant.RedisKeyPrefix.GRID_NOW + TqwUtil.formatLonLatString(lon, latIncr.toString()) + "}", sb.toString()); map.put("{" + TqwConstant.RedisKeyPrefix.GRID_NOW + TqwUtil.formatLonLatString(lon, latDecr.toString()) + "}", sb.toString()); map.put("{" + TqwConstant.RedisKeyPrefix.GRID_NOW + TqwUtil.formatLonLatString(lonIncr.toString(), lat) + "}", sb.toString()); map.put("{" + TqwConstant.RedisKeyPrefix.GRID_NOW + TqwUtil.formatLonLatString(lonDecr.toString(), lat) + "}", sb.toString()); map.put("{" + TqwConstant.RedisKeyPrefix.GRID_NOW + TqwUtil.formatLonLatString(lonIncr.toString(), latIncr.toString()) + "}", sb.toString()); map.put("{" + TqwConstant.RedisKeyPrefix.GRID_NOW + TqwUtil.formatLonLatString(lonIncr.toString(), latDecr.toString()) + "}", sb.toString()); map.put("{" + TqwConstant.RedisKeyPrefix.GRID_NOW + TqwUtil.formatLonLatString(lonDecr.toString(), latIncr.toString()) + "}", sb.toString()); map.put("{" + TqwConstant.RedisKeyPrefix.GRID_NOW + TqwUtil.formatLonLatString(lonDecr.toString(), latDecr.toString()) + "}", sb.toString()); if ( map.size() >= DataUtil.object2Int(Constant.PROPERTIES.get("pipeline.count")) ) { RedisUtil.setByPipeline(map); map.clear(); }通过Redis String数据类型存储,每次存储覆盖上次数据,四类数据均采用此数据类型。尝试使用Hash,但是由于Hash内部存储数据大小有限制,存储信息超过一定大小,内存溢出,不能继续存储,虽然机器内容仍然够用。
- 将备份文件存储在缓存中,方便备份请求获取数据 空间换时间的概念
public File getGridNowFile(String[] arr) { logger.info(" grid now start back up !"); String filename = TqwConstant.BASE_PATH + File.separator + RegexUtil.getRandomString(10) + ".txt"; //打包文件 数组 try { String dataFormat = "yyyyMMddHHmm"; BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(new FileOutputStream(filename))); String gridNowStart = RedisUtil.get("{" + TqwConstant.GRID_NOW_TIME + "}"); DateTime dateTime = new DateTime(TqwUtil.parse(gridNowStart, dataFormat)); bw.write(dateTime.toString("yyyyMMddHH") + "00"); bw.newLine(); List<String> keys = new ArrayList<>(); for ( int i = 0; i < arr.length; i++ ) { String[] lonlat = arr[i].split(","); String lonlatStr = TqwUtil.formatLonLatString(lonlat[0], lonlat[1]); keys.add("{" + TqwConstant.RedisKeyPrefix.GRID_NOW + lonlatStr + "}"); if ( keys.size() >= 10000 ) { List<String> values = RedisUtil.getByPipeline(keys, String.class); for ( int j = 0; j < values.size(); j++ ) { String value = values.get(j); if ( TqwUtil.isEmpty(value) ) { bw.write("######################################"); } else { bw.write(value); } bw.newLine(); } keys.clear(); } } if ( keys.size() > 0 ) { List<String> values = RedisUtil.getByPipeline(keys, String.class); for ( int j = 0; j < values.size(); j++ ) { String value = values.get(j); if ( TqwUtil.isEmpty(value) ) { bw.write("######################################"); } else { bw.write(value); } bw.newLine(); } } IOUtils.closeQuietly(bw); } catch ( Exception e ) { logger.error(" 生成格点实况数据异常 ", e); FileUtils.deleteQuietly(new File(filename)); return null; } logger.info(" grid now end back up "); String zipFilename = TqwConstant.BASE_PATH + File.separator + RegexUtil.getRandomString(10) + ".zip"; try { ZipUtil.zip(zipFilename, "", filename); logger.info(" grid now end package "); } catch ( Exception e ) { logger.error("Grid Now zip error!", e); } finally { FileUtils.deleteQuietly(new File(filename)); } return new File(zipFilename); }
File zipFile = this.backFileGenerator.getGridNowFile(array); try { ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(); FileUtils.copyFile(zipFile, byteArrayOutputStream); TqwRedisUtil.set("{" + TqwConstant.FileBytesKey.NOW_BYTES + "}", byteArrayOutputStream.toByteArray()); IOUtils.closeQuietly(byteArrayOutputStream);
2.高德请求数据
根据经纬度数组返回经纬度对应的数据
public File getGridNowFile(String[] arr) { logger.info(" grid now start generate "); String filename = TqwConstant.BASE_PATH + "/grid-now-" + RegexUtil.getRandomString(10) + ".txt"; File file = new File(filename); try { String dataFormat = "yyyyMMddHHmm"; BufferedWriter br = new BufferedWriter(new OutputStreamWriter(new FileOutputStream(file))); String gridNowStart = RedisUtil.get("{" + TqwConstant.GRID_NOW_TIME + "}"); DateTime dateTime = new DateTime(TqwUtil.parse(gridNowStart, dataFormat)); br.write(dateTime.toString("yyyyMMddHH") + "00"); br.newLine(); List<String> requestKeys = new ArrayList<>(); List<String> keys = new ArrayList<>(); for ( int i = 0; i < arr.length; i++ ) { String[] lonlat = arr[i].split(","); boolean isValid = TqwUtil.isValid(lonlat[0], lonlat[1]); if ( !isValid ) { continue; } double[] lonLatArray = CoordinateUtil.gcj02ToBd09(Double.valueOf(lonlat[0]), Double.valueOf(lonlat[1])); String lonlatStr = TqwUtil.formatLonLatString(String.valueOf(lonLatArray[0]), String.valueOf(lonLatArray[1])); keys.add("{" + TqwConstant.RedisKeyPrefix.GRID_NOW + lonlatStr + "}"); requestKeys.add(arr[i] + "_"); if ( requestKeys.size() >= 10000 ) { List<String> values = RedisUtil.getByPipeline(keys, String.class); for ( int j = 0; j < values.size(); j++ ) { br.write(requestKeys.get(j)); String value = values.get(j); if ( TqwUtil.isEmpty(value) ) { br.write("#########################"); } else { br.write(value.substring(0, 25)); } br.newLine(); } keys.clear(); requestKeys.clear(); } } if ( requestKeys.size() > 0 ) { List<String> values = RedisUtil.getByPipeline(keys, String.class); for ( int j = 0; j < values.size(); j++ ) { br.write(requestKeys.get(j)); String value = values.get(j); if ( TqwUtil.isEmpty(value) ) { br.write("#########################"); } else { br.write(value.substring(0, 25)); } br.newLine(); } } IOUtils.closeQuietly(br); } catch ( Exception e ) { logger.error(" 生成格点实况数据异常 ", e); FileUtils.deleteQuietly(file); return null; } logger.info(" grid now end generate "); String zipFilename = TqwConstant.BASE_PATH + "/grid-now-" + RegexUtil.getRandomString(10) + ".zip"; try { ZipUtil.zip(zipFilename, "", filename); logger.info(" grid now end package "); } catch ( Exception e ) { logger.error("Grid Now zip error!", e); } finally { FileUtils.deleteQuietly(file); } return new File(zipFilename); }
总结:
1、key采用{key}yyyyMMdd 数据格式 目的:在集群环境中,只根据{}中的值计算slot对应的槽值,使同一个分类的key落在相同的slot中,且均匀分布
2、使用批量操作提高效率时需要注意 原生命令:例如mget、mset。非原生命令:可以使用pipeline提高效率。原生是原子操作,pipeline是非原子操作。
3、淘汰策略 在Redis中设置过期时间是35天,当有访问并命中时,对key进行续命,延长过期时间,未在35天出现的自然淘汰。(这个会在下次系统缓存使用时用到)。
4、用Bucket的概念将key均匀的分散在hash中(get(key1) -> hget(md5(key1), key1) 从而得到value),让很多key可以在BucketId空间里碰撞,那么可以认为一个BucketId下面挂了多个key。比如平均每个BucketId下面挂10个key,那么理论上我们将会减少超过90%的redis key的个数。
一千万的数据使用String类型存储,需要一千万个key,我们可以使用一种算法只会产生十万个数据(key%100000 便会产生100000个Bucket,每个Bucket中存储100个key,达到节约内存效果)
使用Bucket散列时注意事项:
1)kv存储的量级必须事先规划好,浮动的范围大概在桶个数的十到十五倍,比如我就想存储百亿左右的kv,那么最好选择30bit~31bit作为桶的个数。也就是说业务增长在一个合理的范围(10~15倍的增长)是没问题的,如果业务太多倍数的增长,会导致hashset增长过快导致查询时间增加,甚至触发zip-list阈值,导致内存急剧上升。
2)适合短小value,如果value太大或字段太多并不适合,因为这种方式必须要求把value一次性取出,比如人口标签是非常小的编码,甚至只需要3、4个bit(位)就能装下。
3)典型的时间换空间的做法,由于我们的业务场景并不是要求在极高的qps之下,一般每天亿到十亿级别的量,所以合理利用CPU租值,也是十分经济的。
4)由于使用了信息摘要降低了key的大小以及约定长度,所以无法从redis里面random出key。如果需要导出,必须在冷数据中导出。
5)expire需要自己实现,目前的算法很简单,由于只有在写操作时才会增加消耗,所以在写操作时按照一定的比例抽样,用HLEN命中判断是否超过15个entry,超过才将过期的key删除,TTL的时间戳存储在value的前32bit中。
6)桶的消耗统计是需要做的。需要定期清理过期的key,保证redis的查询不会变慢。
2)适合短小value,如果value太大或字段太多并不适合,因为这种方式必须要求把value一次性取出,比如人口标签是非常小的编码,甚至只需要3、4个bit(位)就能装下。
3)典型的时间换空间的做法,由于我们的业务场景并不是要求在极高的qps之下,一般每天亿到十亿级别的量,所以合理利用CPU租值,也是十分经济的。
4)由于使用了信息摘要降低了key的大小以及约定长度,所以无法从redis里面random出key。如果需要导出,必须在冷数据中导出。
5)expire需要自己实现,目前的算法很简单,由于只有在写操作时才会增加消耗,所以在写操作时按照一定的比例抽样,用HLEN命中判断是否超过15个entry,超过才将过期的key删除,TTL的时间戳存储在value的前32bit中。
6)桶的消耗统计是需要做的。需要定期清理过期的key,保证redis的查询不会变慢。
5、减少碎片 碎片主要原因在于内存无法对齐、过期删除后,内存无法重新分配。尽量用相同长度key、value表示,这样可以保证内存对齐
参考链接 :http://www.aboutyun.com/thread-19603-1-1.html