Redis RedisTimeSeries 在springboot中的应用

场景

我们现在有这样一个场景,使用国产的Gbase数据库,有个分钟表的数据,大概有20个字段,存储的的站点大概是4000多个,大概算下数据量

每小时:4000*60=240000

每天:40000* 60 * 24=5760000

但是我们实际上用的最频繁的是时间字段和一个要素字段,时间字段已经加了索引,要素字段数据是上来的监测数据。对此字段的请求频率是最高的,目前响应时间过长,严重影响到用户体验感 ,现需要进行对其优化,考虑到库的维护由用户负责,我们只有查询权限。提出如下几种方式

1、分库分表

2、国产时序库TDengine进行替代

3、历史数据和最新数据分开存储

4、使用Redis缓存

5、大数据方案

考虑到用户单位的特殊性,第一要满足国产要求,第二是数据库是指定专用,第三数据库维护不在我们这里。最终决定使用Redis的TS来解决。

主要实现逻辑:redis中缓存最近3小时内的所有有效数据,注意是有效数据,比如某个站点的数据缺测或者是一直没数据,则不进行缓存,其实很多时候都是没数据的,除非到雨季,这样下来redis缓存的数据会少很多。

数据由用户同步到Redis服务,我们进行查询,所以本文围绕使用来写

如果您有更好的解决方案,不妨交流一下,非常感谢您指导。

Redis-TS

了解

RedisTimeSeries 是一个 Redis 模块,用于存储、查询和管理时间序列数据。与传统的时间序列数据库相比,它的优势在于极低的延迟和与 Redis 生态的无缝集成。

核心概念:

  • 时间序列:一个由时间戳和值组成的数据点序列。
  • 标签 :可以为每个时间序列设置多个标签(如 station_id=52889, element=V12001),从而实现高效的筛选和分组查询。

1、本地安装

这里我们安装最新版的,默认自带了TS模块,推荐使用Linux环境,你会省去很多麻烦

xml 复制代码
 docker run -d \
  --privileged=true \
  -p 6380:6379 \
  --restart always \
  -v /data/redis/redis.conf:/etc/redis/redis.conf \
  -v /data/redis/data:/data \
  --name myredis \
  redis \
  redis-server /etc/redis/redis.conf --appendonly yes

可以把用户侧数据的rdb文件直接拷贝过来,数据就到本地了

2、使用

我用的springboot框架为2.7.18,网上找了一圈,没找到比较好用的关于TS封装的组件

官网上有详细的命令使用方式,这里不在赘述 redis.ac.cn/docs/latest...

本次使用到几个命令

1、获取最新数据 (TS.MGET)

2、正常查询某个时间段数据 (TS.MRANGE)

3、管道方式查询(TS.RANGE)

管道和lua脚本的区别对比,鉴于对比,所以选择了管道方式执行

特性 管道 Lua 脚本
核心目标 提升吞吐量,减少网络延迟 保证原子性,实现复杂逻辑
原子性 不保证。管道中的命令可能会被其他客户端的命令插入。 保证。脚本在执行期间,Redis服务器不会处理其他命令,相当于"数据库事务"。
逻辑能力 无。只是一批命令的简单打包,命令之间无法相互影响。 强大。可以使用变量、条件判断、循环等编程结构,前一个命令的结果可以直接用于下一个命令。
性能优势 极高。将多个网络往返压缩为一次,极大提升批量操作的吞吐量。 。虽然也需要一次网络往返,但避免了多次网络延迟。其优势更多体现在原子性上,而非纯粹的吞吐量。
错误处理 管道中某个命令失败,不会影响其他命令的执行。 脚本中如果出现错误,整个脚本都会回滚,所有修改都不会生效。
使用场景 - 批量写入数据 - 批量读取不相关的数据 - 更新大量计数器 - 需要原子性的操作(如:秒杀扣库存、转账) - 前后命令有依赖(如:先检查再设置) - 实现复杂的业务逻辑(如:计算滑动窗口平均值)

4、数据统计

单站点,单要素使用:TS.RANGE,多站点多要素使用TS.MRANGE

5、窗口统计(TS.MRANGE)

注意:此版本的RedisTemplated的execute方法是没办法直接执行TS的命令的,我们需要使用Lettuce中的方法来执行

java 复制代码
package com.dfec.api.util;

