網(wǎng)站首頁 編程語言 正文
Redis - 時間序列數(shù)據(jù)類型的保存方案和消息隊列實現(xiàn)
- 一. 用 Redis 保存時間序列類型數(shù)據(jù)方案
- 1.1 內(nèi)存和范圍查找的支持性問題
- 1.2 聚合操作的支持性問題(僅供參考)
- 二. 用 Redis 實現(xiàn)消息隊列
- 2.1 消息保序的實現(xiàn)
- 2.2 重復消費問題解決
- 2.3 消息可靠性保證
- 2.4 Redis 做中間件的優(yōu)劣勢
一. 用 Redis 保存時間序列類型數(shù)據(jù)方案
我們?nèi)粘i_發(fā)中,有很多這種類似的場景,記錄某一個時刻下,某個目標的相關屬性或者狀態(tài)。 那么常規(guī)的,我們可以用時間戳作為Key
,而這個目標的相關屬性我們可以將其轉化為JSON
串或者通過字符串拼接的方式來保存為String
類型,然后作為Value
。
但是,這樣的存儲是針對于一個時間戳而言的,而實際環(huán)境中,往往需要記錄非常多的這樣的數(shù)據(jù),甚至可能需要對這種數(shù)據(jù)進行統(tǒng)計、聚合、范圍查找等操作。那么可想而知,Redis
中的String
類型,雖然可以提供存儲功能,但是卻難以提供統(tǒng)計、聚合、范圍查找等這些復雜操作。 除此之外,String
類型我在String內(nèi)存開銷問題以及基本/擴展數(shù)據(jù)類型的使用這篇文章有提到,如果存儲的數(shù)據(jù)量太大,那么內(nèi)存的占用是非常龐大的。耗內(nèi)存。
同時用時間戳作為Key
的數(shù)據(jù),往往也有以下特點:
- 數(shù)據(jù)的插入比較頻繁。
- 讀操作的查詢模式的種類比較多(統(tǒng)計、聚合…)。
1.1 內(nèi)存和范圍查找的支持性問題
場景:按照一定的時間間隔記錄某一個設備集群中每臺機器的溫度。
針對內(nèi)存占用問題,我們可以選擇Hash
類型去替代String
。
# 傳統(tǒng)的String類型存儲
set 1659947381000 31
set 1659947392000 35
set 1659947413000 28
# 改為Hash存儲
set temperature 1659947381000 31 1659947392000 35 1659947413000 28
雖然Hash
結構彌補了String
類型在內(nèi)存開銷上的短板。但是僅僅這樣是無法滿足數(shù)據(jù)的一些范圍查詢或者操作的。
那么針對范圍查找問題:因此我們可以再使用Sorted Set
去保存相同的一份數(shù)據(jù)。這里做個解釋,為何要同時用兩個數(shù)據(jù)結構來存儲相同的數(shù)據(jù):
-
Hash
結構:用來提供單值查詢。 -
Sorted Set
:提供范圍查詢。雖然將范圍的前后指定為一個相同的值,看起來像是單個值查詢,但是本質(zhì)上依舊是范圍查詢,效率不如人家Hash
來的快。
那么此時就可以用這樣的命令來進行查找:
zrangebyscore temperature 1659947381000 1659947413000
此外,我們既然使用了兩種數(shù)據(jù)結構來保存相同的數(shù)據(jù),就應該保證數(shù)據(jù)一致性。我們應該保證兩個數(shù)據(jù)結構中的數(shù)據(jù)是完全一樣的,不能出現(xiàn)哪個結構中的數(shù)據(jù)有少或者不一致的情況。因此我們可以在進行數(shù)據(jù)插入的時候,保障兩個操作的原子性。即使用簡單的事務操作:
-
multi
:事務開始。之后的操作將會放入一個隊列中,而不會真正的去執(zhí)行。 -
exec
:事務結束,開始執(zhí)行隊列中的一系列命令。
例如:
multi
hset temperature 1659947381000 25
zadd temperature 1659947381000 25
exec
那么Java
對應的操作就是:
Transaction multi = jedis.multi();
multi.hset("temperature", "1659947381000", "25");
System.out.println("事務執(zhí)行中:hash:" + multi.hget("temperature", "1659947381000"));
multi.zadd("temperatureZSet", 1659947381000L, "25");
System.out.println("事務執(zhí)行中:sorted set:" + multi.zrangeByScore("temperatureZSet", 1659947381000L, 1659947381000L));
multi.exec();
System.out.println("**************事務執(zhí)行完畢******************");
System.out.println("事務執(zhí)行中:hash:" + jedis.hget("temperature", "1659947381000"));
System.out.println("事務執(zhí)行中:sorted set:" + jedis.zrangeByScore("temperatureZSet", 1659947341000L, 1659947392000L));
注意:
hash
和sorted set
兩個集合使用的key
不能是同一個。- 并且事務中不能使用普通的
jedis
對象。multi
對象擁有和jedis
對象同樣的API
操作。因為Redis
中事務開啟后,執(zhí)行的操作是放到隊列中的,并不是馬上執(zhí)行的,因此需要做區(qū)分。
結果如下:
到這里,內(nèi)存問題和范圍查找的問題已經(jīng)解決了。雖然我們用了兩個數(shù)據(jù)類型來保存相同的一份數(shù)據(jù),但是整體的內(nèi)存消耗,是比全部用String
類型存儲要節(jié)省的。那么接下來要解決的,就是聚合操作問題。
備注:
- 到這里為止,如果業(yè)務上只涉及到時序的范圍查找,是可以同時用
Hash
和Sorted Set
去替代傳統(tǒng)的String
的。如果僅僅限于此,我個人建議1.2節(jié)可以不看。 -
Redis
中對于事務的使用,在文章中提到的原子性問題也是有一定缺陷的。因為Redis
中的事務并不像Mysql
那樣,倘若在一個事務中,先后執(zhí)行了A和B操作,但是在執(zhí)行C操作的時候發(fā)生了錯誤,A和B的操作是不會回滾的。 -
Redis
主要還是拿來做緩存比較多,這種專門的時序數(shù)據(jù)處理最好交給專門的時序數(shù)據(jù)庫處理,例如influxDB
。 - 1.2節(jié)內(nèi)容僅供參考,并且實用性和實際操作起來是否簡單這個問題上,有待商榷,因為并不容易實現(xiàn)。(至少我寫這篇文章的時候,關于
RedisTimeSeries
的JavaAPI
操作沒有找到)
1.2 聚合操作的支持性問題(僅供參考)
首先,我們當然可以在客戶端將相關的數(shù)據(jù)全部讀取過來,然后再客戶端自行完成聚合操作。但是倘若有這么幾個點:
- 數(shù)據(jù)量很大。
- 聚合操作的頻率很高。
那么這種情況下,就會有很多請求(包含了大量數(shù)據(jù))在Redis
和客戶端之間來回穿梭,就會造成資源的競爭,降低Redis
的性能。
那么針對聚合操作問題:我們可以使用RedisTimeSeries
,它支持在Redis
實例上對時間維度進行聚合計算。
但是使用這個,卻比較麻煩,需要了解這么幾個點:
-
RedisTimeSeries
是Redis
的擴展模塊,原生Redis
并不支持。 - 使用的時候需要將
Redis
源碼單獨編譯成動態(tài)鏈接庫redistimeseries.so
,再使用loadmodule
命令進行加載。
loadmodule redistimeseries.so
那么針對上述的聚合場景,使用RedisTimeSeries
的大致流程如下:
# 創(chuàng)建時間序列數(shù)據(jù)集合,創(chuàng)建一個key為temperature ,數(shù)據(jù)的有效期為800s。(過后會自動刪除),同時這個集合的標簽屬性uid為1
ts.create temperature retention 800000 labels uid 1
# 插入數(shù)據(jù)
ts.add temperature 1659947381000 25
# 最新數(shù)據(jù)的獲取 ts.get只能返回最新的數(shù)據(jù)
ts.get temperature
# 按照標簽過濾查詢
ts.mget FILTER uid = xxx
# 聚合計算 在[1659947371000 ,1659947381000]范圍內(nèi),按照每180s的時間間隔,對這個時間窗口內(nèi)的數(shù)據(jù)做均值計算
ts.range temperature 1659947371000 1659947381000 AGGREGATION avg 180000
二. 用 Redis 實現(xiàn)消息隊列
前言:Redis
是可以做消息隊列的,但是對于一些不允許出現(xiàn)消息丟失的情形下,例如金融支付操作。不要用Redis
作為中間件,請使用專門的中間件去做存儲。例如Kafka、ActiveMQ、RabbitMQ
等。具體原因下面分析。
首先消息隊列需要解決三個問題:
- 消息保序。
- 消息的重復消費問題。
- 消息的可靠性保證。
2.1 消息保序的實現(xiàn)
那么如何用Redis
作為消息隊列呢?利用Redis
中的List
數(shù)據(jù)結構。
-
List
這個數(shù)據(jù)結構本身就是FIFO
,先進先出的順序對數(shù)據(jù)進行存儲的。 - 實際操作上,生產(chǎn)者通過
lpush
命令將數(shù)據(jù)寫入List
中。消費者端則通過rpop
命令將其彈出。
這是一般的操作。但是光憑這樣的操作并不滿足一個合格的消息中間件具備的條件。因為在生產(chǎn)者向Redis
中寫入數(shù)據(jù)的時候,Redis
并不會主動地通知消費者有新消息寫入了。此時消費者只能通過這樣的偽代碼來實現(xiàn)輪詢:
while(true){
String json = jedis.rpop('key');
process(json);
}
問題:這樣的無限循環(huán),會導致CPU
一直消耗在這里執(zhí)行rpop
命令。造成性能損失。
解決:建議使用brpop
命令,即阻塞式讀取,客戶端在沒有讀到隊列數(shù)據(jù)時,自動阻塞,直到有新的數(shù)據(jù)寫入隊列,再開始讀取新數(shù)據(jù)。
2.2 重復消費問題解決
對于消息的重復消費問題,我們只需要提供一個唯一標識,然后消費的時候做判斷即可。
- 生產(chǎn)者端:發(fā)送消息的時候,給消息里面塞一個唯一標識。
- 消費者端:將消費完成的消息的唯一標識記錄下來。在后續(xù)消費的時候,都要反查一遍先。
# 唯一標識:主題:內(nèi)容
lpush key 1000001:title:helloworld
2.3 消息可靠性保證
背景:當消費者程序從Redis
中讀取一條消息并做處理,但是還沒處理完成的時候就發(fā)生了宕機,那么Redis
中這條數(shù)據(jù)已經(jīng)被剔除,但這個數(shù)據(jù)并沒有被真正的消費掉。怎么辦?
解決:生產(chǎn)者在推消息給Redis
的時候,使用 BRPOPLPUSH
命令,其作用如下:
- 在生產(chǎn)者推消息的時候,
Redis
會把這個消息再插入到另一個List
留存。 - 這樣一來,如果消費者程序讀了消息但沒能正常處理,等它重啟后,就可以從備份
List
中重新讀取消息并處理。
綜上所述,常規(guī)情況下:
- 生產(chǎn)者端使用
BRPOPLPUSH
命令往Redis
中推數(shù)據(jù),同時塞入唯一標識。 - 消費者端使用
brpop
命令。防止無限循環(huán)調(diào)用rpop()
命令。將消費過的消息的唯一標識做數(shù)據(jù)存儲。 - 消費者倘若消費某個消息成功,由于生產(chǎn)者端往兩個
List
都插入了數(shù)據(jù),此時最好將備份隊列中的消息刪除,避免備份隊列中存儲過多過期數(shù)據(jù),造成內(nèi)存浪費。
2.4 Redis 做中間件的優(yōu)劣勢
先來說下Redis
做中間件的優(yōu)勢:
- 用
Redis
作為消息隊列,由于Redis
的特性,在內(nèi)存上操作,因此性能高。 -
API
操作起來非常方便,沒有復雜的操作,部署輕量。Kafka
的操作相比之下就會復雜許多。維護成本也要更高點。
Redis
做中間件的劣勢:可能出現(xiàn)數(shù)據(jù)丟失。 有這么個幾個場景:
-
AOF
策略為每秒寫盤。該過程為異步,若Redis
發(fā)生宕機,會丟失1秒的數(shù)據(jù)。若改為同步寫盤,則會導致性能下降。 - 在主從集群下,倘若寫操作的頻率非常大,那么主從的數(shù)據(jù)同步就會存在延遲,那么在進行主從切換的時候,也可能存在數(shù)據(jù)丟失問題。詳細可以看Redis - Redis主從數(shù)據(jù)一致性和哨兵機制。
- 無法保證數(shù)據(jù)的完整性,而像Kafka這樣的專業(yè)中間件,副本等機制保證了數(shù)據(jù)的可靠性。哪怕集群的某個節(jié)點掛掉了,也不會丟失數(shù)據(jù)。詳細可以參考Kafka復習計劃 - Kafka基礎知識以及集群參方案和參數(shù)。
原文鏈接:https://blog.csdn.net/Zong_0915/article/details/126228122
相關推薦
- 2024-02-26 gitlab合并分支
- 2022-07-06 C#線程開發(fā)之System.Thread類詳解_C#教程
- 2022-05-13 ffmpeg+pyqt5簡單實現(xiàn)一個抽幀可視化小工具
- 2022-07-21 Pandas文件讀寫操作
- 2022-05-18 React?Hook之使用State?Hook的方法_React
- 2022-03-16 VS2022?安裝.NET4.5目標包的方法_實用技巧
- 2022-04-30 Python自定義指標聚類實例代碼_python
- 2022-05-01 sql時間段切分實現(xiàn)每隔x分鐘出一份高速門架車流量_MsSql
- 最近更新
-
- window11 系統(tǒng)安裝 yarn
- 超詳細win安裝深度學習環(huán)境2025年最新版(
- Linux 中運行的top命令 怎么退出?
- MySQL 中decimal 的用法? 存儲小
- get 、set 、toString 方法的使
- @Resource和 @Autowired注解
- Java基礎操作-- 運算符,流程控制 Flo
- 1. Int 和Integer 的區(qū)別,Jav
- spring @retryable不生效的一種
- Spring Security之認證信息的處理
- Spring Security之認證過濾器
- Spring Security概述快速入門
- Spring Security之配置體系
- 【SpringBoot】SpringCache
- Spring Security之基于方法配置權
- redisson分布式鎖中waittime的設
- maven:解決release錯誤:Artif
- restTemplate使用總結
- Spring Security之安全異常處理
- MybatisPlus優(yōu)雅實現(xiàn)加密?
- Spring ioc容器與Bean的生命周期。
- 【探索SpringCloud】服務發(fā)現(xiàn)-Nac
- Spring Security之基于HttpR
- Redis 底層數(shù)據(jù)結構-簡單動態(tài)字符串(SD
- arthas操作spring被代理目標對象命令
- Spring中的單例模式應用詳解
- 聊聊消息隊列,發(fā)送消息的4種方式
- bootspring第三方資源配置管理
- GIT同步修改后的遠程分支