掘金 后端 ( ) • 2024-05-16 10:41

概述:

Redisson 的 RTimeSeries 并不是 Redis 原生支持的数据类型,而是 Redisson 在客户端层面实现的一种抽象,它利用了 Redis 提供的数据结构,特别是有序集合(sorted sets),来模拟时间序列数据的存储和管理,它是对 Redis 时间序列(TimeSeries)数据类型的封装和扩展。

原理

RTimeSeries 对象允许用户在 Redis 中存储和检索与时间戳相关联的数据点。它利用 Redis 的有序集合(sorted sets)来维护时间序列数据,其中时间戳作为分数(score)使用,而数据点的值则作为成员(member)存储。这允许快速检索特定时间范围内的数据点。

基本操作包括:

  • 添加数据点:将带有时间戳的数据点添加到时间序列中。
  • 获取数据点:检索时间序列中的数据点,可以通过时间范围或最近的数据点数量来指定。
  • 删除数据点:从时间序列中删除数据点。

优点:

  1. 高性能:Redisson 利用 Redis 的高性能特性,提供快速的数据插入和检索操作,这对于时间敏感的应用来说非常重要。
  2. 易于使用:Redisson 提供了一套简洁的 API 来处理时间序列数据,使得开发者可以方便地进行添加、查询和删除操作。
  3. 自动清理RTimeSeries 支持配置自动清理旧数据的功能,这有助于节省存储空间并维护时间序列数据的当前性。
  4. 可扩展性:作为一个分布式数据库,Redis 可以通过集群来扩展,从而支持更大规模的数据存储和更高的吞吐量。
  5. 灵活的时间范围查询RTimeSeries 支持按时间范围查询数据点,这对于时间序列分析至关重要。
  6. 支持多种数据过期策略:可以基于时间或数据点的数量来设置数据的过期策略。

缺点:

  1. 内存成本:Redis 是基于内存的存储系统,因此存储大量时间序列数据可能会占用大量内存,导致成本增加。
  2. 数据持久性:尽管 Redis 支持数据持久化,但默认情况下它是一个内存中的数据结构,如果没有正确配置持久化,可能会在系统故障时丢失数据。
  3. 数据安全性:与传统的数据库相比,Redis 提供的安全性和事务保证较弱,可能不适合对数据一致性要求非常高的应用。
  4. 复杂查询的限制:Redisson 的 RTimeSeries 提供了基本的时间序列数据管理功能,但对于复杂的数据分析和查询可能不如专门的时间序列数据库强大。
  5. 数据备份和恢复:对于大规模的时间序列数据,Redis 的备份和恢复操作可能会比较耗时和复杂。 Redisson 的 RTimeSeries 适用于需要存储和分析随时间变化的数据的场景。

使用场景:

  1. 监控和度量指标:在系统监控中,可以使用 RTimeSeries 来存储服务器的性能指标(如CPU使用率、内存使用、网络流量等)的时间序列数据。
  2. 金融市场数据:金融应用可以利用 RTimeSeries 来存储股票价格、交易量、市场指数等金融时序数据,以便进行实时分析和决策支持。
  3. 物联网(IoT)数据:物联网设备经常生成大量的时间序列数据,如传感器读数。RTimeSeries 可以用来存储这些数据,并支持实时查询和分析。
  4. 用户行为分析:在用户分析中,RTimeSeries 可用于记录用户活动的时间序列数据,比如网站的点击率、应用的使用频率等。
  5. 日志数据:可以使用 RTimeSeries 存储应用程序或系统日志中的事件时间戳,以便于进行时间相关的日志分析。
  6. 实时分析和报警RTimeSeries 适合于需要对数据进行实时分析并触发报警的场景,如实时监控系统状态并在特定条件下发送警报。
  7. 科学研究:在科学研究中,RTimeSeries 可以用来存储实验数据或观测数据,如天文观测数据、气候变化数据等。

时序图:

sequenceDiagram
    participant Client as Client Application
    participant Redisson as RedissonClient
    participant RTimeSeries as RTimeSeries<V, L>
    
    Client->>+Redisson: Connect to Redis
    Redisson->>+RTimeSeries: Create time series instance
    RTimeSeries-->>-Redisson: Instance created
    
    Client->>+RTimeSeries: add(timestamp, value, label)
    RTimeSeries-->>-Client: Data point added
    
    Client->>+RTimeSeries: range(startTime, endTime)
    RTimeSeries-->>-Client: Return data points
    
    Client->>+RTimeSeries: getLastN(count)
    RTimeSeries-->>-Client: Return last N data points
    
    Client->>+RTimeSeries: removeRange(startTime, endTime)
    RTimeSeries-->>-Client: Range removed
    
    Client->>+RTimeSeries: expire(timeToLive, timeUnit)
    RTimeSeries-->>-Client: Expiration set
    
    Client->>+Redisson: Disconnect
    Redisson-->>-Client: Disconnected

在这个时序图中,我们展示了 RTimeSeries 类中的一系列操作及其与客户端应用和 Redisson 客户端之间的交互:

  1. 连接到 Redis:客户端应用通过 Redisson 客户端连接到 Redis。
  2. 创建时间序列实例:通过 Redisson 创建 RTimeSeries 实例。
  3. 添加数据点:客户端向时间序列中添加一个数据点。
  4. 获取数据点范围:客户端请求一定时间范围内的数据点。
  5. 获取最后 N 个数据点:客户端请求时间序列中最后 N 个数据点。
  6. 删除数据点范围:客户端删除一定时间范围内的数据点。
  7. 设置过期时间:客户端为时间序列设置过期时间。
  8. 断开连接:客户端应用断开与 Redis 的连接。

特性:

使用 RTimeSeries 的场景通常需要以下特性:

  • 快速插入和读取:对时间序列数据的插入和读取操作需要高效快速。
  • 时间范围查询:根据时间窗口查询数据点是时间序列分析的常见需求。
  • 实时处理:对于需要即时分析和响应的应用,RTimeSeries 提供了必要的实时数据处理能力。
  • 数据有限持久化:对于数据不需要永久保存的情况,RTimeSeries 的自动过期特性非常有用。

在选择 RTimeSeries 之前,应该评估应用场景是否适合在内存数据库中处理时间序列数据,以及是否能够接受 Redis 的成本和持久性限制。

工具类:

import org.redisson.api.RTimeSeries;
import org.redisson.api.RedissonClient;
import org.redisson.api.TimeSeriesEntry;
import org.redisson.client.codec.Codec;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.Collection;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;

/**
 * @Author derek_smart
 * @Date 202/5/13 9:38
 * @Description RTimeSeries 工具类
 * <p>
 * 针对 Redisson 时间序列操作的封装工具,它提供了一系列方法来处理时间序列数据
 */
public class TimeSeriesTool<V, L> {

    private static final Logger logger = LoggerFactory.getLogger(TimeSeriesTool.class);

    private final RedissonClient redissonClient;
    // 时间序列的键名
    private final String timeSeriesKey;
    // 数据编解码器
    private final Codec codec;

    public TimeSeriesTool(RedissonClient redissonClient, String timeSeriesKey, Codec codec) {
        this.redissonClient = redissonClient;
        this.timeSeriesKey = timeSeriesKey;
        this.codec = codec;
    }

    private RTimeSeries<V, L> getTimeSeries() {
        return redissonClient.getTimeSeries(timeSeriesKey, codec);
    }
    /**
     * 方法用于向时间序列添加一个带有时间戳和标签的数据点
     *
     * @param timestamp
     * @param value
     * @param label
     */
    public void add(long timestamp, V value, L label) {
        try {
            RTimeSeries<V, L> timeSeries = getTimeSeries();
            timeSeries.add(timestamp, value, label);
        } catch (Exception e) {
            logger.error("Failed to add value to time series", e);
        }
    }