import com.alibaba.fastjson2.JSON;
import com.dfec.api.constant.ApiConstant;
import com.dfec.api.entity.TsResult;
import com.dfec.api.entity.cache.StatParam;
import com.dfec.common.bean.constant.RetCode;
import com.dfec.common.bean.exception.OuterException;
import io.lettuce.core.RedisFuture;
import io.lettuce.core.cluster.api.async.RedisClusterAsyncCommands;
import io.lettuce.core.codec.ByteArrayCodec;
import io.lettuce.core.codec.StringCodec;
import io.lettuce.core.internal.Exceptions;
import io.lettuce.core.output.ObjectOutput;
import io.lettuce.core.protocol.CommandArgs;
import io.lettuce.core.protocol.ProtocolKeyword;
import org.apache.commons.lang3.StringUtils;
import org.springframework.data.redis.connection.lettuce.LettuceConnection;
import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory;
import org.springframework.data.redis.core.RedisConnectionUtils;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.data.redis.core.script.RedisScript;
import org.springframework.data.redis.serializer.JdkSerializationRedisSerializer;
import org.springframework.data.redis.serializer.RedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;
import org.springframework.stereotype.Component;

import javax.annotation.Resource;
import java.nio.charset.StandardCharsets;
import java.util.*;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;

import static com.dfec.api.util.DateUtil.dateStrArrToLongArr;

/**
 * Redis TS操作工具类
 *
 * @author tangrg
 * @email 1446232546@qq.com
 * @date 2025-10-2025/10/13 09:39:46
 */
@Component
public class RedisUtil {

    LettuceConnection connection;

    @Resource
    private RedisTemplate<String, Object> redisTemplate;

    @Resource
    private LettuceConnectionFactory lettuceConnectionFactory;

    /**
     * 将 Redis TS.MRANGE 返回的 Map<String, List<Object>>(key=seriesName, value=List<[ts, val]>)
     * 转换为 Map<String, List<TsResult>>,适配你当前的 TsResult 类(含 key: LocalDateTime)
     *
     * @param rawData 原始返回数据,格式为 Map<String, List<Object>>,其中 value 是 List<[ts, val]>
     * @return Map<String, List<TsResult>>
     */
    private static Map<String, List<TsResult>> convertToTsResultMap(Map<String, List<Object>> rawData) {
        Map<String, List<TsResult>> result = new HashMap<>();

        if (rawData == null) {
            return result;
        }

        for (Map.Entry<String, List<Object>> entry : rawData.entrySet()) {
            String seriesKey = entry.getKey();  // 如 "pm:ts:52889:V12001"
            List<Object> dataPoints = entry.getValue();  // List<Object>,每个是 [ts, val]

            List<TsResult> tsResults = new ArrayList<>();

            for (Object dp : dataPoints) {
                if (!(dp instanceof List)) {
                    continue;
                }

                List<Object> point = (List<Object>) dp;
                if (point.size() == 2 && !(point.get(0) instanceof List)) {
                    dealResult(point, tsResults);
                } else {
                    try {
                        point.forEach(item1 -> {
                            if (item1 instanceof List) {
                                List item = (List) item1;
                                dealResult(item, tsResults);
                            }
                        });
                    } catch (Exception e) {
                        // 类型转换异常,跳过该点
                        continue;
                    }
                }
            }

            result.put(seriesKey, tsResults);
        }

        return result;
    }

    private static void dealResult(List<Object> point, List<TsResult> tsResults) {
        TsResult tsResult = new TsResult();
        tsResult.setTime((Long) point.get(0));
        tsResult.setKey(DateUtil.longToLocalDateTime((Long) point.get(0)));
        tsResult.setVal((Double) point.get(1));
        tsResults.add(tsResult);
    }

    private static boolean awaitAll(long timeout, TimeUnit unit, java.util.concurrent.Future<?>... futures) {
        try {
            long nanos = unit.toNanos(timeout);
            long time = System.nanoTime();

            for (java.util.concurrent.Future<?> f : futures) {
                if (timeout <= 0L) {
                    f.get();
                } else {
                    if (nanos < 0L) {
                        return false;
                    }
                    try {
                        f.get(nanos, TimeUnit.NANOSECONDS);
                    } catch (Exception e) {
//                        e.printStackTrace();
//                        System.out.println("结果值不存在");
                    }

                    long now = System.nanoTime();
                    nanos -= now - time;
                    time = now;
                }
            }

            return true;
        } catch (Exception e) {
            throw Exceptions.fromSynchronization(e);
        }
    }

