Redis Zset实现统计模块

1. 背景

公司有一个配置中心系统,使用MySQL存储了大量的配置,但现在不清楚哪些配置正在线上使用,哪些已经废弃了,所以需要实现一个统计模块,实现以下两个功能:

  1. 查看总体配置的数量以及活跃的数量
  2. 查看每一条配置的使用量

2. 分析

2.1 总体配置的数量

直接在MySQL中count即可得到

2.2 每一条配置的使用量

实现方式有很多,经过选择之后,选取了用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 基本结构

经典的三层结构

  1. 存储层,也就是DAO,主要使用RedisTemplate和Redis进行交互
  2. 服务层,也就是Service, 主要用来实现具体的业务
  3. 控制层,业绩是Controller, 主要用来通过HTTP接口收集数据和展示数据

3.2 DAO代码

  1. 覆盖收集数据,永久保存不过期,用来收集存储配置总数类似的数据
	/**
	 * 覆盖收集数据,永久保存
	 *
	 * @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 );
		} );
	}

  1. 按天存储,并保存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 );
		}
	}
  1. 查询单个数据
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 );
	}
  1. 查询时间聚合数据, 其中使用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代码

这里的代码是和业务相关的,因为不方便展示线上的代码,所以稍微调整了一下

  1. 收集和展示系统信息指标
    @PostConstruct
    public 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;
    }
  1. 统计一个月内某个配置的使用量
 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<>();
        }
    }
  1. 按天收集特定指标, 可以用于每条配置的使用量统计,也可以用做其他,例如,前端页面访问量统计
   public void collect(String key, Map<String, Integer> metricToCount) {

        statisticDAO.collectDaily(key, metricToCount);
    }

3.3 Controller层代码

主要是通过对Serivce代码的调用,对外层提供收集和展示服务,在这就不展示了,可以到文尾的源码中查看

4. 成果

  1. 收集好的数据在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 withscores
1) "MAX_MEM"
2) "-477102080"
3) "CPU_NUM"
4) "8"
5) "FREE_MEM"
6) "349881120"
7) "JVM_MEM"
8) "376963072"

  1. 前端的展示如图

5. 源码

Github 中的redisStatistic模块是此文章的源码

posted @ 2022-03-09 20:09  songtianer  阅读(718)  评论(0)    收藏  举报