文章目录
1、InfluxDB简介
时序数据库是近几年一个特殊的概念,与传统的Mysql关系型数据库相比,它的最大的特点是:数据按照时间顺序存储。举例来说,日志数据,是以时间顺序存储的,所以用时序数据库存储是一种很好的选择。使用Mysql在存储的过程中,不对这种基于时间的数据进行优化的,所以在查询、插入上有一些瓶颈。而InfluxDB则会进行优化,并且具备有很多特点,如下:
- 专为时间序列数据编写的自定义高性能数据存储。 TSM引擎允许高摄取速度和数据压缩
- 完全用 Go 语言编写。 它编译成单个二进制文件,没有外部依赖项
- 简单,高性能的写入和查询HTTP API
- 专为类似SQL的查询语言量身定制,可轻松查询聚合数据
- 标签允许对系列进行索引以实现快速有效的查询
- 保留策略有效地自动使过时数据过期
- 连续查询自动计算聚合数据,以提高频繁查询的效率
常见的时间序列数据库除了InfluxDB之外还有:opentsdb、timeScaladb、Druid等。
那么数据是如何在InfluxDB中进行存储的呢?下面我们来看下InfluxDB的数据模型:
2、InfluxDB数据结构
以下是 InfluxDB 和传统关系型数据库(如 MySQL、PostgreSQL)的概念对比表格:
InfluxDB 概念 | 传统关系型数据库概念 | 描述 |
---|---|---|
database | database | 数据库(同) |
Measurement | Table | InfluxDB 中的数据组织单元,是一个容器,类似于关系型数据库中的表。包含了列time,field和tag。 |
Field | Column | InfluxDB 中的数据字段,存储时间序列的具体数据,类似于关系数据库中的列。 |
Timestamp | Timestamp | 每个数据点都有一个时间戳,表示数据的存储时间,关系数据库也可以有时间戳字段。 |
Tag | Index/Metadata | InfluxDB 中的元数据,用于索引和查询,类似于关系数据库中的索引列。 |
Point | Row | 表里面的一行数据,由时间戳(time)、数据(field)和标签(tags)组成 |
Series | Rows | 在 InfluxDB 中,Series 是具有共同retention policy,measurement和tag set的时间序列集合。 |
Continuous Queries | View | InfluxDB 提供的持续查询功能,可以定期计算数据并存储结果,类似于关系型数据库中的视图。 |
Retention Policy | Data Retention | InfluxDB 的数据保留策略,控制数据的存储时间,关系数据库通常没有这种自动过期机制。 |
Line Protocol | SQL | InfluxDB 使用行协议(Line Protocol)写入数据,格式独特;关系型数据库使用 SQL 来管理数据。 |
Schema-less | Schema-based | InfluxDB 是无模式的,数据可以随时添加新字段,而关系型数据库有严格的模式定义。 |
Time Series Data | General Data | InfluxDB 专门为时间序列数据设计,优化高频率写入和查询,而关系数据库更通用,可以存储多种类型的数据。 |
这个表格提供了 InfluxDB 和传统关系型数据库在概念上的主要差异,可以帮助你理解两者的结构和用途。
point属性 | 含义 |
---|---|
time | 数据记录的时间,主索引,默认自动生成,相当于每行数据都具备的列 |
tags | 相当于有索引的列。tag中存储的值的类型总是字符串类型 |
fields | value值,没有索引的列。field中存储的值的类型:字符串、浮点数、整数、布尔型。一个field value总是和一个timestamp相关联 |
Field sets: 每组field key和field value的集合,即我们需要的字段,如internale[key]= 76[value], external[key]= 18[value]。不可索引
Tag sets: 不同的每组tag key和tag value的集合,如device[key]= dev1[value], buiding[key]=b1[value]。可索引
在 InfluxDB 表结构中,field 和 tag 是用于存储数据的两种不同类型。
Field(字段) | Tag(标签) |
---|---|
Field 用于存储实际的数值数据,例如温度、湿度等测量值。 | Tag 用于存储元数据信息和标识数据的键值对,例如传感器名称、地理位置等。 |
Field 是可变的,可以随时间的推移而改变其值。 | Tag 是不可变的,一旦设置就不能更改。 |
Field 的值可以进行聚合计算,例如求平均值或总和等。 | Tag 的值不能进行聚合计算,只能用于过滤和分组查询。 |
Field 不适合用于过滤和索引数据,因为它没有元数据信息。 | Tag 是 InfluxDB 中的主要索引机制之一,可以提高查询性能和数据过滤效率。 |
3、InfluxDB存储架构
在 InfluxDB 中可以创建多个数据库,不同数据库中的数据文件是隔离 存放的,存放在磁盘上的不同目录,每个database 可以有多个RP(retention policy数据保存策略),但是只有一个默认策略。策略下按照时间段分为多个ShardGroup,每个ShardGroup存储一个时间段的数据。每个shardgroup下分多个shard来存储数据。如下图所示:
- retention policy: 存储策略,用于设置数据保留的时间,每个数据库刚开始会自动创建一个默认的存储策略 autogen,数据保留时间为永久,之后用户可以自己设置,例如保留最近2小时的数据。插入和查询数据时如果不指定存储策略,则使用默认存储策略,且默认存储策略可以修改。InfluxDB 会定期清除过期的数据。
- ShardGroup: 是一个逻辑概念,按时间区间划分,是InfluxDB数据过期执行的最小单元。
- Shard: 在 InfluxDB 中是一个比较重要的概念,它和 retention policy 相关联。每一个存储策略下会存在许多 shard,每一个 shard 存储一个指定时间段内的数据,并且不重复,例如 7点-8点 的数据落入 shard0 中,8点-9点的数据则落入 shard1 中。每一个 shard 都对应一个底层的 tsm存储引擎.。
4、InfluxDB基本操作
InfluxDB最好基于docker容器进行安装,这里就略过了。
1_数据库操作
首先我们进入虚拟机中的Influx的镜像
bash
docker exec -it influxdb /bin/bash #进入influxdb虚拟机中
链接InfluxDB
sql
influx #进入influx操作界面
显示数据库
sql
show databases #显示数据库
创建数据库
sql
create database restkeeper #创建数据库
删除数据库
sql
drop database restkeeper #删除数据库
使用指定数据库
sql
use 库名称
2_数据表操作
显示所有的表
在InfluxDB当中,并没有表(table)这个概念,取而代之的是MEASUREMENTS,MEASUREMENTS的功能与传统数据库中的表一致,因此我们也可以将MEASUREMENTS称为InfluxDB中的表。
sql
SHOW MEASUREMENTS
新建表
InfluxDB中没有显式的新建表的语句,只能通过insert数据的方式来建立新表。其中 disk_free 就是表名,hostname是索引(tag),value=xx是记录值(field),记录值可以有多个,系统自带追加时间戳
sql
insert customer,customer_name=张三,identity_card=342401198811180000 age=37,earning=5000
insert customer,customer_name=李四,identity_card=342401198811180001 age=37,earning=4500
insert customer,customer_name=王五,identity_card=342401198411180002 age=41,earning=6000
insert customer,customer_name=黄六,identity_card=342401198311180002 age=42,earning=7000
上面,我们新增一条数据,measurement为customer, tag为customer_name,identity_card, field为age,earning。
我们简单小结一下插入的语句写法:
- 基本格式:
insert + measurement + "," + tag=value,tag=value +空格+ field=value,field=value
; - tag与tag之间用逗号分隔;field与field之间用逗号分隔;
- tag与field之间用空格分隔;
- tag都是string类型,不需要引号将value包裹;
- field如果是string类型,需要加引号;
在 InfluxDB 行协议中,一条数据和另一条数据之间使用换行符分隔, 所以一行就是一条数据。另外,在时序数据库领域,一行数据一行数据由下面 4 种元素构成。
删除表
sql
drop measurement customer
3_数据保存策略
InfluxDB是没有提供直接删除数据记录的方法,但是提供数据保存策略,主要用于指定数据保留时间,超过指定时间,就删除这部分数据。
查看保存策略
sql
show retention policies on "db_name"
show retention policies on restkeeper
注意:其中test为数据库名称
创建保存策略
sql
create retention policy "rp_name" on "db_name" duration 3w replication 1 default
create retention policy rp_restkeeper on restkeeper duration 1h replication 1 default
rp_name :策略名; db_name :具体的数据库名;
3w :保存3周,3周之前的数据将被删除,influxdb具有各种事件参数,比如:h(小时),d(天),w(星期);
replication 1:副本个数,一般为1就可以了; default:设置为默认策略
修改保存策略
sql
alter retention policy "rp_name" on "db_name" duration 30d default
alter retention policy rp_restkeeper on restkeeper duration 2h default
删除保存策略
sql
drop retention policy "rp_name" on "db_name"
drop retention policy rp_restkeeper on restkeeper
注意:其中test为数据库名称
4_数据查询操作
InfluxDB基本查询操作和MySQL的基本查询是类似,综合使用如下所示:
sql
#----综合使用
书写顺序
select distinct * from '表名' where '限制条件' group by '分组依据' having '过滤条件' order by limit '展示条数'
执行顺序
from -- 查询
where -- 限制条件 使用单引号,否则无数据返回或报错
group by -- 分组 只能对tags和time进行分组
having -- 过滤条件
order by -- 排序 只能对time进行排序
limit -- 展示条数
distinct -- 去重
select -- 查询的结果
查询数据表customer的所有记录
sql
select * from customer
条件查询
sql
select * from customer where customer_name ='张三'
排序查询
sql
select * from customer group by time desc
去重 (distinct)
sql
select distinct age from customer
注意:distinct 函数只能有一个值
group by
为了分组我们先插入一条数据
sql
insert customer,customer_name=张七,identity_card=342401198311180002 age=42,earning=7000
sql
select * from customer group by identity_card
聚合函数
count()函数:返回一个(field)字段中的非空值的数量。
sql
select count(*) from customer
mean() 函数:返回一个字段(field)中的值的算术平均值(平均值)。字段类型必须是长整型或float64。
sql
select mean(age) from customer
median()函数:从单个字段(field)中的排序值返回中间值(中位数)。中值是在一组数值中居于中间的数值。字段值的类型必须是长整型或float64格式。
sql
select median(age) from customer
spread()函数:返回字段的最小值和最大值之间的差值。数据的类型必须是长整型或float64。
sql
select spread(age) from customer
sum()函数:返回一个字段中的所有值的和。字段的类型必须是长整型或float64。
sql
select sum(age) from customer
integral()函数:用于计算时间序列数据在制定时间范围内的积分值,积分值可以帮助我们更好的理解时间序列数据的趋势和变化
sql
select integral(age) from customer
limit限制条数
sql
select * from customer limit 1 offset 2
or查询
influxDB中没有in的操作,但是有or。对于习惯了mysql的in来说,用or就需要在代码中循环了。
sql
select * from customer where customer_name='张三' or customer_name='李四'
模糊查询
模糊查询支持正则表达式方式,例如
- =~/给定字段/ 包含指定字段的
- =~/^给定字段/ 以指定字段开始的
- =~/给定字段$/ 以指定字段结尾的
sql
select * from customer where customer_name=~/张/
5、InfluxDB持久层封装
上面介绍了InfluxDB的基本内容,接下来来看下如何使用spring-boot来操作InfluxDB数据库,首先我们来看下整个的系统结构图例:
对比下mybatis中的执行流程:
1_自动装配
首先,我们来看下第一步自动装配:依赖spring-boot自动装配出InfluxDB对象,并把对象交于IOC容器管理,对于spring-boot来说它最大的特点就是自动装配,这里我们用到2个类:配置文件类InfluxDbProperties,配置类InfluxDbAutoConfiguration,如下图所示:
在spring-boot-autoconfigure
中已经构建好对应的类信息,下面我们逐一解读一下,首先我们看下 InfluxDbProperties 配置:
java
package org.springframework.boot.autoconfigure.influx;
import org.springframework.boot.context.properties.ConfigurationProperties;
/**
* Configuration properties for InfluxDB
*/
@ConfigurationProperties(prefix = "spring.influx")
public class InfluxDbProperties {
/**
* URL of the InfluxDB instance to which to connect.
*/
private String url;
/**
* Login user.
*/
private String user;
/**
* Login password.
*/
private String password;
/**
* setter、getter略
*/
}
当我们在使用时,只需要在对应项目的bootstrap.yml文件做如下配置:
yml
spring:
influx:
url: http://192.168.193.141:8086
password: 123456
user: admin
mapper-location: ../mapper
spring-boot 在发现我们引入 InfluxDB.class
后自动按照 InfluxDbProperties
的属性帮我们构建InfluxDB 对象交于spring-IOC容器
java
package org.springframework.boot.autoconfigure.influx;
@Configuration(proxyBeanMethods = false)
@ConditionalOnClass(InfluxDB.class)
@EnableConfigurationProperties(InfluxDbProperties.class)
public class InfluxDbAutoConfiguration {
@Bean
@ConditionalOnMissingBean
@ConditionalOnProperty("spring.influx.url")
public InfluxDB influxDb(InfluxDbProperties properties,
ObjectProvider<InfluxDbOkHttpClientBuilderProvider> builder) {
return new InfluxDBImpl(properties.getUrl(), properties.getUser(), properties.getPassword(),
determineBuilder(builder.getIfAvailable()));
}
private static OkHttpClient.Builder determineBuilder(InfluxDbOkHttpClientBuilderProvider builder) {
if (builder != null) {
return builder.get();
}
return new OkHttpClient.Builder();
}
}
2_配置管理
构建好InfluxDB 对象后,那如何使用呢?下面我们来看下第二步配置管理:项目启动时,通过InfluxDBConfig构建出业务执行器、参数处理器、结果处理器,并把对象交于IOC容器管理,在framework-influxdb
项目中我们构建了一个InfluxDBConfig
配置类,内容如下:
java
package org.example.influxDd.config;
import org.example.influxDd.core.Executor;
import org.example.influxDd.core.ParameterHandler;
import org.example.influxDd.core.ResultSetHandler;
import org.influxdb.InfluxDB;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* 时序数据库配置类
*/
@Configuration
public class InfluxDBConfig {
@Bean(name = "executor")
public Executor executor(InfluxDB influxDB) {
return new Executor(influxDB);
}
@Bean(name = "parameterHandler")
public ParameterHandler parameterHandler(InfluxDB influxDB) {
return new ParameterHandler();
}
@Bean(name = "resultSetHandler")
public ResultSetHandler resultSetHandler(InfluxDB influxDB) {
return new ResultSetHandler();
}
}
将其配置到自动装配文件META-INF/spring.factories
:
yml
org.springframework.boot.autoconfigure.EnableAutoConfiguration=org.example.influxDd.config.InfluxDBConfig
业务执行器Executor :需要从spring-IOC中拿到InfluxDB来完成构建,用于与influxDB进行交互
java
package org.example.influxDd.core;
import lombok.extern.slf4j.Slf4j;
import org.example.influxDd.util.EmptyUtil;
import org.influxdb.InfluxDB;
import org.influxdb.annotation.Measurement;
import org.influxdb.dto.BatchPoints;
import org.influxdb.dto.Point;
import org.influxdb.dto.Query;
import org.influxdb.dto.QueryResult;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* 执行器
*/
@Slf4j
public class Executor {
InfluxDB influxDB;
public Executor() {
}
public Executor(InfluxDB influxDB) {
this.influxDB = influxDB;
}
public List<Map<String,Object>> select(String sql,String database) {
QueryResult queryResult = influxDB.query(new Query(sql, database));
List<Map<String,Object>> resultList = new ArrayList<>();
queryResult.getResults().forEach(result -> {
//查询出错抛出错误信息
if (!EmptyUtil.isNullOrEmpty(result.getError())){
throw new RuntimeException(result.getError());
}
if (!EmptyUtil.isNullOrEmpty(result)&&!EmptyUtil.isNullOrEmpty(result.getSeries())){
//获取所有列的集合,一个迭代是代表一组
List<QueryResult.Series> series= result.getSeries();
for (QueryResult.Series s : series) {
//列中含有多行数据,每行数据含有多列value,所以嵌套List
List<List<Object>> values = s.getValues();
//每组的列是固定的
List<String> columns = s.getColumns();
for (List<Object> v:values){
//循环遍历结果集,获取每行对应的value,以map形式保存
Map<String,Object> queryMap =new HashMap<String, Object>();
for(int i=0;i<columns.size();i++){
//遍历所有列名,获取列对应的值
String column = columns.get(i);
if (v.get(i)==null||v.get(i).equals("null")){
//如果是null就存入null
queryMap.put(column,null);
}else {
//不是null就转成字符串存储
String value = String.valueOf(v.get(i));
//如果是时间戳还可以格式转换,我这里懒了
queryMap.put(column, value);
}
}
//把结果添加到结果集中
resultList.add(queryMap);
}
}
}
});
return resultList;
}
public void insert(Object args[]) {
if (args.length != 1) {
throw new RuntimeException();
}
Object obj = args[0];
List<Object> list = List.of();
if (obj instanceof List){
list = (ArrayList) obj;
}else {
list.add(obj);
}
if (list.size() > 0) {
Object firstObj = list.get(0);
Class<?> domainClass = firstObj.getClass();
List<Point> pointList = new ArrayList<>();
for (Object o : list) {
Point point = Point
.measurementByPOJO(domainClass)
.addFieldsFromPOJO(o)
.build();
pointList.add(point);
}
//获取数据库名和rp
Measurement measurement = firstObj.getClass().getAnnotation(Measurement.class);
String database = measurement.database();
String retentionPolicy = measurement.retentionPolicy();
BatchPoints batchPoints = BatchPoints
.builder()
.points(pointList)
.retentionPolicy(retentionPolicy).build();
influxDB.setDatabase(database);
influxDB.write(batchPoints);
}
}
public void delete(String sql, String database) {
influxDB.query(new Query(sql, database));
}
}
参数处理器 ParameterHandler :用于执行参数的封装处理
java
package org.example.influxDd.core;
import org.example.influxDd.anno.Param;
import java.lang.reflect.Parameter;
/**
* 参数处理器
*/
public class ParameterHandler {
/**
* 拼接sql
*
* @param parameters 参数名
* @param args 参数实际值
* @param sql 未拼接参数的sql语句
* @return 拼接好的sql
*/
public String handleParameter(Parameter[] parameters, Object[] args, String sql) {
for (int i = 0; i < parameters.length; i++) {
Class<?> parameterType = parameters[i].getType();
String parameterName = parameters[i].getName();
Param param = parameters[i].getAnnotation(Param.class);
if (param != null) {
parameterName = param.value();
}
if (parameterType == String.class) {
sql = sql.replaceAll("\\#\\{" + parameterName + "\\}", "'" + args[i] + "'");
sql = sql.replaceAll("\\$\\{" + parameterName + "\\}", args[i].toString());
} else {
sql = sql.replaceAll("\\#\\{" + parameterName + "\\}", args[i].toString());
sql = sql.replaceAll("\\$\\{" + parameterName + "\\}", args[i].toString());
}
}
return sql;
}
}
参数处理器配合参数注解使用:
java
package org.example.influxDd.anno;
import java.lang.annotation.*;
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.PARAMETER})
@Documented
public @interface Param {
String value();
}
结果处理器ResultSetHandler :用于执行结构的封装处理
java
package org.example.influxDd.core;
import lombok.SneakyThrows;
import org.example.influxDd.util.BeanConv;
import org.example.influxDd.util.EmptyUtil;
import java.lang.reflect.Constructor;
import java.lang.reflect.Method;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* 结果集处理器
*/
public class ResultSetHandler {
/***
* @Description 结果处理
*
* @param reultList influx返回结果
* @param method 目标方法
* @param sql 执行sql
* @param resultType 注解声明返回类型
* @return
* java.lang.Object
*/
@SneakyThrows
public Object handleResultSet(List<Map<String,Object>> reultList, Method method, String sql, Class<?> resultType) {
Class<?> returnTypeTarget = method.getReturnType();
//如果结果为空直接返回空构建
if (EmptyUtil.isNullOrEmpty(reultList)){
if (returnTypeTarget== List.class){
return new ArrayList<>();
}else if (returnTypeTarget==Map.class){
return new HashMap<>();
}else if (returnTypeTarget==String.class){
return null;
}else {
return convertStringToObject(resultType,"0");
}
}
//当前method声明返回结果不为list,且resultType与method声明返回结果类型不匹配
if (returnTypeTarget!= List.class&&resultType!=returnTypeTarget){
throw new RuntimeException("返回类型与声明返回类型不匹配");
}
//当前method声明返回结果不为list,且resultType与method声明返回结果类型匹配
if (returnTypeTarget!= List.class&&resultType==returnTypeTarget){
//结果不唯一则抛出异常
if (reultList.size()!=1){
throw new RuntimeException("返回结果不唯一");
}
//驼峰处理
Map<String, Object> mapHandler = convertKeysToCamelCase(reultList.get(0));
//单个Map类型
if (resultType==Map.class){
return mapHandler;
//单个自定义类型
} else if (!isTargetClass(resultType)){
return BeanConv.toBean(mapHandler, resultType);
//单个JDK提供指定类型
}else {
if (mapHandler.size()!=2){
throw new RuntimeException("返回结果非单值");
}
for (String key : mapHandler.keySet()) {
if (!key.equals("time")&&!EmptyUtil.isNullOrEmpty((mapHandler.get(key)))){
String target = String.valueOf(mapHandler.get(key)).replace(".0","");
return convertStringToObject(resultType,target);
}
}
}
}
//当前method声明返回结果为list
if (returnTypeTarget== List.class){
//驼峰处理
List<Map<String, Object>> listHandler = convertKeysToCamelCase(reultList);
//list的内部为map结果
if (resultType==Map.class){
return listHandler;
//list的内部为自定义类型
}else if (!isTargetClass(resultType)){
return BeanConv.toBeanList(listHandler, resultType);
//list的内部为JDK提供指定类型
}else {
List<Object> listResult = new ArrayList<>();
listHandler.forEach(mapHandler->{
if (mapHandler.size()!=2){
throw new RuntimeException("返回结果非单值");
}
for (String key : mapHandler.keySet()) {
if (!key.equals("time")&&!EmptyUtil.isNullOrEmpty((mapHandler.get(key)))){
String target = String.valueOf(mapHandler.get(key)).replace(".0","");
listResult.add(convertStringToObject(resultType,target));
}
}
});
return listResult;
}
}
return null;
}
// 检查类是否是目标类型
public static boolean isTargetClass(Class<?> clazz) {
return clazz == Integer.class ||
clazz == int.class ||
clazz == Long.class ||
clazz == long.class ||
clazz == Float.class ||
clazz == float.class ||
clazz == Double.class ||
clazz == double.class ||
clazz == Short.class ||
clazz == short.class ||
clazz == Byte.class ||
clazz == byte.class ||
clazz == Character.class ||
clazz == char.class ||
clazz == Boolean.class||
clazz == boolean.class||
clazz== BigDecimal.class ||
clazz== String.class;
}
public static Map<String, Object> convertKeysToCamelCase(Map<String, Object> map) {
Map<String, Object> camelCaseMap = new HashMap<>();
for (Map.Entry<String, Object> entry : map.entrySet()) {
String originalKey = entry.getKey();
Object value = entry.getValue();
String camelCaseKey = convertToCamelCase(originalKey);
camelCaseMap.put(camelCaseKey, value);
}
return camelCaseMap;
}
public static List<Map<String, Object>> convertKeysToCamelCase(List<Map<String, Object>> mapList) {
List<Map<String, Object>> listHandler = new ArrayList<>();
mapList.forEach(n->{
listHandler.add(convertKeysToCamelCase(n));
});
return listHandler;
}
public static String convertToCamelCase(String snakeCase) {
StringBuilder camelCase = new StringBuilder();
boolean nextUpperCase = false;
for (int i = 0; i < snakeCase.length(); i++) {
char currentChar = snakeCase.charAt(i);
if (currentChar == '_') {
nextUpperCase = true;
} else {
if (nextUpperCase) {
camelCase.append(Character.toUpperCase(currentChar));
nextUpperCase = false;
} else {
camelCase.append(Character.toLowerCase(currentChar));
}
}
}
return camelCase.toString();
}
@SneakyThrows
public static <T> T convertStringToObject(Class<?> clazz, String str){
if (clazz == String.class) {
return (T)str; // 如果目标类型是 String,则直接返回字符串
} else if (isTargetClass(clazz)){
// 获取目标类型的构造函数,参数为 String 类型的参数
Constructor<?> constructor = clazz.getConstructor(String.class);
return (T)constructor.newInstance(str); // 使用构造函数创建目标类型的对象
}else {
return (T)clazz.newInstance();
}
}
}
3_切面处理
下面我们来看下第三步切面处理:业务系统service调用业务Mapper时,influxDBAspect会对被@ S e l e c t Select Select或 @ I n s e r t @Insert @Insert注解的方法进行切面处理,封装构建参数处理器,然后通过业务执行器请求influxDB,最后交于结果处理器来封装数据。在进行前面之前我们定义了2个注解@Select 和@Insert内容如下:
java
package org.example.influxDd.anno;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Select {
//执行的influxQL
String value();
//返回的类型
Class resultType();
//执行的目标库
String database();
}
Insert:
java
package org.example.influxDd.anno;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Insert {
}
当Mapper中的方法被调用时,会被定义的InfluxDBAspect切面拦截处理:
java
package org.example.influxDd.aspect;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.reflect.MethodSignature;
import org.example.influxDd.anno.Insert;
import org.example.influxDd.anno.Select;
import org.example.influxDd.core.Executor;
import org.example.influxDd.core.ParameterHandler;
import org.example.influxDd.core.ResultSetHandler;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.lang.reflect.Method;
import java.lang.reflect.Parameter;
import java.util.List;
import java.util.Map;
/**
* @ClassName InfluxDBAspect.java
* @Description 拦截influxDb操作
*/
@Aspect
@Component
public class InfluxDBAspect {
private final Executor executor;
private final ParameterHandler parameterHandler;
private final ResultSetHandler resultSetHandler;
@Autowired
public InfluxDBAspect(Executor executor, ParameterHandler parameterHandler, ResultSetHandler resultSetHandler) {
this.executor = executor;
this.parameterHandler = parameterHandler;
this.resultSetHandler = resultSetHandler;
}
@Around("@annotation(select)")
public Object select(ProceedingJoinPoint joinPoint, Select select) {
MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
Method method = methodSignature.getMethod();
Select selectAnnotation = method.getAnnotation(Select.class);
//获得执行参数
Parameter[] parameters = method.getParameters();
//获得执行参数值
Object[] args = joinPoint.getArgs();
//获得执行sql
String sql = selectAnnotation.value();
//替换参数
sql = parameterHandler.handleParameter(parameters,args,sql);
//注解声明返回类型
Class<?> resultType = selectAnnotation.resultType();
//查询结果
List<Map<String,Object>> reultList = executor.select(sql,selectAnnotation.database());
//根据返回类型返回结果
return resultSetHandler.handleResultSet(reultList, method,sql,resultType);
}
@Around("@annotation(insert)")
public void insert(ProceedingJoinPoint joinPoint, Insert insert) {
//获得执行参数值
Object[] args = joinPoint.getArgs();
executor.insert(args);
}
}
当切面select方法处理就可以通过反射拿到参数、sql、返回类型,然后通过 executor 来进行执行对应查询,而executor 中通过 parameterHandler 参数处理器解析参数,最后通过 resultSetHandler 结果处理器完成结果的处理。
4_其他工具的封装以及依赖
项目的结构如下:
shell
framework-influxdb
└─src
├─main
├─java
│ └─org
│ └─example
│ └─influxDd
│ ├─anno
│ ├─aspect
│ ├─config
│ ├─core
│ └─util
└─resources
└─META-INF
操作influxDB的基础接口:
java
package org.example.influxDd;
import org.example.influxDd.anno.Insert;
import java.util.List;
public interface InfluxDBBaseMapper<T> {
@Insert
void insertOne(T entity);
@Insert
void insertBatch(List<T> entityList);
}
对象转换工具
java
package org.example.influxDd.util;
//这里想要使用mp的分页进行完善,不过还没有完成,可删除
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import lombok.extern.slf4j.Slf4j;
import ma.glasnost.orika.MapperFacade;
import ma.glasnost.orika.MapperFactory;
import ma.glasnost.orika.MappingContext;
import ma.glasnost.orika.converter.BidirectionalConverter;
import ma.glasnost.orika.converter.ConverterFactory;
import ma.glasnost.orika.impl.DefaultMapperFactory;
import ma.glasnost.orika.metadata.Type;
import org.springframework.beans.BeanUtils;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.util.ArrayList;
import java.util.List;
/***
* @description 对象转换工具,当对象成员变量属性:名称及类型相同时候会自动
* 填充其值
*
*/
@Slf4j
public class BeanConv {
private static MapperFacade mapper;
private static MapperFacade notNullMapper;
static {
MapperFactory mapperFactory = new DefaultMapperFactory.Builder().build();
ConverterFactory converterFactory = mapperFactory.getConverterFactory();
converterFactory.registerConverter(new LocalDateTimeConverter());
converterFactory.registerConverter(new LocalDateConverter());
converterFactory.registerConverter(new LocalTimeConverter());
mapper = mapperFactory.getMapperFacade();
MapperFactory notNullMapperFactory = new DefaultMapperFactory.Builder().mapNulls(false).build();
notNullMapper = notNullMapperFactory.getMapperFacade();
}
private static class LocalDateTimeConverter extends BidirectionalConverter<LocalDateTime, LocalDateTime> {
@Override
public LocalDateTime convertTo(LocalDateTime localDateTime, Type<LocalDateTime> type, MappingContext mappingContext) {
return LocalDateTime.from(localDateTime);
}
@Override
public LocalDateTime convertFrom(LocalDateTime localDateTime, Type<LocalDateTime> type, MappingContext mappingContext) {
return LocalDateTime.from(localDateTime);
}
}
private static class LocalDateConverter extends BidirectionalConverter<LocalDate, LocalDate> {
@Override
public LocalDate convertTo(LocalDate localDate, Type<LocalDate> type, MappingContext mappingContext) {
return LocalDate.from(localDate);
}
@Override
public LocalDate convertFrom(LocalDate localDate, Type<LocalDate> type, MappingContext mappingContext) {
return LocalDate.from(localDate);
}
}
private static class LocalTimeConverter extends BidirectionalConverter<LocalTime, LocalTime> {
@Override
public LocalTime convertTo(LocalTime localTime, Type<LocalTime> type, MappingContext mappingContext) {
return LocalTime.from(localTime);
}
@Override
public LocalTime convertFrom(LocalTime localTime, Type<LocalTime> type, MappingContext mappingContext) {
return LocalTime.from(localTime);
}
}
/**
* @Description 异常转换工具
*/
static class ExceptionsUtil {
/**
*
* <b>方法名:</b>:getStackTraceAsString<br>
* <b>功能说明:</b>:将ErrorStack转化为String<br>
*/
public static String getStackTraceAsString(Exception e) {
StringWriter stringWriter = new StringWriter();
e.printStackTrace(new PrintWriter(stringWriter));
return stringWriter.toString();
}
}
/**
* 分页对象复制
* @param source 源对象
* @param destinationClass 目标对象类型
*/
public static <S,D> Page<D> toPage(Page<S> source, Class<D> destinationClass) {
if (EmptyUtil.isNullOrEmpty(source)){
return null;
}
Class<? extends Page> handlerClass = source.getClass();
Page<D> destination = mapper.map(source, handlerClass);
destination.setRecords(mapper.mapAsList(source.getRecords(),destinationClass));
return destination;
}
/***
* @description 深度复制对象
*
* @param source 源对象
* @param destinationClass 目标类型
* @return
*/
public static <T> T toBean(Object source, Class<T> destinationClass) {
if (EmptyUtil.isNullOrEmpty(source)){
return null;
}
return mapper.map(source, destinationClass);
}
/***
* @description 深度复制对象
*
* @param source 源对象
* @param destinationClass 目标类型
* @return
*/
public static <T> T toBean(Object source, Class<T> destinationClass, String... fieldsToIgnore) {
try {
T t = destinationClass.getDeclaredConstructor().newInstance();
BeanUtils.copyProperties(source, t, fieldsToIgnore);
return t;
}catch (Exception e){
ExceptionsUtil.getStackTraceAsString(e);
return null;
}
}
/***
* @description 复制List
*
* @param sourceList 源list对象
* @param destinationClass 目标类型
* @return
*/
public static <T> List<T> toBeanList(List<?> sourceList, Class<T> destinationClass) {
if (EmptyUtil.isNullOrEmpty(sourceList)){
return new ArrayList<>();
}
return mapper.mapAsList(sourceList,destinationClass);
}
}
判空工具:
java
package org.example.influxDd.util;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Map;
/**
* @Description 判断对象是否为空的工具类
*/
public abstract class EmptyUtil {
/***
* @description 对string字符串是否为空判断
*
* @param str 被判定字符串
* @return
*/
public static boolean isNullOrEmpty(String str) {
if (str == null || "".equals(str.trim()) || "null".equalsIgnoreCase(str.trim()) || "undefined".equalsIgnoreCase(str.trim())) {
return true;
} else {
return false;
}
}
/***
* @description 对于StringBuffer类型的非空判断
*
* @param str 被判定StringBuffer
* @return
*/
public static boolean isNullOrEmpty(StringBuffer str) {
return (str == null || str.length() == 0);
}
/***
* @description 对于string数组类型的非空判断
*
* @param str 被判定字符串数组
* @return
*/
public static boolean isNullOrEmpty(String[] str) {
if (str == null || str.length == 0) {
return true;
} else {
return false;
}
}
/***
* @description 对于Object类型的非空判断
*
* @param obj 被判定对象
* @return
*/
public static boolean isNullOrEmpty(Object obj) {
if (obj == null || "".equals(obj)) {
return true;
} else {
return false;
}
}
/***
* @description 对于Object数组类型的非空判断
*
* @param obj 被判定对象数组
* @return
*/
public static boolean isNullOrEmpty(Object[] obj) {
if (obj == null || obj.length == 0) {
return true;
} else {
return false;
}
}
/***
* @description 对于Collection类型的非空判断
*
* @param collection 被判定Collection类型对象
* @return
*/
public static boolean isNullOrEmpty(Collection collection) {
if (collection == null || collection.isEmpty()) {
return true;
} else {
return false;
}
}
/**
* @方法名:对于Map类型的非空判断
* @功能说明:对于Map类型的非空判断
* @return boolean true-为空,false-不为空
* @throws
*/
@SuppressWarnings("rawtypes")
public static boolean isNullOrEmpty( Map map) {
if (map == null || map.isEmpty()) {
return true;
} else {
return false;
}
}
/**
*
* @方法名:removeNullUnit
* @功能说明: 删除集合中的空元素
* @return
*/
public static <T> List<T> removeNullUnit(List<T> xllxList) {
List<T> need = new ArrayList<T>();
for (int i = 0; i < xllxList.size(); i++) {
if (!isNullOrEmpty(xllxList.get(i))) {
need.add(xllxList.get(i));
}
}
return need;
}
}
使用的关键依赖(除Spring框架外):
xml
<dependency>
<groupId>org.influxdb</groupId>
<artifactId>influxdb-java</artifactId>
<version>2.24</version>
</dependency>
<!--orika 拷贝工具 -->
<dependency>
<groupId>ma.glasnost.orika</groupId>
<artifactId>orika-core</artifactId>
<version>1.5.4</version>
</dependency>
<!-- 切面-->
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
</dependency>
5_使用
经过上面的一系列封装,我们就可以使用类似mybatis注解的方式操作influxDB了。
我们来看下他在业务mapper中的使用:
java
import org.apache.ibatis.annotations.Mapper;
import org.example.influxDd.InfluxDBBaseMapper;
import org.example.influxDd.anno.Param;
import org.example.influxDd.anno.Select;
import java.util.List;
/**
* BusinessLogMapper
*
* @describe: 数据埋点日志持久层(influxDB)
* @date: 2024/10/8 20:10
*/
@Mapper
public interface BusinessLogMapper extends InfluxDBBaseMapper {
/**
* 每日新注册用户
* @param begin
* @param end
* @return
*/
@Select(value = "SELECT * FROM log WHERE response_code = '200' and time > #{begin} and time < #{end} and request_uri =~/register-user/",
resultType = BusinessLog.class,database = "point_data")
List<BusinessLog> dnu(@Param("begin")String begin, @Param("end")String end);
}
Mock 的实体类对象(略)
6、总结
Influx官网上有更加详细的说明,比如:相关文档、自研TSM存储引擎的发展过程和选择转变的思考等。