    private static List<byte[]> buildArgsFilterMap(Map<String, Object> filterMap) {
        List<byte[]> args = new ArrayList<>();
        if(filterMap.containsKey(ApiConstant.FILTER_BY_VALUE)){
            args.add(ApiConstant.FILTER_BY_VALUE.getBytes());
            byte[] bytes = filterMap.get(ApiConstant.FILTER_BY_VALUE).toString().getBytes();
            args.add(bytes);
            filterMap.remove(ApiConstant.FILTER_BY_VALUE);
        }
        if (filterMap != null && !filterMap.isEmpty()) {
            args.add("FILTER".getBytes());
            filterMap.forEach((key, value) -> {
                String param = key + "=" + value;
                args.add(param.getBytes());
            });

        }
        return args;
    }

    private static long getBucketSizeMs(Long from, Long to, Integer windowSize, String dateType) {
        long bucketSizeMs = 0;
        if (windowSize == null) {
            bucketSizeMs = to - from;
        } else {
            bucketSizeMs = windowSize * getAdjTime(dateType);
        }
        return bucketSizeMs;
    }

    private static Long getAdjTime(String dateType) {
        switch (dateType) {
            case "MIN":
                return 60 * 1000L;
            case "HOUR":
                return 60 * 60 * 1000L;
            case "DAY":
                return 24 * 60 * 60 * 1000L;
            default:
                return 0L;
        }
    }

    /**
     * 查询某个时间范围内的时间序列数据
     *
     * @param key  时间序列的 key,比如 "mytimeseries"
     * @param from 开始时间戳(毫秒,比如 1712345600000)
     * @param to   结束时间戳(毫秒,比如 1712345720000)
     * @return 原始返回结果,是一个 List<Object>,每两个元素代表 [timestamp, value]
     */
    public List<TsResult> queryTimeRange(String key, long from, long to, Map<String, Object> filterMap,String dataCode) {
        byte[][] args = buildTsRangeArgs(key, from, to, filterMap);

        LettuceConnection connection1 = getLettuceConnection();
        ObjectOutput keyValueListOutput = new ObjectOutput(new StringCodec());
        List<List<Object>> execute = (List<List<Object>>) connection1.execute("TS.RANGE", keyValueListOutput, args);

        List<TsResult> tsResults = new ArrayList<>();
        execute.forEach(item -> {
            dealResult(item, tsResults);
        });
        return tsResults;

    }

    public Map<String, List<TsResult>> queryTimeRangeByPipe(List<String> keys, String timeRange,String redisEleRange,String dataCode) {
        Long[] dateArr = dateStrArrToLongArr(timeRange,dataCode);
        return queryTimeRangeByPipe(keys, dateArr[0], dateArr[1],redisEleRange);
    }

