一、前言
在项目设计方案中经常会涉及到数据聚合统计查询
,尤其在大屏设计中,例如,电商统计一周内某个商品每天购买量,通过折线图呈现,商城统计一个月没每天客流量
,按照折线图
呈现等等,这也是项目设计中的常规功能,功能的实现设计方案在不同的数据量场景使用的设计方案不一样;
比较常见的设计方案围绕着数据库、时序数据库、大数据设计方案(Elasticsearch等);
折线图示例:
二、思路
在实际应用并不是项目设计方案越复杂越好,性能越高越好,一般功能的性能与设计方案、人力投入成本、开发周期和服务器资源占用等都是成反比,同时,性能和数据量也是成正比的,所有功能实现的方案设计要围绕着这些方案进行评估和设计;
本文主要总结通过数据库postgresql
、高频数据量存储方案
、时序数据库(influxdb、TDengine)
实现方式;
三、基于数据库聚合函数
类似于IOT使用场景或者ELK日志采集场景会把采集到时序数据存储到数据库postgresql
记录表中,时序数据一般不存储数据删除、修改
操作,只会按照时间循序插入数据
和查询数据
;
数据记录表,如下:
根据设备编码dev_code
,统计一天内每个小时的数据量(sum求和),一周内每天的数据量,一个内每周的数据量按照折线图统计呈现;
我们按照小时统计为例:
SELECT date_trunc('hour', pass_time) AS hour,
COUNT(*) AS count,
AVG(sum) AS avg_value,
SUM(sum) AS sum_value
FROM record_table
GROUP BY hour
ORDER BY hour;
查询结果:
查询性能提升:
数据表提高性能最直接的方式就是创建索引,所以在记录表中也一样要创建索引,我们这里使用时序索引;
PostgreSQL 支持创建时序索引
(也称为时间序列索引或时间分区索引),这种索引特别适用于基于时间范围查询的优化。创建时序索引的方法之一是使用 BRIN 索引(Block Range INdex),它适用于非常大的表,特别是那些数据在物理存储上自然排序(如时间序列数据)的表。
postgresql时序索引创建方式:
CREATE INDEX record_pass_time_idx ON record_table USING BRIN (pass_time);
四、基于数据库清洗
除了记录表(record_table
)以外,新增一张统计表(statistics_table
),所以在查询统计数据时不需要再使用数据库的聚集函数查询,使用函数查询性能会低于直接查询数据库,但是增加了存储空间和数据一致性问题;
查询结果直接存储到统计表中,如下:
查询sql:
select * from statistics_table where dev_code='dev_1';
这里使用最小的时间统计单位小时存储
,如果按照天存储可以使用聚合按照查询或者新增以天为单位
的统计表,这个根据实际应该的场景决定,不再举例;
这里使用查询很简单,但是在数据存储数据库可能会比较耗用性能,以下主要在数据存储上总结的三种方案:
1、直接更新数据库
在采集到新增数据时,存储记录表的同时存储到统计表中
方式一:
方式二:
方式一性能低于方式二,方式二可能存储数据不一致性问题,方式一不会,代码比较常规,不在举例;
2、定时清洗跟新数据库
在采集数据量并发频率比较大时,会采用定时分析采集数据的存储到统计表中,流程如下:
3、使用缓存统计数据,定时更新数据库
在同一个设备在相同时间单位类如果数据量比较大
,如果直接更新数据表统计表,会对数据库造成较大的性能压力,所以我们通过在缓存中先统计数据,定时更新到数据库中,并不是每条数据都直接更新到数据库统计表中,定时更新缓存统计结果,降低对数据库的请求压力;尤其数据库表的update操作,会涉及加锁
的问题;
实现代码示例:
MessageDelayed.java
package com.sk.proxytest.bean;
import lombok.Data;
import java.util.concurrent.Delayed;
import java.util.concurrent.TimeUnit;
@Data
public class MessageDelayed implements Delayed{
/**
* 报文内容
*/
private final String devCode;
/**
* 计时开始时间
*/
private final long startTime;
/**
* 超时时间
*/
private static final long EXPIRE_TIME = 30 * 1000;
/**
* 构造函数
* @param message 报文内容
*/
public MessageDelayed(String message) {
this.devCode = message;
this.startTime = System.currentTimeMillis();
}
@Override
public long getDelay(TimeUnit unit) {
long elapsedTime = System.currentTimeMillis() - startTime;
return unit.convert(System.currentTimeMillis() - elapsedTime, TimeUnit.MILLISECONDS);
}
@Override
public int compareTo(Delayed o) {
if(this == o){
return 0;
}
// 根据剩余时间来进行排序
long diff = getDelay(TimeUnit.MILLISECONDS) - o.getDelay(TimeUnit.MILLISECONDS);
if(diff == 0){
return 0;
}else if(diff < 0){
return -1;
}else {
return 1;
}
}
}
DataService.java
package com.sk.proxytest.init;
import com.alibaba.fastjson.JSONObject;
import com.sk.proxytest.bean.MessageDelayed;
import com.sk.proxytest.bean.RecordInfo;
import com.sk.proxytest.bean.StatisticsInfo;
import java.util.Calendar;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.DelayQueue;
public class DataService {
public static final Map<String, StatisticsInfo> statisticsMap = new HashMap<>();
public static final Map<String, String> queueMap = new HashMap<>();
/**
* 延时队列存储报文
*/
public static final DelayQueue<MessageDelayed> RESULT_MESSAGE_DELAY_QUEUE = new DelayQueue<>();
public void dataDeal(RecordInfo recordInfo){
String devCode = recordInfo.getDevCode();
StatisticsInfo statisticsInfo = statisticsMap.get(devCode);
statisticsInfo.setDevCode(devCode);
statisticsInfo.setHour(getHour(recordInfo.getPassTime()));
statisticsInfo.setSum(recordInfo.getSum());
statisticsMap.put(devCode,statisticsInfo);
if(!queueMap.containsKey(devCode)){
queueMap.put(devCode,devCode);
RESULT_MESSAGE_DELAY_QUEUE.put(new MessageDelayed(devCode));
}
}
public Date getHour(Date date){
Calendar calendar = Calendar.getInstance();
calendar.setTime(date);
calendar.set(Calendar.MINUTE, 0);
calendar.set(Calendar.SECOND, 0);
calendar.set(Calendar.MILLISECOND, 0); // 可选,设置毫秒为0
// 获取修改后的Date对象
Date updatedDate = calendar.getTime();
return updatedDate;
}
}
**DataThread.java**
package com.sk.proxytest.init;
import com.sk.proxytest.bean.MessageDelayed;
import com.sk.proxytest.bean.StatisticsInfo;
import lombok.SneakyThrows;
public class DataThread implements Runnable{
@SneakyThrows
@Override
public void run() {
while(true){
MessageDelayed messageDelayed = DataService.RESULT_MESSAGE_DELAY_QUEUE.take();
String devCode = messageDelayed.getDevCode();
StatisticsInfo statisticsInfo = DataService.statisticsMap.get(devCode);
//TODO
//statisticsInfo存储数据库统计表
DataService.queueMap.remove(devCode);
}
}
}
注:可以不使用延迟队列,可以通过定时变量集合statisticsMap
,更新到数据库统计表中,但是使用延迟队列可以具体只更新具体某个设备,如果变量集合statisticsMap
时,如果集合statisticsMap
数据量比较大,会有性能问题,并且使用延迟队列可以使用多线程存储;
五、基于时序数据库
时序数据库全称为时间序列数据库。时间序列数据库指主要用于处理带时间标签(按照时间的顺序变化,即时间序列化)的数据,带时间标签的数据也称为时间序列数据。
时间序列数据主要由电力行业、化工行业、气象行业、地理信息等各类型实时监测、检查与分析设备所采集、产生的数据,这些工业数据的典型特点是:产生频率快(每一个监测点一秒钟内可产生多条数据)、严重依赖于采集时间(每一条数据均要求对应唯一的时间)、测点多信息量大(常规的实时监测系统均有成千上万的监测点,监测点每秒钟都产生数据,每天产生几十GB的数据量)。
工作中比较常接触的时序数据库有,influxDB
和TDengine
,influxDB使用用户最多,社区资料最全,但是最大的问题是influxDB2.x版本集群环境没有开源,TDengine时序数据库是一款国产数据库,集群当前是开源的,部署、维护和使用方式比较简单,缺点是社区资料相对较少;
本文章使用TDengine时序数据库举例;时序数据库是NoSql数据库,聚合数据的查询是它最基本的功能之一,一般应用在大数据量场景;
TDengine环境搭建:
插入数据:
按照小时聚集查询:
注:TDengine数据存储使,每个设备一张表,单表查询就是按照某个设备查询,单表数据量有限,查询性能更高;
========TDegnine时序数据库使用不做过多介绍,具体使用可以查看TDengine官网,介绍比较详细;
六、总结
这里举例的三种使用场景主要对数据做聚合统计查询,如果要对数据做其它应该场景的分析可以选择Elasticsearch
、clickhouse
等其他大数据组件方案,并且TDengine时序数据库还有更多其它使用场景和功能,大家可以通过官网学习了解;
----------------------------------👇👇👇注:源码请关注公众号获取
👇👇👇--------------------------------------------