场景
我们现在有这样一个场景,使用国产的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来使用,后面有时间会考虑