    /**
     * redis 的pipe
     * 存在的问题:如果其中有一个站点的值不存在,则会报错Caused by: io.lettuce.core.RedisCommandExecutionException: ERR TSDB: the key does not exist,通过重写的方式来解决
     * 
     *
     * @param keys
     * @param from
     * @param to
     * @return
     */
    public Map<String, List<TsResult>> queryTimeRangeByPipe(List<String> keys, long from, long to,String redisEleRange) {
        Map<String, List<TsResult>> output = new HashMap<>();

        LettuceConnection connection = getLettuceConnection();

        RedisClusterAsyncCommands<byte[], byte[]> commands = connection.getNativeConnection();
        try {

            // 3. 开启 Pipeline 模式(关闭自动 flush)
            commands.setAutoFlushCommands(false);


            List<RedisFuture<?>> futures = new ArrayList<>();


            // 单独保存 TS.RANGE 的 futures
            List<RedisFuture<List<Object>>> tsRangeFutures = new ArrayList<>();

            for (String tsKey : keys) {
                byte[] keyBytes = tsKey.getBytes(StandardCharsets.UTF_8);

                ProtocolKeyword tsRangeKeyword = new ProtocolKeyword() {
                    @Override
                    public byte[] getBytes() {
                        return "TS.RANGE".getBytes(StandardCharsets.UTF_8);
                    }

                    @Override
                    public String name() {
                        return "TS.RANGE";
                    }
                };

                CommandArgs<byte[], byte[]> args = new CommandArgs<>(new ByteArrayCodec()).addKey(keyBytes).add(from).add(to);

                if(StringUtils.isNotBlank(redisEleRange)){
                    args.add(ApiConstant.FILTER_BY_VALUE).add(redisEleRange);
                }

                ObjectOutput keyValueListOutput = new ObjectOutput(new StringCodec());

                @SuppressWarnings("unchecked") RedisFuture<List<Object>> tsFuture = (RedisFuture<List<Object>>) commands.dispatch(tsRangeKeyword, keyValueListOutput, args);

                futures.add(tsFuture);
                tsRangeFutures.add(tsFuture);
            }

            // 执行 pipeline
            commands.flushCommands();

            // 等待所有完成
            RedisFuture[] array = futures.toArray(new RedisFuture[0]);
            awaitAll(60, TimeUnit.SECONDS, array);

            // 解析 TS.RANGE 结果
            int tsIndex = 0;
            for (String tsKey : keys) {
                RedisFuture<List<Object>> tsFuture = tsRangeFutures.get(tsIndex);
                List<Object> rawTsData = null;
                try {
                    /**
                     * 阻塞获取结果
                     * 这里如果获取结果失败,则会抛出异常,直接执行失败,我们需要制造一个空集合给后面使用
                     */
                    rawTsData = tsFuture.get();
                } catch (InterruptedException | ExecutionException e) {
                    rawTsData = new ArrayList<>();
                }


//                System.out.println("TS.RANGE 结果 for key [" + tsKey + "]: " + rawTsData);

                List<TsResult> tsResults = new ArrayList<>();
                if (rawTsData != null) {

                    for (Object item : rawTsData) {
                        if (item instanceof List) {
                            List<?> tsEntry = (List<?>) item;
                            if (tsEntry.size() >= 2) {
                                TsResult tsResult = new TsResult();
                                Long timestamp = ((Number) tsEntry.get(0)).longValue();
                                Object value = tsEntry.get(1);
                                tsResult.setTime(timestamp);
                                tsResult.setVal((Double) value);
                                tsResult.setKey(DateUtil.longToLocalDateTime(timestamp));
                                tsResults.add(tsResult);
                            }
                        }
                    }
                }
                output.put(tsKey, tsResults);

                tsIndex++;
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            commands.setAutoFlushCommands(true);
            if (connection != null) {
                RedisConnectionUtils.releaseConnection(connection, lettuceConnectionFactory);
            }
        }

        return output;

    }

    private LettuceConnection getLettuceConnection() {
        if (connection != null) {
            return connection;
        }
        connection = (LettuceConnection) lettuceConnectionFactory.getConnection();
        return connection;
    }

    private void executeLua() {
        String luaScript = "local from = tonumber(ARGV[#ARGV - 1]) " + "local to = tonumber(ARGV[#ARGV]) " + "if not from or not to then return { err = 'from 和 to 必须是有效的时间戳(毫秒级数字,例如 1760895600000)' } end " + "local result = {} " + "for i = 1, #ARGV - 2 do " + "  local key = ARGV[i] " + "  local res = redis.call('TS.RANGE', key, from, to) " + "  local row = { key } " + "  for j = 1, #res do " + "    table.insert(row, res[j][1]) " + "    table.insert(row, res[j][2]) " + "  end " + "  table.insert(result, row) " + "end " + "return result";
        RedisScript<Object> objectRedisScript = new DefaultRedisScript<>(luaScript, Object.class);
        List<String> args1 = Arrays.asList("pm:ts:52980:V13011", "pm:ts:52981:V13011", "pm:ts:52983:V13011", "pm:ts:53906:V13011", "pm:ts:53908:V13011", "1761068400000",  // from(字符串,但会被 tonumbe() 转成数字)
                "1761270000000"   // to(字符串,但会被 tonumbe() 转成数字)
        );
        List<byte[]> argsList = Arrays.asList("pm:ts:52980:V13011".getBytes(), "pm:ts:52981:V13011".getBytes(), "pm:ts:52983:V13011".getBytes(), "pm:ts:53906:V13011".getBytes(), "pm:ts:53908:V13011".getBytes(), "1761068400000".getBytes(),  // from(字符串,但会被 tonumbe() 转成数字)
                "1761270000000".getBytes()   // to(字符串,但会被 tonumbe() 转成数字)
        );
        RedisSerializer stringRedisSerializer = new StringRedisSerializer();
        JdkSerializationRedisSerializer jdkSerializationRedisSerializer = new JdkSerializationRedisSerializer();

        Object execute1 = redisTemplate.execute(objectRedisScript, stringRedisSerializer, jdkSerializationRedisSerializer, new ArrayList<>(), args1.toArray());
    }

    private byte[][] buildTsRangeArgs(String key, long from, long to, Map<String, Object> filterMap) {
        List<byte[]> argsList = new ArrayList<>();
        argsList.add(key.getBytes());
        argsList.add(String.valueOf(from).getBytes());
        argsList.add(String.valueOf(to).getBytes());

        if (filterMap != null && !filterMap.isEmpty()) {
            argsList.add("FILTER".getBytes());
            String filterStr = filterMap.entrySet().stream().map(entry -> entry.getKey() + "=" + entry.getValue()).collect(Collectors.joining(" "));
            argsList.add(filterStr.getBytes());
        }

        return argsList.toArray(new byte[0][]);
    }

    public List<TsResult> queryTimeRange(String key, Long[] longs, Map<String, Object> filterMap,String dataCode) {
        return queryTimeRange(key, longs[0], longs[1], filterMap,dataCode);
    }

    /**
     * 查询某个时间范围内的时间序列数据
     *
     * @param key       时间序列的 key,比如 "mytimeseries"
     * @param timeRange 时间范围,格式为 "[开始时间, 结束时间]",如 "[20251020014000,20251023064000]"
     * @return 原始返回结果,是一个 List<Object>,每两个元素代表 [timestamp, value]
     */
    public List<TsResult> queryTimeRange(String key, String timeRange, Map<String, Object> filterMap,String dataCode) {
        Long[] longs = dateStrArrToLongArr(timeRange,dataCode);
        return queryTimeRange(key, longs[0], longs[1], filterMap,dataCode);
    }

    public Object get(String key) {
        return redisTemplate.opsForValue().get(key);
    }

    public <T> T getObject(String key, Class<T> clazz) {
        String value = (String) redisTemplate.opsForValue().get(key);
        return value == null ? null : JSON.parseObject(value, clazz);
    }

    /**
     * 获取 HashMap 数据
     *
     * @param key HashMap 的 key
     * @return HashMap 数据
     */
    public Map<Object, Object> getHash(String key) {
        return redisTemplate.opsForHash().entries(key);

    }


    /**
     * 解析 TS.MRANGE 命令返回的原始数据(List<Object>),构造 TsResult 列表
     */

    /**
     * 执行 TS.MRANGE 命令,查询满足 FILTER 条件的多个 TimeSeries 在指定时间范围内的数据
     *
     * @param timeRange 时间范围,格式为 "[开始时间, 结束时间]",如 "[20251020014000,20251023064000]"
     * @param filterMap 标签过滤条件,如 {"table": "pm", "station_id": "52889", "element": "V12001"}
     * @return 查询结果,每个元素为 TsResult(包含 key、timestamp、value)
     * @see <a href="https://oss.redis.com/redistimeseries/commands/#tsmrange">TS.MRANGE</a>
     *
     */
    public Map<String, List<TsResult>> queryMrange(String timeRange, Map<String, Object> filterMap,String dataCode) {
        Long[] longs = dateStrArrToLongArr(timeRange,dataCode);
        return queryMrange(longs[0], longs[1], filterMap, "TS.MRANGE");
    }

    /**
     *
     * 执行 TS.MGET 命令,查询满足 FILTER 条件的多个 TimeSeries 的最新数据
     * 示例:TS.MGET FILTER table=pm station_id=(52889,52674) element=V13011
     *
     * @param filterMap 标签过滤条件,如 {"table": "pm", "station_id": "52889", "element": "V12001"}
     * @return 查询结果,每个元素为 TsResult(包含 key、timestamp、value)
     * @see <a href="https://oss.redis.com/redistimeseries/commands/#tsmrange">TS.MGET</a>
     *
     **/
    public Map<String, List<TsResult>> queryMget(Map<String, Object> filterMap) {
        return queryMrange(null, null, filterMap, "TS.MGET");
    }

    /**
     * 执行 TS.MRANGE 命令,查询满足 FILTER 条件的多个 TimeSeries 在指定时间范围内的数据
     *
     * @param from      开始时间戳(毫秒)
     * @param to        结束时间戳(毫秒)
     * @param filterMap 标签过滤条件,如 {"table": "pm", "station_id": "52889", "element": "V12001"}
     * @return 查询结果,每个元素为 TsResult(包含 key、timestamp、value)
     */
    public Map<String, List<TsResult>> queryMrange(Long from, Long to, Map<String, Object> filterMap, String cmd) {
        // 1. 构造 TS.MRANGE 命令的参数
        List<byte[]> args = null;
        if (from == null && to == null) {
            args = buildArgsFilterMap(filterMap);
        } else {
            args = buildMrangeArgs(from, to, filterMap);
        }

        return executeCmdByArgs(cmd, args, null);
    }

    private Map<String, List<TsResult>> executeCmdByArgs(String cmd, List<byte[]> args, String key) {
        // 2. 获取 Lettuce 原生连接
        LettuceConnection connection = getLettuceConnection();

        // 3. 执行 TS.MRANGE 命令
        ObjectOutput keyValueListOutput = new ObjectOutput(new StringCodec());
        Object execute = connection.execute(cmd, keyValueListOutput, args.toArray(new byte[0][]));
        Map<String, List<Object>> rawResult = null;
        if (execute instanceof List) {
            Map<String, List<Object>> objectObjectHashMap = new HashMap<>();
            objectObjectHashMap.put(key, (List<Object>) execute);
            rawResult = objectObjectHashMap;
        } else if (execute instanceof Map) {
            rawResult = (Map<String, List<Object>>) execute;
        }

        // 4. 解析返回结果,构造 TsResult 列表
        return convertToTsResultMap(rawResult);
    }

    /**
     * 构造 TS.MRANGE 命令的参数(byte[] 格式)
     */
    private List<byte[]> buildMrangeArgs(Long from, Long to, Map<String, Object> filterMap) {
        List<byte[]> args = new ArrayList<>();
        args.add(String.valueOf(from).getBytes());  // from (timestamp)
        args.add(String.valueOf(to).getBytes());    // to (timestamp)

        List<byte[]> bytes = buildArgsFilterMap(filterMap);
        args.addAll(bytes);
        return args;
    }

    /**
     * 构件统计参数-多个站点,多个要素(这里只能统计多站点、单要素)
     * TS.MRANGE 1761323040000 1761526920000   AGGREGATION sum 203880000 BUCKETTIMESTAMP + FILTER station_id=(52889,52881) table=pm GROUPBY station_id REDUCE min
     * TS.MRANGE 1701323040000 1761526920000   AGGREGATION sum 203880000 BUCKETTIMESTAMP + FILTER station_id=(52889,52881) table=mm GROUPBY station_id REDUCE min
     * TS.MRANGE 1729787040000 1761872520000   AGGREGATION sum 203880000 BUCKETTIMESTAMP + FILTER station_id=(52980,52881,52889) element=(V12001,V11291) table=mm GROUPBY station_id   REDUCE min
     * TS.MRANGE 1729787040000 1761872520000   AGGREGATION sum 203880000 BUCKETTIMESTAMP + FILTER station_id=(52980,52881,52889) element=(V11291) table=mm GROUPBY station_id,element   REDUCE min (不行)
     * TS.MRANGE 1729787040000 1761872520000   AGGREGATION sum 203880000 BUCKETTIMESTAMP + FILTER station_id=(52980,52881,52889) element=(V12001) table=mm GROUPBY station_id   REDUCE min
     * TS.MRANGE 1729787040000 1761872520000   FILTER station_id=(52980,52881,52889) element=(V12001) table=mm AGGREGATION sum 203880000 BUCKETTIMESTAMP +  GROUPBY station_id   REDUCE min
     *
     * @param from
     * @param to
     * @param stationIds
     * @param table
     * @return
     */
    private List<byte[]> buildStatMrangeArgs(Long from, Long to, List<String> stationIds, List<String> elements, String table, String dateType,String redisEleRange) {
        List<byte[]> args = new ArrayList<>();
        args.add(String.valueOf(from).getBytes());
        args.add(String.valueOf(to).getBytes());
        if(StringUtils.isNotBlank(redisEleRange)){
            args.add(ApiConstant.FILTER_BY_VALUE.getBytes());
            args.add(redisEleRange.getBytes());
        }
        args.add("AGGREGATION".getBytes());
        args.add("sum".getBytes());
        long bucket_size_ms = to - from;
        args.add(String.valueOf(bucket_size_ms).getBytes());
        args.add("BUCKETTIMESTAMP".getBytes());
        args.add("+".getBytes());
        args.add("ALIGN".getBytes());
        long alignTime = getAlignTime(from, bucket_size_ms, dateType);
        args.add(String.valueOf(alignTime).getBytes());
        args.add("FILTER".getBytes());
        args.add(("station_id=(" + String.join(",", stationIds) + ")").getBytes());
        args.add(("element=(" + String.join(",", elements) + ")").getBytes());
        args.add(("table=" + table).getBytes());
        args.add("GROUPBY".getBytes());
        args.add("station_id".getBytes());
//        args.add("element".getBytes());
        args.add("REDUCE".getBytes());
        args.add("min".getBytes());
        return args;
    }

    /**
     * 构件单站点的 聚合或者5分钟的聚合
     * TS.RANGE pm:ts:52889:V13011 1761323040000 1761526920000  AGGREGATION sum 203880000 BUCKETTIMESTAMP + ALIGN align_time
     * TS.RANGE pm:ts:52889:V13011 1761323040000 1761526920000 FILTER_BY_VALUE -inf 30  AGGREGATION sum 203880000 BUCKETTIMESTAMP + ALIGN align_time
     *
     * @param from
     * @param to
     * @param key
     * @return
     */
    private List<byte[]> buildStatRangeArgs(Long from, Long to, String key, StatParam statParam, String dateType,String redisEleRange) {
        Integer windowSize = statParam.getWindowSize();
        String statType = statParam.getStatType();
        List<byte[]> args = new ArrayList<>();
        args.add(key.getBytes());
        args.add(String.valueOf(from).getBytes());
        args.add(String.valueOf(to).getBytes());
        if(StringUtils.isNotBlank(redisEleRange)){
            args.add(ApiConstant.FILTER_BY_VALUE.getBytes());
            args.add(redisEleRange.getBytes());
        }
        args.add("AGGREGATION".getBytes());
        args.add(statType.getBytes());
        long bucket_size_ms = getBucketSizeMs(from, to, windowSize, dateType);
        args.add(String.valueOf(bucket_size_ms).getBytes());
        args.add("BUCKETTIMESTAMP".getBytes());
        args.add("+".getBytes());
        args.add("ALIGN".getBytes());
        long alignTime = getAlignTime(from, bucket_size_ms, dateType);
        args.add(String.valueOf(alignTime).getBytes());
        return args;
    }

    /**
     * 获取对齐时间
     * align_time = (start_ts // bucket_size_ms) * bucket_size_ms+ adj_time
     *
     * @param from
     * @param bucket_size_ms
     * @param dateType
     * @param dateType
     * @return
     */
    private Long getAlignTime(Long from, long bucket_size_ms, String dateType) {
        return from / bucket_size_ms * bucket_size_ms + getAdjTime(dateType);
    }

    private String getKey(String stationId, String table, String element) {
        return table + ":ts:" + stationId + ":" + element;
    }

    /**
     * 数据统计
     * 1.如果是单个站点,按照单个站点的方式进行聚合
     * TS.RANGE pm:ts:52889:V13011 start_ts end_ts  AGGREGATION sum bucket_size_ms BUCKETTIMESTAMP + ALIGN align_time
     * TS.RANGE pm:ts:52889:V13011 1761323040000 1761526920000  AGGREGATION sum 203880000 BUCKETTIMESTAMP + ALIGN align_time
     * 2.如果是多个站点,按照多个站点进行聚合
     * TS.MRANGE 1761323040000 1761526920000   AGGREGATION sum 203880000 BUCKETTIMESTAMP + FILTER station_id=(52889,52881) table=pm GROUPBY station_id REDUCE min
     * 3.需要区分一下是不是5分钟的聚合方式
     *
     * @param timeRange  时间范围 [start_ts, end_ts]
     * @param stationIds 站点编号
     * @param table      表名 pm/mm
     * @return
     */
    public Map<String, List<TsResult>> statTsDataMiddle(String timeRange, List<String> stationIds, List<String> elements, String table, StatParam statParam, String dateType,String redisEleRange,String dataCode) {
        Long[] timeParamArr = dateStrArrToLongArr(timeRange,dataCode);
        if (stationIds.isEmpty()) {
            throw new OuterException(RetCode.BAD_REQUEST, "站点编号不能为空");
        }
        if (elements.isEmpty()) {
            throw new OuterException(RetCode.BAD_REQUEST, "要素不能为空");
        }
        if (stationIds.size() == 1 && elements.size() == 1) {
            //单站点、单要素按照 TS.RANGE的方式进行统计
            String key = getKey(stationIds.get(0), table, elements.get(0));
            List<byte[]> args = buildStatRangeArgs(timeParamArr[0], timeParamArr[1], key, statParam, dateType,redisEleRange);
            return executeCmdByArgs("TS.RANGE", args, key);
        } else {
            //多站点、单要素按照 TS.MRANGE的方式进行统计
            List<byte[]> args = buildStatMrangeArgs(timeParamArr[0], timeParamArr[1], stationIds, elements, table, dateType,redisEleRange);
            Map<String, List<TsResult>> stringListMap = executeCmdByArgs("TS.MRANGE", args, null);
            //结果值转换
            return convertToResultMap(stringListMap, table, elements.get(0));
        }

    }

    private Map<String, List<TsResult>> convertToResultMap(Map<String, List<TsResult>> stringListMap, String tableName, String ele) {
        return stringListMap.entrySet()
                .stream()
                .collect(Collectors.toMap(
                        entry -> buildNewKey(entry.getKey(), tableName, ele),
                        Map.Entry::getValue
                ));
    }

    private String buildNewKey(String originalKey, String tableName, String ele) {
        String[] split = originalKey.split("=");
        return tableName + ":ts:" + split[1] + ":" + ele;
    }

    /**
     * 基于redis的数据统计结果,做两件事
     * 1.如果是窗口聚合,使用ALIGN参数调整桶的开始时间,使其从01开始,获取的时间戳减去adj_time,得到实际的聚合时间
     * 2.统一处理map的key,让其后面的流程可统一处理
     *
     * @param timeRange
     * @param stationIds
     * @param elements
     * @param table
     * @return
     */
    public Map<String, List<TsResult>> statTsData(String timeRange, List<String> stationIds, List<String> elements, String table, String dateType, StatParam statParam,String redisEleRange,String dataCode) {
        Map<String, List<TsResult>> stringListMap = statTsDataMiddle(timeRange, stationIds, elements, table, statParam, dateType,redisEleRange,dataCode);
        if (statParam.getWindowSize() == null) {
            return stringListMap;
        }
        return stringListMap.entrySet().stream().collect(Collectors.toMap(entry -> entry.getKey(), entry -> entry.getValue().stream().map(result -> {
            long l = result.getTime() - getAdjTime(dateType);
            result.setTime(l);
            result.setKey(DateUtil.longToLocalDateTime(l));
            return result;
        }).collect(Collectors.toList())));
    }


    /**
     * 获取所有以 meta:staid: 开头的 Hash Key,并返回它们的 HGETALL 数据
     */
    public Map<String, Map<String, Object>> getAllMetaStaidHashes() {
        // 1. 使用 keys() 查找所有匹配的 key
        String pattern = "meta:staid:*";
        Set<String> keys = redisTemplate.keys(pattern);

        if (keys == null || keys.isEmpty()) {
            return new HashMap<>();
        }

        Map<String, Map<String, Object>> collect = keys.stream().collect(Collectors.toMap(key -> key.split(":")[2], key -> {
            // 获取原始 Hash 数据:Map<Object, Object>
            Map<Object, Object> rawEntries = redisTemplate.opsForHash().entries(key);

            // 转换为 Map<String, Object>
            Map<String, Object> stringifiedEntries = rawEntries.entrySet().stream().collect(Collectors.toMap(entry -> entry.getKey().toString(),          // 字段名转 String
                    entry -> entry.getValue()                  // 字段值保持 Object
            ));
            return stringifiedEntries;
        }));
        return collect;
    }




}
xml 复制代码
package com.dfec.api.entity;

import java.time.LocalDateTime;

/**
 *
 * @author tangrg
 * @email 1446232546@qq.com
 * @date 2025-10-2025/10/23 10:38:06
 */
public class TsResult {

