Redis除了緩存,還有哪些應用?
Redis實現消息隊列
**使用Pub/Sub模式:**Redis的Pub/Sub是一種基于發布/訂閱的消息模式,任何客戶端都可以訂閱一個或多個頻道,發布者可以向特定頻道發送消息,所有訂閱該頻道的客戶端都會收到此消息。該方式實現起來比較簡單,發布者和訂閱者完全解耦,支持模式匹配訂閱。但是這種方式不支持消息持久化,消息發布后若無訂閱者在線則會被丟棄;不保證消息的順序和可靠性傳輸。
使用List結構:使用List的方式通常是使用LPUSH命令將消息推入一個列表,消費者使用BLPOP或BRPOP阻塞地從列表中取出消息(先進先出FIFO)。這種方式可以實現簡單的任務隊列。這種方式可以結合Redis的過期時間特性實現消息的TTL;通過Redis事務可以保證操作的原子性。但是需要客戶端自己實現消息確認、重試等機制,相比專門的消息隊列系統功能較弱。
Redis實現分布式鎖
set nx方式:Redis提供了幾種方式來實現分布式鎖,最常用的是基于SET命令的爭搶鎖機制。客戶端可以使用SET resource_name lock_value NX PX milliseconds命令設置鎖,其中NX表示只有當鍵不存在時才設置,PX指定鎖的有效時間(毫秒)。如果設置成功,則認為客戶端獲得鎖。客戶端完成操作后,解鎖的還需要先判斷鎖是不是自己,再進行刪除,這里涉及到 2 個操作,為了保證這兩個操作的原子性,可以用 lua 腳本來實現。
**RedLock算法:**為了提高分布式鎖的可靠性,Redis作者Antirez提出了RedLock算法,它基于多個獨立的Redis實例來實現一個更安全的分布式鎖。它的基本原理是客戶端嘗試在多數(大于半數)Redis實例上同時加鎖,只有當在大多數實例上加鎖成功時才認為獲取鎖成功。鎖的超時時間應該遠小于單個實例的超時時間,以避免死鎖。該方式可以通過跨多個節點減少單點故障的影響,提高了鎖的可用性和安全性。
Redis支持并發操作嗎?
單個 Redis 命令的原子性:Redis 的單個命令是原子性的,這意味著一個命令要么完全執行成功,要么完全不執行,確保操作的一致性。這對于并發操作非常重要。但是一系列操作命令不是原子的
多個操作的事務:Redis 支持事務,可以將一系列的操作放在一個事務中執行,使用 MULTI、EXEC、DISCARD 和 WATCH 等命令來管理事務。這樣可以確保一系列操作的原子性。
Redis分布式鎖的實現原理?什么場景下用到分布式鎖?
分布式鎖是用于分布式環境下并發控制的一種機制,用于控制某個資源在同一時刻只能被一個應用所使用。如下圖所示:Redis 本身可以被多個客戶端共享訪問,正好就是一個共享存儲系統,可以用來保存分布式鎖,而且 Redis 的讀寫性能高,可以應對高并發的鎖操作場景。Redis 的 SET 命令有個 NX 參數可以實現「key不存在才插入」,所以可以用它來實現分布式鎖:
如果 key 不存在,則顯示插入成功,可以用來表示加鎖成功;
如果 key 存在,則會顯示插入失敗,可以用來表示加鎖失敗。
基于 Redis 節點實現分布式鎖時,對于加鎖操作,我們需要滿足三個條件。
加鎖包括了讀取鎖變量、檢查鎖變量值和設置鎖變量值三個操作,但需要以原子操作的方式完成,所以,我們使用 SET 命令帶上 NX 選項來實現加鎖;
鎖變量需要設置過期時間,以免客戶端拿到鎖后發生異常,導致鎖一直無法釋放,所以,我們在 SET 命令執行時加上 EX/PX 選項,設置其過期時間;
鎖變量的值需要能區分來自不同客戶端的加鎖操作,以免在釋放鎖時,出現誤釋放操作,所以,我們使用 SET 命令設置鎖變量值時,每個客戶端設置的值是一個唯一值,用于標識客戶端;
滿足這三個條件的分布式命令如下:
SET lock_key unique_value NX PX 10000
lock_key 就是 key 鍵;
unique_value 是客戶端生成的唯一的標識,區分來自不同客戶端的鎖操作;
NX 代表只在 lock_key 不存在時,才對 lock_key 進行設置操作;
PX 10000 表示設置 lock_key 的過期時間為 10s,這是為了避免客戶端發生異常而無法釋放鎖。
而解鎖的過程就是將 lock_key 鍵刪除(del lock_key),但不能亂刪,要保證執行操作的客戶端就是加鎖的客戶端。所以,解鎖的時候,我們要先判斷鎖的 unique_value 是否為加鎖客戶端,是的話,才將 lock_key 鍵刪除。
可以看到,解鎖是有兩個操作,這時就需要 Lua 腳本來保證解鎖的原子性,因為 Redis 在執行 Lua 腳本時,可以以原子性的方式執行,保證了鎖釋放操作的原子性。
// 釋放鎖時,先比較 unique_value 是否相等,避免鎖的誤釋放
if redis.call("get",KEYS[1]) == ARGV[1] then
??return redis.call("del",KEYS[1])
else
??return 0
end
這樣一來,就通過使用 SET 命令和 Lua 腳本在 Redis 單節點上完成了分布式鎖的加鎖和解鎖。
Redis的大Key問題是什么?
Redis大key問題指的是某個key對應的value值所占的內存空間比較大,導致Redis的性能下降、內存不足、數據不均衡以及主從同步延遲等問題。
到底多大的數據量才算是大key?
沒有固定的判別標準,通常認為字符串類型的key對應的value值占用空間大于1M,或者集合類型的k元素數量超過1萬個,就算是大key。
Redis大key問題的定義及評判準則并非一成不變,而應根據Redis的實際運用以及業務需求來綜合評估。
例如,在高并發且低延遲的場景中,僅10kb可能就已構成大key;然而在低并發、高容量的環境下,大key的界限可能在100kb。因此,在設計與運用Redis時,要依據業務需求與性能指標來確立合理的大key閾值。
大Key問題的缺點?
內存占用過高。大Key占用過多的內存空間,可能導致可用內存不足,從而觸發內存淘汰策略。在極端情況下,可能導致內存耗盡,Redis實例崩潰,影響系統的穩定性。
性能下降。大Key會占用大量內存空間,導致內存碎片增加,進而影響Redis的性能。對于大Key的操作,如讀取、寫入、刪除等,都會消耗更多的CPU時間和內存資源,進一步降低系統性能。
阻塞其他操作。某些對大Key的操作可能會導致Redis實例阻塞。例如,使用DEL命令刪除一個大Key時,可能會導致Redis實例在一段時間內無法響應其他客戶端請求,從而影響系統的響應時間和吞吐量。
網絡擁塞。每次獲取大key產生的網絡流量較大,可能造成機器或局域網的帶寬被打滿,同時波及其他服務。
例如:一個大key占用空間是1MB,每秒訪問1000次,就有1000MB的流量。
主從同步延遲。當Redis實例配置了主從同步時,大Key可能導致主從同步延遲。由于大Key占用較多內存,同步過程中需要傳輸大量數據,這會導致主從之間的網絡傳輸延遲增加,進而影響數據一致性。
數據傾斜。在Redis集群模式中,某個數據分片的內存使用率遠超其他數據分片,無法使數據分片的內存資源達到均衡。另外也可能造成Redis內存達到maxmemory參數定義的上限導致重要的key被逐出,甚至引發內存溢出。
Redis大key如何解決?
對大Key進行拆分。例如將含有數萬成員的一個HASH Key拆分為多個HASH Key,并確保每個Key的成員數量在合理范圍。在Redis集群架構中,拆分大Key能對數據分片間的內存平衡起到顯著作用。
對大Key進行清理。將不適用Redis能力的數據存至其它存儲,并在Redis中刪除此類數據。注意,要使用異步刪除。
監控Redis的內存水位。可以通過監控系統設置合理的Redis內存報警閾值進行提醒,例如Redis內存使用率超過70%、Redis的內存在1小時內增長率超過20%等。
對過期數據進行定期清。堆積大量過期數據會造成大Key的產生,例如在HASH數據類型中以增量的形式不斷寫入大量數據而忽略了數據的時效性。可以通過定時任務的方式對失效數據進行清理。
什么是熱key?
通常以其接收到的Key被請求頻率來判定,例如:
QPS集中在特定的Key:Redis實例的總QPS(每秒查詢率)為10,000,而其中一個Key的每秒訪問量達到了7,000。
帶寬使用率集中在特定的Key:對一個擁有上千個成員且總大小為1 MB的HASH Key每秒發送大量的HGETALL操作請求。
CPU使用時間占比集中在特定的Key:對一個擁有數萬個成員的Key(ZSET類型)每秒發送大量的ZRANGE操作請求。
如何解決熱key問題?
在Redis集群架構中對熱Key進行復制。在Redis集群架構中,由于熱Key的遷移粒度問題,無法將請求分散至其他數據分片,導致單個數據分片的壓力無法下降。此時,可以將對應熱Key進行復制并遷移至其他數據分片,例如將熱Key foo復制出3個內容完全一樣的Key并名為foo2、foo3、foo4,將這三個Key遷移到其他數據分片來解決單個數據分片的熱Key壓力。
使用讀寫分離架構。如果熱Key的產生來自于讀請求,您可以將實例改造成讀寫分離架構來降低每個數據分片的讀請求壓力,甚至可以不斷地增加從節點。但是讀寫分離架構在增加業務代碼復雜度的同時,也會增加Redis集群架構復雜度。不僅要為多個從節點提供轉發層(如Proxy,LVS等)來實現負載均衡,還要考慮從節點數量顯著增加后帶來故障率增加的問題。Redis集群架構變更會為監控、運維、故障處理帶來了更大的挑戰。
如何保證 redis 和 mysql 數據緩存一致性問題?
更新緩存還是刪除緩存:
下面,我們來分析一下,應該采用更新緩存還是刪除緩存的方式。
更新緩存
優點:每次數據變化都及時更新緩存,所以查詢時不容易出現未命中的情況。
缺點:更新緩存的消耗比較大。如果數據需要經過復雜的計算再寫入緩存,那么頻繁的更新緩存,
就會影響服務器的性能。如果是寫入數據頻繁的業務場景,那么可能頻繁的更新緩存時,卻沒有業
務讀取該數據。
刪除緩存
優點:操作簡單,無論更新操作是否復雜,都是將緩存中的數據直接刪除。?
缺點:刪除緩存后,下一次查詢緩存會出現未命中,這時需要重新讀取一次數據庫。
從上面的比較來看,一般情況下,刪除緩存是更優的方案
如果先操作緩存
什么時候會出現數據緩存不一致的情況?線程1想要修改數據,那它先把redis里的舊數據刪除,然后再去數據庫里去修改數據,這樣的話下次查詢數據的時候,redis里不存在就會去數據庫里查找,數據庫里找到后就會更新一份最新的數據給redis里,這樣redis和mysql數據就一致了,但是假如在數據庫正要修改數據的時候網絡突然阻塞了,這樣的話線程2去讀數據,讀到一個過時的未修改的舊數據,然后數據庫修改成新數據,這樣的話redis和數據庫數據就不一致了。
緩存是通過犧牲強一致性來提高性能的。這是由CAP理論決定的。緩存系統適用的場景就是非強一致性的場景,它屬于CAP中的AP。所以,如果需要數據庫和緩存數據保持強一致,就不適合使用緩存(因為保證了強一致性,那么性能就沒法保證了,那么用緩存的意義也不大了,因為我們用緩存本身就是要提高性能)。
所以說我們只能保證最終一致性(也就是最終結果是一樣的),不能保證強一致性
解決策略:延遲雙刪
1. 刪除緩存;
2. 更新數據庫;
3. sleep N毫秒; (延遲是為了讓redis讀取到舊的數據)
4. 再次刪除緩存。
如果先操作數據庫
??也無法避免數據緩存不一致的情況
對于讀數據,我會選擇旁路緩存策略,如果 cache 不命中,會從 db 加載數據到 cache。對于寫數據,我會選擇更新 db 后,再刪除緩存。
所以使用緩存提升性能,就是會有數據更新的延遲。這需要我們在設計時結合業務仔細思考是否適合用緩存。然后緩存一定要設置過期時間,這個時間太短、或者太長都不好:
太短的話請求可能會比較多的落到數據庫上,這也意味著失去了緩存的優勢。
太長的話緩存中的臟數據會使系統長時間處于一個延遲的狀態,而且系統中長時間沒有人訪問的數據一直存在內存中不過期,浪費內存。
但是,通過一些方案優化處理,是可以最終一致性的。
針對刪除緩存異常的情況,可以使用 2 個方案避免:
刪除緩存重試策略(消息隊列)
訂閱 binlog,再刪除緩存(Canal+消息隊列)
消息隊列方案
我們可以引入消息隊列,將第二個操作(刪除緩存)要操作的數據加入到消息隊列,由消費者來操作數據。
如果應用刪除緩存失敗,可以從消息隊列中重新讀取數據,然后再次刪除緩存,這個就是重試機制。當然,如果重試超過的一定次數,還是沒有成功,我們就需要向業務層發送報錯信息了。
如果刪除緩存成功,就要把數據從消息隊列中移除,避免重復操作,否則就繼續重試。
訂閱 MySQL binlog,再操作緩存
「先更新數據庫,再刪緩存」的策略的第一步是更新數據庫,那么更新數據庫成功,就會產生一條變更日志,記錄在 binlog 里。
于是我們就可以通過訂閱 binlog 日志,拿到具體要操作的數據,然后再執行緩存刪除,阿里巴巴開源的 Canal 中間件就是基于這個實現的。
Canal 模擬 MySQL 主從復制的交互協議,把自己偽裝成一個 MySQL 的從節點,向 MySQL 主節點發送 dump 請求,MySQL 收到請求后,就會開始推送 Binlog 給 Canal,Canal 解析 Binlog 字節流之后,轉換為便于讀取的結構化數據,供下游程序訂閱使用。
下圖是 Canal 的工作原理:
將binlog日志采集發送到MQ隊列里面,然后編寫一個簡單的緩存刪除消息者訂閱binlog日志,根據更新log刪除緩存,并且通過ACK機制確認處理這條更新log,保證數據緩存一致性