    /**
     * 检索在指定开始时间和结束时间之间的所有数据点。它返回一个 `TimeSeriesEntry` 集合,每个 `TimeSeriesEntry` 包含一个数据点的时间戳和值。
     *
     * @param startTime
     * @param endTime
     * @return
     */
    public Collection<TimeSeriesEntry<V, L>> getRange(long startTime, long endTime) {
        try {
            RTimeSeries<V, L> timeSeries = getTimeSeries();
            return (Collection<TimeSeriesEntry<V, L>>) timeSeries.range(startTime, endTime);
        } catch (Exception e) {
            logger.error("Failed to retrieve range from time series", e);
            return null;
        }
    }

    /**
     * 删除在指定开始时间和结束时间之间的所有数据点。
     *
     * @param startTime
     * @param endTime
     */
    public void removeRange(long startTime, long endTime) {
        try {
            RTimeSeries<V, L> timeSeries = getTimeSeries();
            timeSeries.removeRange(startTime, endTime);
        } catch (Exception e) {
            logger.error("Failed to remove range from time series", e);
        }
    }

    /***
     *
     * 方法假设时间序列中的数据点是连续存储的,且没有间隙。如果时间序列数据点之间存在间隙,那么这种方法可能不会返回正确数量的最新数据点。
     * 此外,如果时间序列非常大,获取其大小可能会有性能影响。在这种情况下,可能需要考虑其他策略,如维护一个单独的计数器或使用其他 Redis 数据结构。
     * @param count
     * @return
     */
    public Collection<TimeSeriesEntry<V, L>> getLastN(int count) {
        try {
            RTimeSeries<V, L> timeSeries = getTimeSeries();
            long size = timeSeries.size(); // 获取时间序列当前的大小
            long startIndex = size - count; // 计算起始索引
            if (startIndex < 0) {
                startIndex = 0; // 确保起始索引不小于0
            }
            // 使用计算出的起始索引和结束索引来获取数据点
            return (Collection<TimeSeriesEntry<V, L>>) timeSeries.range(startIndex, size - 1);
        } catch (Exception e) {
            logger.error("Failed to retrieve last N entries from time series", e);
            return null;
        }
    }

    // 异步 getLastN
    public CompletableFuture<Collection<TimeSeriesEntry<V, L>>> getLastNAsync(int count) {
        return CompletableFuture.supplyAsync(() -> getLastN(count), Executors.newSingleThreadExecutor());
    }

    /**
     * 设置整个时间序列的过期时间。`timeToLive` 参数指定了过期时间的长度,`timeUnit` 参数指定了时间的单位。
     * @param timeToLive
     * @param timeUnit
     */
    public void setExpiration(long timeToLive, TimeUnit timeUnit) {
        try {
            RTimeSeries<V, L> timeSeries = getTimeSeries();
            timeSeries.expire(timeToLive, timeUnit);
        } catch (Exception e) {
            logger.error("Failed to set expiration for time series", e);
        }
    }

    /**
     * 获取时间序列中的数据点数量。如果操作失败,会记录错误日志并返回 `0`
     * @return
     */
    public long size() {
        try {
            RTimeSeries<V, L> timeSeries = getTimeSeries();
            return timeSeries.size();
        } catch (Exception e) {
            logger.error("Failed to get size of time series", e);
            return 0;
        }
    }
}

1715575941500.png

  • add 方法用于向时间序列添加数据点。
  • getRange 方法用于检索指定时间范围内的数据点。
  • removeRange 方法用于删除指定时间范围内的数据点。
  • getLastN 方法用于获取最近的 N 个数据点。
  • setExpiration 方法用于设置数据点的过期时间。
  • size 方法返回时间序列中的数据点数量。

流程图:

flowchart TB
    start([开始]) --> |创建RTimeSeries实例| createRTimeSeries
    createRTimeSeries --> |添加数据点| addDataPoint
    addDataPoint --> |获取数据点范围| getRange
    addDataPoint --> |获取最后N个数据点| getLastN
    addDataPoint --> |删除数据点范围| removeRange
    addDataPoint --> |设置过期时间| setExpiration
    getRange --> ends([结束])
    getLastN --> ends
    removeRange --> ends
    setExpiration --> ends

