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来使用,后面有时间会考虑

相关推荐
You丶小明快跑6 分钟前
部署redis 集群和redis常用命令
数据库·redis·缓存
Libby博仙8 分钟前
Spring Boot 条件化注解深度解析
java·spring boot·后端
源代码•宸26 分钟前
Golang原理剖析(Map 源码梳理)
经验分享·后端·算法·leetcode·golang·map
小周在成长29 分钟前
动态SQL与MyBatis动态SQL最佳实践
后端
瓦尔登湖懒羊羊38 分钟前
TCP的自我介绍
后端
小周在成长40 分钟前
MyBatis 动态SQL学习
后端
子非鱼92142 分钟前
SpringBoot快速上手
java·spring boot·后端
我爱娃哈哈1 小时前
SpringBoot + XXL-JOB + Quartz:任务调度双引擎选型与高可用调度平台搭建
java·spring boot·后端
JavaGuide1 小时前
Maven 4 终于快来了,新特性很香!
后端·maven
开心就好20251 小时前
全面解析iOS应用代码混淆和加密加固方法与实践注意事项
后端