使用RedisZset实现简单的统计计数模块1. 背景公司有一个配置中心系统 , 使用MySQL存储了大量的配置 , 但现在不清楚哪些配置正在线上使用 , 哪些已经废弃了 , 所以需要实现一个统计模块 , 实现以下两个功能:
- 查看总体配置的数量以及活跃的数量
- 查看每一条配置的使用量
2.2 每一条配置的使用量【Redis Zset实现统计模块】实现方式有很多 , 经过选择之后 , 选取了用Redis的Zset来实现
2.2.1 HashMap使用HashMap, 当获取到配置的使用 , 那配置的key获取value加1即可
可能存在的问题 , 并发问题 , 集群中每个节点的数据怎么聚合
2.2.2 MySQL增加字段在MySQL的配置表中增加一个使用次数字段 , 当每次获取配置时 , 更新此字段
可能存在的问题 , 性能问题 , 频繁更新必然会影响MySQL的性能 , 这个功能相比提供配置来说 , 算作是一个辅助的、可有可无的功能 , 不能影响主要的业务
2.2.3 Redis存储Redis存储性能比较高 , 可以使用string的INCR或者Zset的INCR命令对执行ID的配置进行计数 , 我选择了Zset, 原因是查询的时候方便聚合
3. 代码以下代码是从线上代码中排除业务相关代码的示例
3.1 基本结构经典的三层结构
- 存储层 , 也就是DAO , 主要使用RedisTemplate和Redis进行交互
- 服务层 , 也就是Service, 主要用来实现具体的业务
- 控制层 , 业绩是Controller, 主要用来通过HTTP接口收集数据和展示数据
- 覆盖收集数据 , 永久保存不过期 , 用来收集存储配置总数类似的数据
/*** 覆盖收集数据 , 永久保存** @param key数据分类(类似MySQL表)* @param metricToCount 指标-数量*/ public void collect( String key, Map<String, Integer> metricToCount ){key = makeKey( key );String finalKey = key;metricToCount.forEach( ( oneMetric, value ) -> {redisTemplate.opsForZSet().add( finalKey, oneMetric, value );} ); }- 按天存储 , 并保存30天 , 用来收集每条配置用量的数据
/*** 按天增量收集数据 , 保存30天** @param key数据分类(类似MySQL表)* @param metricToCount 指标-数量*/ public void collectDaily( String key, Map<String, Integer> metricToCount ){key = makeDailyKey( key );String finalKey = key;metricToCount.forEach( ( oneMetric, value ) -> {redisTemplate.opsForZSet().incrementScore( finalKey, oneMetric, value );} );Long expire = redisTemplate.getExpire( finalKey );if( expire != null && expire == -1 ){redisTemplate.expire( finalKey, 30, TimeUnit.DAYS );} }- 查询单个数据
private Map<String, Integer> queryDirectly( String key ){Map<String, Integer> rs = new HashMap<>();Set<ZSetOperations.TypedTuple<String>> mertricToCountTuple = redisTemplate.opsForZSet().rangeWithScores( key, 0, -1 );if( mertricToCountTuple != null ){for( ZSetOperations.TypedTuple<String> oneMetricCount : mertricToCountTuple ){if( oneMetricCount.getScore() != null ){rs.put( oneMetricCount.getValue(), oneMetricCount.getScore().intValue() );}}}return rs; } /*** 根据数据分类查询数据** @param key 数据分类* @return 指标-数量*/ public Map<String, Integer> query( String key ){key = this.makeKey( key );return queryDirectly( key ); }- 查询时间聚合数据, 其中使用Redis管道操作来提高性能
/*** 根据数据分类和指定时间段查询数据** @param key数据分类* @param start 开始时间* @param end结束时间* @return 指标-数量*/ public Map<String, Map<String, Integer>> queryTimeRange( String key, LocalDate start, LocalDate end ){Map<String, Map<String, Integer>> rs = new HashMap<>();List<LocalDate> keys = new ArrayList<>();List<Object> tupleSets = redisTemplate.executePipelined( ( RedisCallback<Object> )redisConnection -> {redisConnection.openPipeline();LocalDate dayInRange = start;for( ; dayInRange.isBefore( end ); dayInRange = dayInRange.plusDays( 1 ) ){String dayKey = makeDailyKey( key, dayInRange );keys.add( dayInRange );redisConnection.zRangeWithScores( dayKey.getBytes( StandardCharsets.UTF_8 ), 0, -1 );}return null;} );for( int i = 0; i < keys.size(); i++ ){@SuppressWarnings( "unchecked" )Set<DefaultTypedTuple<String>> tupleSet = ( Set<DefaultTypedTuple<String>> )tupleSets.get( i );Map<String, Integer> metricToCount = new HashMap<>();for( DefaultTypedTuple<String> tuple : tupleSet ){if( tuple.getScore() != null ){metricToCount.put( tuple.getValue(), tuple.getScore().intValue() );}}rs.put( keys.get( i ).toString(), metricToCount );}return rs; }3.3 Service代码这里的代码是和业务相关的 , 因为不方便展示线上的代码 , 所以稍微调整了一下- 收集和展示系统信息指标
@PostConstructpublic void collectEveryConfigNum() {Map<String, Integer> metricToCount = new HashMap<>();metricToCount.put(MetricKey.CPU_NUM.name(), Runtime.getRuntime().availableProcessors());metricToCount.put(MetricKey.FREE_MEM.name(), (int) Runtime.getRuntime().freeMemory());metricToCount.put(MetricKey.MAX_MEM.name(), (int) Runtime.getRuntime().maxMemory());metricToCount.put(MetricKey.JVM_MEM.name(), (int) Runtime.getRuntime().totalMemory());statisticDAO.collect(StatKey.SYSTEM_INFO.name(), metricToCount);}public List<ConfigStat> configStat() {List<ConfigStat> rs = new ArrayList<>();Map<String, Integer> typeToTotalNum = statisticDAO.query(StatKey.SYSTEM_INFO.name());for (String type : typeToTotalNum.keySet()) {ConfigStat configStat = new ConfigStat();configStat.setType(type);configStat.setNum(typeToTotalNum.get(type));rs.add(configStat);}return rs;}- 统计一个月内某个配置的使用量
public Map<String, Integer> lastMonthUseCount(String key) {try {Map<String, Integer> rs = new HashMap<>();LocalDate now = LocalDate.now();LocalDate lastMonthDate = now.minusDays(29);LocalDate endDate = now.plusDays(1);Map<String, Map<String, Integer>> dateToUseCount = statisticDAO.queryTimeRange(key, lastMonthDate, endDate);for (Map<String, Integer> metricToCount : dateToUseCount.values()) {for (Map.Entry<String, Integer> entry : metricToCount.entrySet()) {rs.merge(entry.getKey(), entry.getValue(), Integer::sum);}}return rs;} catch (Exception e) {LOGGER.error("StatisticManager lastMonthUseCount error", e);return new HashMap<>();}}- 按天收集特定指标, 可以用于每条配置的使用量统计 , 也可以用做其他 , 例如 , 前端页面访问量统计
public void collect(String key, Map<String, Integer> metricToCount) {statisticDAO.collectDaily(key, metricToCount);}3.3 Controller层代码主要是通过对Serivce代码的调用 , 对外层提供收集和展示服务 , 在这就不展示了 , 可以到文尾的源码中查看4. 成果
- 收集好的数据在Redis中是这样存储的
127.0.0.1:6379> keys *1) "CC_STATISTIC:2022-03-08:API"2) "CC_STATISTIC:SYSTEM_INFO"127.0.0.1:6379> zrange CC_STATISTIC:SYSTEM_INFO 0 -1 withscores1) "MAX_MEM"2) "-477102080"3) "CPU_NUM"4) "8"5) "FREE_MEM"6) "349881120"7) "JVM_MEM"8) "376963072"- 前端的展示如图

文章插图

文章插图
5. 源码Github 中的redisStatistic模块是此文章的源码
- 春季老年人吃什么养肝?土豆、米饭换着吃
- 三八妇女节节日祝福分享 三八妇女节节日语录
- 老人谨慎!选好你的“第三只脚”
- 校方进行了深刻的反思 青岛一大学生坠亡校方整改校规
- 脸皮厚的人长寿!有这特征的老人最长寿
- 长寿秘诀:记住这10大妙招 100%增寿
- 春季老年人心血管病高发 3条保命要诀
- 眼睛花不花要看四十八 老年人怎样延缓老花眼
- 香槟然能防治老年痴呆症? 一天三杯它人到90不痴呆
- 老人手抖的原因 为什么老人手会抖