RTimeSeries 类中的四个主要操作:

  1. 创建 RTimeSeries 实例:开始使用时间序列之前,需要创建 RTimeSeries 的实例。
  2. 添加数据点:向时间序列中添加带有时间戳的数据点。
  3. 获取数据点范围:获取指定时间范围内的所有数据点。
  4. 获取最后 N 个数据点:获取时间序列中最后 N 个数据点。
  5. 删除数据点范围:删除指定时间范围内的数据点。
  6. 设置过期时间:为整个时间序列设置过期时间。

使用 TimeSeriesTool,使用者可以专注于业务逻辑,而不必担心时间序列数据的底层存储和操作细节。这个工具类适用于需要记录和分析随时间变化的数据的各种应用场景,如金融市场监控、物联网设备数据收集、用户行为跟踪、日志记录和事件监控等。

使用示例:

import org.redisson.Redisson;
import org.redisson.api.RedissonClient;
import org.redisson.codec.JsonJacksonCodec;
import org.redisson.config.Config;
import org.redisson.client.codec.StringCodec;
import org.redisson.api.TimeSeriesEntry;

import java.util.Collection;
import java.util.concurrent.TimeUnit;

/**
 * @Author derek_smart
 * @Date 202/5/13 12:31
 * @Description RTimeSeries 测试类
 * <p>
 * 针对 Redisson 时间序列操作的封装工具,它提供了一系列方法来处理时间序列数据
 * 金融数据追踪
 * 物联网 (IoT) 传感器数据收集
 * 用户活动跟踪
 * 日志数据和事件监控
 */
public class TimeSeriesToolTest {