    private Long time;

    private LocalDateTime  key;

    private Double val;

    public Long getTime() {
        return time;
    }

    public void setTime(Long time) {
        this.time = time;
    }

    public LocalDateTime getKey() {
        return key;
    }

    public void setKey(LocalDateTime key) {
        this.key = key;
    }

    public Double getVal() {
        return val;
    }

    public void setVal(Double val) {
        this.val = val;
    }
}

最近项目比较赶,其实可以考虑将这部分封装成一个starter来使用,后面有时间会考虑

相关推荐
回家路上绕了弯2 小时前
高并发订单去重:布隆过滤器过滤已存在订单号的实战方案
分布式·后端
刘一说2 小时前
Spring Boot 应用的云原生 Docker 化部署实践指南
spring boot·docker·云原生
申阳2 小时前
Day 11:集成百度统计以监控站点流量
前端·后端·程序员
Cache技术分享2 小时前
239. Java 集合 - 通过 Set、SortedSet 和 NavigableSet 扩展 Collection 接口
前端·后端
demonre2 小时前
阿里云 Debian 13.1 安装 docker 并切换阿里云镜像源
后端·docker
武子康2 小时前
大数据-152 Apache Druid 集群模式 [下篇] 低内存集群实操:JVM/DirectMemory与启动脚本
大数据·后端·nosql
程序猿DD3 小时前
探索 Java 中的新 HTTP 客户端
java·后端
lizhongxuan3 小时前
eBPF性能揭秘 - XDP 和 JIT
后端
用户69371750013843 小时前
Kotlin 协程 快速入门
android·后端·kotlin