【技术总结】SpringBoot中实现数据按照时、天、星期单位聚合统计查询的三种方式

SpringBoot中实现数据按照时、天、星期单位聚合统计查询的三种方式

一、前言

在项目设计方案中经常会涉及到数据聚合统计查询,尤其在大屏设计中,例如,电商统计一周内某个商品每天购买量,通过折线图呈现,商城统计一个月没每天客流量,按照折线图呈现等等,这也是项目设计中的常规功能,功能的实现设计方案在不同的数据量场景使用的设计方案不一样;
比较常见的设计方案围绕着数据库时序数据库大数据设计方案(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的数据量)。

在这里插入图片描述
工作中比较常接触的时序数据库有,influxDBTDengineinfluxDB使用用户最多,社区资料最全,但是最大的问题是influxDB2.x版本集群环境没有开源,TDengine时序数据库是一款国产数据库,集群当前是开源的,部署、维护和使用方式比较简单,缺点是社区资料相对较少;
本文章使用TDengine时序数据库举例;时序数据库是NoSql数据库,聚合数据的查询是它最基本的功能之一,一般应用在大数据量场景;

TDengine环境搭建:
在这里插入图片描述
插入数据:
在这里插入图片描述
按照小时聚集查询:
在这里插入图片描述
注:TDengine数据存储使,每个设备一张表,单表查询就是按照某个设备查询,单表数据量有限,查询性能更高;

========TDegnine时序数据库使用不做过多介绍,具体使用可以查看TDengine官网,介绍比较详细;

六、总结

这里举例的三种使用场景主要对数据做聚合统计查询,如果要对数据做其它应该场景的分析可以选择Elasticsearchclickhouse等其他大数据组件方案,并且TDengine时序数据库还有更多其它使用场景和功能,大家可以通过官网学习了解;

----------------------------------👇👇👇注:源码请关注公众号获取👇👇👇--------------------------------------------

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Dylan~~~

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值