    public static void main(String[] args) {
        // 创建 Redisson 客户端
        Config config = new Config();
        config.useSingleServer().setAddress("redis://127.0.0.1:6379");
        RedissonClient redissonClient = Redisson.create(config);
        //------------------------ 使用Demo---------------------------------------------S
        // 创建 TimeSeriesTool 实例,使用 String 作为值和标签的类型
        String timeSeriesKey = "sampleTimeSeries";
        TimeSeriesTool<String, String> timeSeriesTool = new TimeSeriesTool<>(
                redissonClient,
                timeSeriesKey,
                StringCodec.INSTANCE
        );

        // 添加一些数据点到时间序列
        long timestamp = System.currentTimeMillis();
        timeSeriesTool.add(timestamp, "value1", "label1");
        timeSeriesTool.add(timestamp + 1000, "value2", "label2");
        timeSeriesTool.add(timestamp + 2000, "value3", "label3");

        // 获取时间序列的大小
        long size = timeSeriesTool.size();
        System.out.println("Size of time series: " + size);

        // 检索时间序列中的所有数据点
        Collection<TimeSeriesEntry<String, String>> entries = timeSeriesTool.getRange(0, Long.MAX_VALUE);
        if (entries != null) {
            for (TimeSeriesEntry<String, String> entry : entries) {
                System.out.println("Timestamp: " + entry.getTimestamp() + ", Value: " + entry.getValue());
            }
        }

        // 获取最后两个数据点
        Collection<TimeSeriesEntry<String, String>> lastTwoEntries = timeSeriesTool.getLastN(2);
        if (lastTwoEntries != null) {
            for (TimeSeriesEntry<String, String> entry : lastTwoEntries) {
                System.out.println("Last entries - Timestamp: " + entry.getTimestamp() + ", Value: " + entry.getValue());
            }
        }

        // 设置时间序列的过期时间
        timeSeriesTool.setExpiration(10, TimeUnit.MINUTES);

        // 删除时间序列中的一个范围内的数据点
        timeSeriesTool.removeRange(timestamp, timestamp + 1500);

        // 关闭 Redisson 客户端
        redissonClient.shutdown();
        //------------------------ 使用Demo---------------------------------------------S



//------------------------金融数据追踪---------------------------------------------S
        // 假设我们有一个股票价格的时间序列
        String stockPriceTimeSeriesKey = "stockPrice:APPL";
        TimeSeriesTool<Double, String> stockPriceTimeSeries = new TimeSeriesTool<>(
                redissonClient, stockPriceTimeSeriesKey, new JsonJacksonCodec());

        // 添加股票价格数据点
        stockPriceTimeSeries.add(System.currentTimeMillis(), 150.25, "APPL");

        // 获取最近一小时内的股票价格数据
        long oneHourAgo = System.currentTimeMillis() - TimeUnit.HOURS.toMillis(1);
        Collection<TimeSeriesEntry<Double, String>> lastHourPrices = stockPriceTimeSeries.getRange(oneHourAgo, System.currentTimeMillis());
//----------------------------金融数据追踪-----------------------------------------E


        //------------------------物联网 (IoT) 传感器数据收集---------------------------------------------S

//物联网设备和传感器经常需要收集时间序列数据,如温度、湿度或运动数据。

        String temperatureTimeSeriesKey = "sensor:temperature:1234";
        TimeSeriesTool<Float, String> temperatureTimeSeries = new TimeSeriesTool<>(
                redissonClient, temperatureTimeSeriesKey, new JsonJacksonCodec());
        // 添加温度读数
        temperatureTimeSeries.add(System.currentTimeMillis(), 22.5f, "sensor1234");

// 获取过去24小时内的温度数据
        long twentyFourHoursAgo = System.currentTimeMillis() - TimeUnit.DAYS.toMillis(1);
        Collection<TimeSeriesEntry<Float, String>> lastDayTemperatures = temperatureTimeSeries.getRange(twentyFourHoursAgo, System.currentTimeMillis());
        //------------------------物联网 (IoT) 传感器数据收集---------------------------------------------E


        //------------------------ 用户活动跟踪---------------------------------------------S
        //网站或应用程序可以使用时间序列工具来追踪用户活动,例如页面访问、点击或者交互事件。

// 假设我们追踪用户的页面访问时间序列
        String userActivityTimeSeriesKey = "userActivity:userId123";
        TimeSeriesTool<String, String> userActivityTimeSeries = new TimeSeriesTool<>(
                redissonClient, userActivityTimeSeriesKey, new JsonJacksonCodec());

// 添加用户访问页面的事件
        userActivityTimeSeries.add(System.currentTimeMillis(), "visited_home_page", "userId123");

// 获取用户最近10次活动
        Collection<TimeSeriesEntry<String, String>> lastTenActivities = userActivityTimeSeries.getLastN(10);


        //------------------------ 用户活动跟踪---------------------------------------------E


        //------------------------ 日志数据和事件监控---------------------------------------------S
        //时间序列工具可以用来存储日志数据和监控系统事件,以便于分析和故障排查。

        // 假设我们存储系统日志的时间序列
        String systemLogTimeSeriesKey = "systemLogs:server1";
        TimeSeriesTool<String, String> systemLogTimeSeries = new TimeSeriesTool<>(
                redissonClient, systemLogTimeSeriesKey, new JsonJacksonCodec());

// 添加系统日志事件
        systemLogTimeSeries.add(System.currentTimeMillis(), "ERROR: Out of memory", "server1");

// 获取最近100条日志
        Collection<TimeSeriesEntry<String, String>> lastHundredLogs = systemLogTimeSeries.getLastN(100);
        //------------------------ 日志数据和事件监控---------------------------------------------E


    }
}

以上工具针对使用场景:

* 金融数据追踪
* 物联网 (IoT) 传感器数据收集
* 用户活动跟踪
* 日志数据和事件监控

企业微信截图_17155760043501.png

企业微信截图_17155760233872.png

总结:

在使用 RTimeSeries 时,应根据应用场景的具体需求权衡其优缺点。对于需要快速访问和实时分析的时间序列数据,RTimeSeries 提供了一个强大的解决方案。然而,对于需要长期存储大量历史数据的应用,可能需要考虑其他更适合大数据量持久存储的时间序列数据库解决方案。