# 14 如何在 Redis 中保存時間序列數據? ## 時間序列數據(Time Series Data) 時間序列數據(Time Series Data)是依時間先後順序排列的數據集合,通常用來表示某個變數隨時間變化的情況。 特點: 1. 時間順序重要 1. 數據點具備時間戳(例如:每天的氣溫、每分鐘的股價) 1. 常用於分析趨勢、季節性、異常等 常見應用: - 股票市場價格 - 氣象資料 - IoT 裝置感測數據 - 網站流量記錄 簡單來說,就是「帶有時間的數據」,重點在於時間與數值的關聯性。 ## 時間序列數據的讀寫特點 寫入特點(Write): 1. 追加為主(Append-only) - 資料通常是依時間不斷新增(例如:每秒一筆感測資料)。 2. 高頻率寫入 - 很多時間序列應用(如 IoT、金融)每秒可產生大量數據。 3. 時間戳不可或缺 - 每筆資料都需要明確的時間戳記。 4. 批次寫入效率高 - 常用批量(batch)寫入提升效能。 5. 通常資料寫入後就不會變了, 因為它就代表了一個設備在某個時刻的狀態值 讀取特點(Read): 1. 範圍查詢為主(Time-range query) - 例如查「某裝置在過去一小時的數據」。 2. 聚合操作常見 - 如平均、最大值、最小值(用於顯示圖表、統計分析)。 3. 順序讀取效率高 - 資料已按時間排序,可快速掃描。 4. 常與可視化結合 - 前端圖表(折線圖、K線圖)經常讀時間序列資料來繪圖。 ## 基於 Hash 和 Sorted Set 保存時間序列數據 ### Hash ![CleanShot 2025-05-21 at 23.34.13@2x](https://hackmd.io/_uploads/B18FMdjbeg.jpg) ```redis= HGET device:temperature 202008030905 "25.1" HMGET device:temperature 202008030905 202008030907 202008030908 1) "25.1" 2) "25.9" 3) "24.9" ``` Hash 類型有個缺點:它並不支持對數據進行範圍查詢。 如果要對 Hash 類型進行範圍查詢的話,就需要掃描 Hash 集合中的所有數據,再把這些數據取回到客戶端進行排序,然後,才能在客戶端得到所查詢範圍內的數據。顯然,查詢效率很低。 ### Sorted Set ![CleanShot 2025-05-21 at 23.35.15@2x](https://hackmd.io/_uploads/HkM6zOsWeg.jpg) 使用 Sorted Set 保存數據後,我們就可以使用 ZRANGEBYSCORE 命令,按照輸入的最大時間戳和最小時間戳來查詢這個時間範圍內的溫度值了 ```redis= ZRANGEBYSCORE device:temperature 202008030907 202008030910 1) "25.9" 2) "24.9" 3) "25.3" 4) "25.2" ``` ### 如何保證寫入 Hash 和 Sorted Set 是一個原子性的操作呢? ![CleanShot 2025-05-21 at 23.37.11@2x](https://hackmd.io/_uploads/Sk_V7dsWgx.jpg) ```redis= 127.0.0.1:6379> MULTI OK 127.0.0.1:6379> HSET device:temperature 202008030911 26.8 QUEUED 127.0.0.1:6379> ZADD device:temperature 202008030911 26.8 QUEUED 127.0.0.1:6379> EXEC 1) (integer) 1 2) (integer) 1 ``` ### 如何對時間序列數據進行聚合計算? 因為 Sorted Set 只支持範圍查詢,無法直接進行聚合計算,所以,我們只能先把時間範圍內的數據取回到客戶端,然後在客戶端自行完成聚合計算。 這個方法雖然能完成聚合計算,但是會帶來一定的潛在風險,也就是大量數據在 Redis 實例和客戶端間頻繁傳輸,這會和其他操作命令競爭網絡資源,導致其他操作變慢。 ## 基於 RedisTimeSeries 模塊保存時間序列數據 因為 RedisTimeSeries 不屬於 Redis 的內建功能模塊,在使用時,我們需要先把它的源碼單獨編譯成動態鏈接庫 redistimeseries.so,再使用 loadmodule 命令進行加載,如下所示: ``` loadmodule redistimeseries.so ``` ### RedisTimeSeries 的操作命令 主要有五個 1. 用 TS.CREATE 命令創建時間序列數據集合; 1. 用 TS.ADD 命令插入數據; 1. 用 TS.GET 命令讀取最新數據; 1. 用 TS.MGET 命令按標籤過濾查詢數據集合; 1. 用 TS.RANGE 支持聚合計算的範圍查詢。 #### 用 TS.CREATE 命令創建一個時間序列數據集合 ``` TS.CREATE device:temperature RETENTION 600000 LABELS device_id 1 ``` | **部分** | **說明** | | ------------------ | ------------------------------------------------------------ | | TS.CREATE | 建立一個新的時間序列 | | device:temperature | 時間序列的 key 名稱 | | RETENTION 600000 | 設定資料保留期限為 **600,000 毫秒**(= 10 分鐘),過期的資料會被自動刪除 | | LABELS device_id 1 | 給這個時間序列加上一組標籤(key-value),這裡是 device_id=1,可用於過濾與分組查詢 | #### 用 TS.ADD 命令插入數據,用 TS.GET 命令讀取最新數據 ```Reids= TS.ADD device:temperature 1596416700 25.1 1596416700 TS.GET device:temperature 25.1 ``` #### 用 TS.MGET 命令按標籤過濾查詢數據集合 假設我們一共用 4 個集合為 4 個設備保存時間序列數據,設備的 ID 號是 1、2、3、4,我們在創建數據集合時,把 device_id 設置為每個集合的標籤。 我們就可以使用下列 TS.MGET 命令,以及 FILTER ,查詢 device_id 不等於 2 的所有其他設備的數據集合 ```redis= TS.MGET FILTER device_id!=2 1) 1) "device:temperature:1" 2) (empty list or set) 3) 1) (integer) 1596417000 2) "25.3" 2) 1) "device:temperature:3" 2) (empty list or set) 3) 1) (integer) 1596417000 2) "29.5" 3) 1) "device:temperature:4" 2) (empty list or set) 3) 1) (integer) 1596417000 2) "30.1" ``` #### 用 TS.RANGE 支持需要聚合計算的範圍查詢 ```redis= TS.RANGE device:temperature 1596416700 1596417120 AGGREGATION avg 180000 1) 1) (integer) 1596416700 2) "25.6" 2) 1) (integer) 1596416880 2) "25.8" 3) 1) (integer) 1596417060 2) "26.1" ``` | TS.RANGE | 取出某個時間序列的歷史資料(根據時間範圍) | | ---------------------- | ------------------------------------------------------------ | | device:temperature | 要查的時間序列 key | | 1596416700 | 起始時間(timestamp,Unix time) | | 1596417120 | 結束時間(timestamp) | | AGGREGATION avg 180000 | 使用平均值進行聚合,每個區間長度為 180000 毫秒(= 180 秒 = 3 分鐘) | 與使用 Hash 和 Sorted Set 來保存時間序列數據相比,RedisTimeSeries 是專門為時間序列數據訪問設計的擴展模塊,能支持在 Redis 實例上直接進行聚合計算,以及按標籤屬性過濾查詢數據集合,當我們需要頻繁進行聚合計算,以及從大量集合中篩選出特定設備或用戶的數據集合時,RedisTimeSeries 就可以發揮優勢了。