Redis到底是多線程還是單線程 Redis 6.0版本之前的單線程指的是其網路I/O和鍵值對的讀寫是由一個線程完成的。 多線程在Redis 6.0中的引入是為了改善一些特定場景下的性能問題,特別是在大型多核系統上。Redis 6.0引入了多個I/O線程,這些線程負責處理網路事件的監聽和接收。主線程 ...
Redis到底是多線程還是單線程
Redis 6.0版本之前的單線程指的是其網路I/O和鍵值對的讀寫是由一個線程完成的。
多線程在Redis 6.0中的引入是為了改善一些特定場景下的性能問題,特別是在大型多核系統上。Redis 6.0引入了多個I/O線程,這些線程負責處理網路事件的監聽和接收。主線程仍然是單線程的,負責命令的執行和響應的返回
舉個例子,假設有多個客戶端同時向Redis發送請求,這些請求在網路上到達Redis伺服器。主線程會監聽這些網路事件,並將請求分發給空閑的I/O線程進行處理。每個I/O線程負責接收請求、解析命令,並將命令發送給主線程執行。主線程執行完命令後,將響應發送給對應的I/O線程,然後I/O線程將響應返回給客戶端。而鍵值對讀寫命令仍然是單線程處理,所以Redis依然是併發安全的。
只有網路請求模塊和數據操作模塊式是單線程的,而其他的持久化、集群數據同步等,其實是由額外的線程執行的。
服務端的讀寫命令是單線程的。
Redis單線程為什麼還能那麼快
- 命令執行基於記憶體操作,一條命令在記憶體里操作的時間是幾十納秒
- 命令執行時單線程操作,沒有線程切換的開銷
- 基於IO多路復用機制提升Redis的IO利用率
- 高效的數據存儲結構:全局hash表以及多種高效數據結構,比如跳錶,壓縮列表,鏈表等等。
Redis底層數據如何用跳錶存儲的
有序集合放到鏈表中,跳錶就是根據鏈表查找較慢這點而去做優化
在Redis中,跳錶(Skip List)是用於實現有序集合(Sorted Set)的數據結構。跳錶通過引入多層索引來加速有序集合的查找操作。
跳錶的底層原理是由多層鏈表組成,其中最底層是原始鏈表,包含所有的元素節點。上層的索引節點通過指針連接了下層的節點,形成了一種跳躍的結構。
跳錶的層數是根據元素數量和概率計算得出的,通常由一個隨機演算法決定。每一層的索引節點數量是根據元素數量和層數計算得出的。
O(logN)
把有序鏈表改造為支持近似“折半查找”演算法,可以進行更快速的插入、輸出、查找操作
Redis刪除過期key的策略
Redis對於過期key的刪除策略主要有三種:
- 定時刪除:在設置key的過期時間的同時,為該key創建一個定時器,讓定時器在key的過期時間來臨時,對key進行刪除。這種方式可以保證記憶體被儘快釋放,但如果過期key很多,刪除這些key會占用很多的CPU時間¹。
- 惰性刪除:key過期的時候不刪除,每次從資料庫獲取key的時候去檢查是否過期,若過期,則刪除,返回null。這種方式對CPU時間的占用是比較少的,但如果大量的key在超出超時時間後,很久一段時間內,都沒有被獲取過,那麼可能發生記憶體泄露。
- 定期刪除:每隔一段時間執行一次刪除過期key操作。通過限制刪除操作的時長和頻率,來減少刪除操作對CPU時間的占用¹。
定時刪除和定期刪除為主動刪除:Redis會定期主動淘汰一批已過去的key。惰性刪除為被動刪除:用到的時候才會去檢驗key是不是已過期,過期就刪除¹。
惰性刪除為redis伺服器內置策略。定期刪除可以通過配置redis.conf 的hz選項,預設為10 (即1秒執行10次,100ms一次),以及配置redis.conf的maxmemory最大值,當已用記憶體超過maxmemory限定時,就會觸發主動清理策略¹。
Redis Key過期了為什麼記憶體沒釋放
你在使用Redis時,肯定經常使用SET
命令
SET
除了可以設置key-value 之外,還可以設置key的過期時間,就像下麵這樣:
127.0.0.1:6379> SET tuling zhuge EX 120
oK
127.0.0.1:6379> TTL tuling4 (integer)117
此時如果你想修改key的值,但只是單純地使用SET命令,而沒有加上過期時間的參數,那這個key的過期時間將會被擦除
127.0.0.1:6379> SET tuling zhuge6662oK
127.0.0.1:6379> TTL tuling // key永遠不過期了!4 (integer) -1
導致這個問題的原因在於:SET
命令如果不設置過期時間,那麼Redis 會自動擦除這個key的過期時間
如果你發現Redis的記憶體持續增長,而且很多key原來設置了過期時間,後來發現過期時間丟失了,很有可能是因為這個原因導致的。
這時你的Redis中就會存在大量不過期的 key,消耗過多的記憶體資源
所以,你在使用SET
命令時,如果剛開始就設置了過期時間,那麼之後修改這個key,也務必要加上過期時間的參數,避免過期時間丟失問題。
Redis對於過期key的處理一般有惰性刪除和定時刪除兩種策略
1、惰性刪除:當讀/寫一個已經過期的key時,會觸發惰性刪除策略,判斷key是否過期,如果過期了直接刪除掉這個key。
2、定時刪除:由於惰性刪除策略無法保證冷數據被及時刪掉,所以Redis會定期(預設每100ms)主動淘汰一批已過期的key ,這裡的一批只是部分過期key,所以可能會出現部分key已經過期但還沒有被清理掉的情況,導致記憶體並沒有被釋放。
Redis Key沒設置過期時間為什麼被Redis主動刪除了
當redis已用記憶體超過maxmemory
限定時,觸發主動清理策略
主動清理策略在Redis 4.0之前一共實現了6種記憶體淘汰策略,在4.0之後,又增加2種策略,總共8種:
a)淘汰策略只對設置了TTL的key生效:
- volatile-ttl:是基於生存時間(Time-To-Live,TTL)的策略。這種策略會從已設置過期時間的數據集中挑選將要過期的數據進行淘汰。淘汰的策略不是LRU,而是基於key的剩餘壽命TTL的值,TTL越小的key越優先被淘汰。
- volatile-random:就像它的名稱一樣,在設置了過期時間的鍵值對中,進行隨機刪除。
- volatile-lru:會使用LRU 演算法篩選設置了過期時間的鍵值對刪除。
- volatile-lfu:會使用LFU 演算法篩選設置了過期時間的鍵值對刪除。
b)淘汰策略對所有key生效:
- allkeys-random:從所有鍵值對中隨機選擇並刪除數據。
- allkeys-lru:使用LRU (Least Recently Used)演算法在所有數據中進行篩選刪除。
- allkeys-lfu:使用LFU(Least Frequently Used)演算法在所有數據中進行篩選刪除。
c) 不處理:
- noeviction:不會剔除任何數據,拒絕所有寫入操作並返回客戶端錯誤信息"(error) OOM command not allowed whenused memory",此時Redis只響應讀操作。(預設的記憶體淘汰策略)
LRU (Least Recently Used)演算法:在淘汰時應該優先選擇最近最少使用的數據項。LRU演算法維護一個使用順序隊列,最近訪問的數據項被移動到隊列的末尾(redis預設)。
LFU(Least Frequently Used)演算法:在淘汰時應該優先選擇最不經常使用的數據項。LFU演算法維護一個使用頻率計數器,記錄每個數據項被訪問的次數。當存在大量的熱點緩存數據時,LFU可能更好。
刪除Key的命令會阻塞Redis嗎
會!
當你試圖刪除一個非常大的key時。例如,如果你有一個包含數百萬個元素的列表或集合或者很大的String,並且你試圖用一個DEL命令來刪除它,那麼這個操作可能會花費一些時間,併在此期間阻塞其他操作。
為什麼當Key非常大的時候redis會阻塞?
Redis是單線程的,這意味著它一次只能執行一個操作。當你要求Redis刪除一個大的key時,這個操作可能會花費一些時間。在這個時間內,Redis不能處理其他任何操作,因此會出現阻塞。
這是因為在Redis中,刪除一個key需要遍歷並釋放key所關聯的所有記憶體。如果key關聯的數據結構非常大(例如,一個包含數百萬個元素的列表或集合),那麼遍歷和釋放記憶體的過程就會耗費更多的時間。在這個過程中,Redis不能處理其他操作,因此會出現阻塞。
為了避免這種阻塞,Redis 4.0引入了UNLINK
命令。當你使用UNLINK
命令刪除一個key時,Redis會立即返回,併在後臺非同步刪除key。這樣,即使你正在刪除一個大的key,其他的Redis操作也不會被阻塞。但是,UNLINK命令不能保證立即刪除key,如果你需要立即刪除key,你仍然需要使用DEL命令。
Redis 主從、哨兵、集群架構的優缺點
- 監控:哨兵會不斷的檢查master和slave是否按照預期工作
- 自動故障恢復:如果master故障了,就選取一個slave來“當老大”,即使故障修複了也不會換回來。
- 通知:在選出新的master後,需要讓其他的slave和客戶端與新的master通信,那麼就需要哨兵在執行故障轉移的時候,通知其他的slave和客戶端。
哨兵節點(sentinel)也是redis的實例,對每一個主從redis節點監聽
客戶端直接訪問哨兵節點。
如果主節點掛了
哨兵會監視到,然後從從節點中選取一個作為主節點。並且把這個新的主節點告訴客戶端。
主從切換的時候不需要運維介入,全程自動化。
切換的過程雖然自動化,但是沒有那麼快,如果在這個過程中訪問可能會瞬斷/報錯。
單節點實際支持的併發只有10萬。大公司滿足不了併發。
單節點不宜設置過大<10G,太大會影響數據恢復或主從同步的效率
Redis集群是一個由多個主從節點群組成的分散式伺服器群,它具有複製、高可用和分片的特性。
假設存儲100G的數據,總的緩存數據分片存儲(一個主節點30G,30G,40G),可以現行擴容到上萬個節點,但是官方推薦不超過1000個。
瞬斷的問題:假設一個主節點掛了,那麼會選取一個從節點作為主節點,訪問當前主節點的數據會瞬斷,但是如果訪問的數據在其他主從節點就不會瞬斷,因此還存在瞬斷,但是概率降低了。
redis集群中一個master掛了,怎麼選舉新的master
在Redis集群中,當一個master節點宕機或者不可達時,會從它的slave節點中選出一個新的master節點接替它,具體的選舉過程如下:
- 發現故障
集群中的每個節點會定期使用ping消息檢測相鄰節點是否可達。如果一個節點檢測到master節點失敗,會將這個master標記為PFAIL狀態。
- 選舉投票
檢測到故障的slave節點會向集群內的所有master節點發送投票消息,請求將自己設置為新的master。
- 處理投票
收到投票請求的master節點會比較所有候選slave,根據slave優先順序、偏移量(offset)、運行時長等來選出最合適的那個slave。
- 響應投票
集群中的所有master節點向該slave發送確認消息,同意其成為新的master。
- 配置更新
該slave在收到超過半數(包括自己的主節點 )master的確認後,會將自己的配置切換為master,包括生成新的節點ID。(這裡解釋了集群為什麼至少需要三個主節點,如果只有兩個,當其中一個掛了,只剩一個主節點是不能選舉成功的)
- 主從切換
原來的slave會向新的master發送SYNC命令,成為其slave。新的master會更新原來master的slot信息。
至此,選舉完成,這個slave已切換成新的master,原有的master節點也從集群中移除。這保證了集群的高可用性。
整個重新選舉的時間通常在1-2秒,對客戶端無感知。Redis集群能夠自動快速完成故障轉移和新master的選舉。
Redis集群數據hash分片演算法
Redis集群將所有數據劃分為16384個槽位(slot)
Redis執行命令有死迴圈阻塞bug
RANDOMKEY
從當前資料庫中返回(不刪除)一個key。
#資料庫不為空
redis> MSET fruit "apple" drink "beer" food "cookies" #設置多個key
OK
redis> RANDOMKEY
"fruit"
redis> RANDOMKEY
"food"
redis> KEYS * #查看資料庫內所有key,證明RANDOMKEY 並不刪除key
1) "food"
2) "drink"
3) "fruit"
Redis對於過期Key的處理策略是惰性刪除的方式,RANDOMKEY在隨機拿到一個key時,首先會檢查key是否過期,如果過期,就會刪除這個key,因為這個key被刪除了,RANDOMKEY無法把當前key作為正確結果輸出,因此會再次查找下一個隨機key。如果有大量key已經過期還未來得及被清理調,那麼會一直去尋找沒有過期的key,這個過程可能會持續很久,因此會影響redis性能。
Redis 在複製過程中使用的是非同步複製機制,主伺服器將命令發送給從伺服器,但從伺服器並不會中斷正在執行的查詢命令來執行主伺服器發送的刪除命令。相反,從伺服器會繼續處理當前的查詢命令,保持與主伺服器的同步,並且只有在查詢命令執行完畢後,才會開始執行主伺服器發送的刪除命令。
如果在從節點(slave)上執行RANDOMKEY,那麼問題會更嚴重,
slave自己是不會清理過期key,當一個key要過期時,master會先清理刪除它,之後master向slave發送一個DEL命令,告訴slave也刪除key以此達到主從庫的數據一致性。
因此在redis中存在大量過期key的情況下,在slave身上執行RANDOMKEY取到隨機但是過期的key不會刪除它,而是繼續尋找不過期的key,由於大量key都已過期,因此大概率陷入死迴圈狀態。
這其實是Redis的一個Bug,這個bug一直持續到5.0才被修複。
修複的解決方案就是在slave中的查找次數做出限制。
一次線上事故,Redis主從切換導致了緩存雪崩
假設slave的機器時鐘比master走得快很多,比如主伺服器12點,此時從伺服器已經1點了。
此時master中一些沒有過期的key,在slave的視角就是過期的,如果此時進行主從切換操作,把時間快的從伺服器換到主伺服器上,新主伺服器就會開始大量清理過期key,引發緩存雪崩。
一定要保證主從伺服器時鐘的一致性。
Redis持久化RDB、AOF、混合持久化是怎麼回事
RDB快照(snapshot)
在預設情況下,Redis將記憶體資料庫快照保存在名字為dump.rdb文件中。
需要註意的是,RDB 持久化是一種全量持久化方式,它將整個 Redis 數據集寫入到磁碟。因此,在每次持久化操作期間,Redis 會將記憶體中的所有數據保存到 dump.rdb 中,而不是增量地向文件中添加數據。
rdb的啟動方式:
save 900 1
save 300 10
save 60 10000 #在60秒的實踐中10000個改動
在redis中的save命令可以手動持久化數據。
bgsave
(backgroundsave)後臺save,其是通過操作系統提供的寫時複製技術(Copy-On-Write,COW)實現的,在生成快照的同時,依然可以正常處理命令。
在Redis中,bgsave
命令用於在後臺非同步地創建當前資料庫的快照。這個命令實際上會調用fork()系統調用來創建一個子進程,子進程會將數據寫入磁碟。 在創建子進程時,父進程的記憶體空間會被覆制到子進程。這是一個昂貴的操作,因為如果你有一個占用了10GB記憶體的Redis實例,那麼執行fork()就需要額外的10GB記憶體。但是,這就是Copy-On-Write技術發揮作用的地方。
Copy-On-Write (COW) 是一種可以延遲或避免複製數據的技術。當父進程創建子進程時,操作系統並不立即複製所有的記憶體頁,而是讓父進程和子進程共用同樣的記憶體頁。只有當其中一個進程嘗試修改某個記憶體頁時,操作系統才會創建那個記憶體頁的副本,讓修改的進程(主進程)使用這個副本,而另一個進程(bgsave子進程)繼續使用原來的記憶體頁。這就是所謂的"寫時複製"。
在Redis的BGSAVE命令中,COW技術可以幫助節省記憶體。當BGSAVE命令執行時,子進程開始寫快照,而父進程繼續處理命令。如果在此過程中,父進程需要修改某個記憶體頁,那麼操作系統會為父進程創建一個新的記憶體頁,而子進程仍然可以看到原來的記憶體頁,因此可以正確地將數據寫入快照。
總的來說,COW技術允許Redis在創建快照時最大限度地節省記憶體,並且確保了快照的一致性,因為子進程看到的始終是fork()時的數據。
兩者的區別如下。
缺點:
- 生成快照占用記憶體巨大。
- 按照快照的保存策略進行持久化,例如save 60 1000,如果在下一個60秒內沒有滿足save條件但是redis宕機了,那麼就有數據丟失的問題。
AOF(Append-Only file)持久化
可以通過修改配置文件來打開AOF功能
# appendonly yes
將修改的每一條指令記錄進文件appendonly.aof中(先寫入os cache每隔一段時間fsync到磁碟),逐條的把所有命令執行。
可以配置redis多久fsync到磁碟一次:
appendfsync always #每次有新命令追加到AOF文件時就執行一次fsync
appendfsync everysec #每秒fsync一次,足夠快,並且故障時只會丟失1秒鐘的數據
appendfsync no #從不fsync,將數據交給操作系統來處理,足夠快但是不安全。
推薦並且預設的就是每秒fsync一次,appendfsync everysec具體來說:
- Redis會用一個1秒計時器來實現這個時間間隔的控制。
- 在一個1秒時間段內,所有發生的寫命令會存在記憶體緩衝區。
- 到了每個1秒時間點,Redis會把這1秒內緩衝起來的所有寫命令一次性非同步寫入AOF文件。
- 然後計時器重置,開始計時下一個1秒時間段內的寫命令。
缺點:redis運行時間很長,AOF文件會很大,恢復的速度會很慢。
RDB和AOF的優劣:
生產環境可以都啟動
AOF重寫:
incr readcount #多次執行,會在AOF文件中多次記錄
set readcount n #不如直接set為最後一次的readcount的值
多條命令可以直接重寫為一條命令。
#aof文件至少要達到64m才會自動重寫,文件太小恢復速度本來就很快,重寫意義不大
auto-aof-rewrite-percentage 100
#aof文件自上次重寫後文件大小增長了100%會觸發重寫
auto-aof-rewrite-min-size 64mb
aof重寫是執行bgrewriteaof命令重寫aof,類似bgsave也是有些消耗性能。
Redis4.0 混合持久化
必須先開啟AOF
aof-use-rdb-preamble yes
AOF在重寫時,直接寫成RDB的二進位格式數據,.aof文件中追加 快照的形式。
混合持久化的文件結構。
需要註意的是,這個RDB格式的快照是直接寫入到AOF文件中的,而不是作為一個單獨的文件存在。也就是說,一個AOF文件可能既包含RDB格式的數據快照,又包含普通的AOF命令。實際上,我們無法直接讀取或者理解RDB格式的數據快照,因為它是二進位的。我們只能知道,當Redis載入這個AOF文件時,它會首先載入RDB格式的數據快照,然後再執行後面的命令,從而恢復所有的數據。
此時就不需要rdb持久化方式了。
優勢:
- 數據格式更加緊湊,
- 數據啟動恢復效率更高,
- 又兼顧安全性。
線上Redis持久化策略一般如何設置
如果對性能要求較高,在Master最好不要做持久化,可以在某個Slave開啟AOF備份數據,策略設置為每秒同步一次即可。
一次線上事故,Redis主節點宕機導致數據全部丟失
如果你的Redis 採用如下模式部署,就會發生數據丟失的問題:
- master-slave+哨兵部署實例。
- master 沒有開啟數據持久化功能。
- Redis進程使用supervisor管理,並配置為進程宕機,自動重啟。
如果此時master宕機,就會導致下麵的問題:
- master宕機,哨兵還未發起切換,此時 master進程立即被supervisor自動拉起。
- 但master沒有開啟任何數據持久化,啟動後是一個空實例。
- 此時 slave為了與master保持一致,它會自動清空實例中的所有數據,slave也變成了一個空實例。在這個場景下,master / slave 的數據就全部丟失了。
這時,業務應用在訪問Redis時,發現緩存中沒有任何數據,就會把請求全部打到後端資料庫上,這還會進一步引發緩存雪崩,對業務影響非常大。
這種情況下我們一般不應孩給Redis主節點配置進程宕機馬上自動重啟策略,而應該等哨兵把某個Redis從節點切換為主節點後再重啟之前宕機的Redis主節點讓其變為slave節點。
Supervisor是用Python開發的一個client/server服務,是Linux/Unix系統下的一個進程管理工具,不支持Windows系統。它可以很方便的監聽、啟動、停止、重啟一個或多個進程。用用Supervisor管理的進程,當一個進程意外被殺死,supervisort監聽到進程死後,會自動將它重新拉起,很方便的做到進程自動恢復的功能,不再需要自己寫shell腳本來控制。
Redis線上數據如何備份
- 寫crontab定時調度腳本,每小時都copy一份rdb或aof文件到另一臺機器中,保留最近48小時的備份。
- 每天都保留一份當日的數據備份到一個目錄中去,可以保留最近一個月的的備份
- 每次copy備份的時候,都把太舊的備份刪除。
Redis主從複製風暴是怎麼回事
大規模數據同步
如果Redis主節點有很多從節點,在某一時刻如果所有從節點都同時連接柱節點,那麼主節點會同時把記憶體快照RDB發送給多個從節點,這樣導致Redis主節點壓力非常大,此外還有可能因為數據量非常大而導致主從複製風暴。這就是所謂的Redis主從複製風暴問題。
這種問題可以對Redis主從架構做一些優化得以避免。
網路延遲或擁塞
主節點和從節點之間的網路延遲或擁塞也可能導致主從複製風暴。當網路延遲較高時,主節點可能會頻繁地重新發送數據同步請求,從節點收到大量的重覆數據,導致不斷重覆的同步過程。如果網路帶寬不足或網路擁塞,數據同步的速度將受到影響,可能會導致複製風暴。
解決方案:
優化主從節點之間的網路連接,確保帶寬充足,減少網路延遲和擁塞的影響。可以使用高速網路連接、合理配置網路參數等方式來改善網路性能。
批量操作或大量寫入
如果在主節點上進行了大批量的寫入操作,或者有大量的寫入請求同時涌入主節點,主節點需要將這些寫入操作同步到從節點,可能引發主從複製風暴。大規模的數據寫入會導致複製緩衝區堆積,從節點無法及時處理所有的寫入請求,造成複製延遲和複製風暴。
解決方案:
控制寫入壓力:限制寫入操作的頻率和併發量,避免批量寫入操作和大量寫入請求同時涌入主節點,以減輕主從複製的壓力。
定期監控主從複製的延遲和系統負載情況,及時發現問題並採取相應的調優措施,如增加從節點、調整複製緩衝區大小等。
Redis集群網路抖動導致頻繁主從切換怎麼處理
真實世界機房網路往往不是風平浪靜的。
為解決這種問題Redis Cluster提供一種選項cluster-node-timeout,其作用是當某個節點連續timeout的時間才失聯,才可以認定該節點出現故障需要主從切換,如果這個選項設置的timeout時間較低或者為0,網路抖動會導致頻繁切換(數據也需要重覆複製)。
Redis集群支持批量操作命令嗎
可以,mset、mget等命令支持多個key的原生批量操作命令,但是redis集群只支持所操作的key都落在同一個slot的情況。
如果多個key不在同一個slot,則命令會報錯。
如果有多個key一定要用mset命令在集群中操作,這可以在key的前面加上相同的{XXX}(hash tag),這樣進行hash分片演算法時只會使用大括弧里的值進行計算,可以保證不同的key能落到同一個slot中。
mset {user1}:1:name zhuge {user1}:1:age 18
Lua腳本能在Redis集群里執行嗎
Redis官方規定Lua腳本如果想在Redis集群里執行,需要Lua腳本里操作的所有Redis Key落在集群的同一個節點上,這種的話我們可以給Lua腳本的Key前面加一個相同的hash tag,就是{XXX},這樣就能保證Lua腳本里所有Key落在相同的節點上了。
分散式鎖
redisson分散式鎖實現原理
Redissson中的分散式鎖主要是基於Redlock演算法實現的,其具體實現原理可以概括為:
- 獲取鎖
在獲取分散式鎖時,Redisson會使用Redis的SETNX
命令去設置一個鎖的key(註意要添加過期時間,否則當setnx鎖住的線程宕機那麼其他線程永遠無法得到這個鎖)。如果設置成功,表示獲取鎖,設置失敗則未獲取鎖。
SETNX
命令實際上就就是為瞭解決java的synchronized鎖只能鎖本地,對於分散式的服務無法上鎖。
- 隨機等待
設置失敗時,會根據超時時間生成一個隨機等待時間,等待後再次嘗試獲取鎖。這樣可以避免多個節點在同一時刻重覆請求鎖。
- 檢查占有
成功獲取到鎖後,會啟動一個定時續期線程,周期性地續期鎖的超時時間(添加子線程,每十秒確認線程是否線上,如果線上則重設過期時間),以避免鎖被自動釋放。
這是由於當業務處理時間大於鎖的過期時間,需要延長鎖的周期,防止在業務處理的過程中鎖被釋放。
- 釋放鎖
釋放鎖時,會發送一條Lua腳本,這段腳本可以保證只有鎖真正的占有者才能夠釋放鎖。釋放時需要判斷是否是某個id的值,且占有鎖的時間內才可以。(給鎖加唯一的ID(UUID))
這是防止一個線程處理完業務,釋放鎖時釋放的是別的線程持有的鎖。
- 失敗重試
獲取或釋放鎖失敗時,會重試一定次數,以便在併發情況下能夠重試成功。
- 解決死鎖
如果一個節點在指定時間內一直失敗,會通過讓當前線程睡眠一段時間來避免死鎖。
以上加鎖、占有、釋放、重試的流程,可以保證分散式環境下Redisson鎖的安全性和可靠性。
RLock lock = redisson.getLock(LOCK_KEY);
lock.lock() ;
這種方式有一個問題,就是當加鎖是給一組主從節點加鎖時,是直接存儲在主節點的。由於redis的AP特性,只支持高性能,高可用,不支持高一致性,因此如果此時主節點宕機,從節點沒來得及同步主節點的信息,那麼會導致鎖的加鎖失敗,因此就有了紅鎖(Redlock),redlock會將鎖強制記錄給所有主從節點。
Redlock 紅鎖
至少三個redis節點,setnx命令,給所有節點加鎖,半數以上的節點加鎖成功才會加鎖成功。
缺點:
- 如果每個節點添加一個從節點,在加鎖key的過程中其中一個節點掛了,換成其從節點頂替該節點,那麼別的進程就會得不到這個節點的鎖。如果要redlock高可用,那麼就需要更多的單點伺服器。
- redis每秒鐘持久化一次,某個節點在持久化的1秒鐘時間還沒到的情況下,有可能還沒加鎖成功就掛了,運維重啟這個節點,恢復的數據是沒有加鎖的,這樣會導致併發問題。
- 解決方案:每執行一條命令就持久化一次。
大廠線上大規模數據的緩存
只有1%的商品是經常被訪問的,因此對數據設置過期時間,比如一天的時間。商品數據被訪問就延長過期時間。
冷熱數據分離,熱點數據放在緩存,冷數據走資料庫。
緩存雪崩
什麼是緩存雪崩?
當某一個時刻出現大規模的緩存失效的情況,那麼就會導致大量的請求直接打在資料庫上面,導致資料庫壓力巨大,如果在高併發的情況下,可能瞬間就會導致資料庫宕機。這時候如果運維馬上又重啟資料庫,馬上又會有新的流量把資料庫打死。這就是緩存雪崩。
分析
造成緩存雪崩的關鍵在於在同一時間大規模的key失效。為什麼會出現這個問題呢,有幾種可能,第一種可能是Redis宕機,第二種可能是採用了相同的過期時間。搞清楚原因之後,那麼有什麼解決方案呢?
解決方案
1、在原有的失效時間上加上一個隨機值,比如1-5分鐘隨機。這樣就避免了因為採用相同的過期時間導致的緩存雪崩。
如果真的發生了緩存雪崩,有沒有什麼兜底的措施?
2、使用熔斷機制。當流量到達一定的閾值時,就直接返回“系統擁擠”之類的提示,防止過多的請求打在資料庫上。至少能保證一部分用戶是可以正常使用,其他用戶多刷新幾次也能得到結果。
3、提高資料庫的容災能力,可以使用分庫分表,讀寫分離的策略。
4、為了防止Redis宕機導致緩存雪崩的問題,可以搭建Redis集群,提高Redis的容災性。
緩存擊穿問題
什麼是緩存擊穿
熱點數據失效,大量請求請求到資料庫。
其實跟緩存雪崩有點類似,緩存雪崩是大規模的key失效,而緩存擊穿是一個熱點的Key,有大併發集中對其進行訪問,突然間這個Key失效了,導致大併發全部打在資料庫上,導致資料庫壓力劇增。這種現象就叫做緩存擊穿。
解決方案
1、上面說過了,如果業務允許的話,對於熱點的key可以設置永不過期的key。
2、使用互斥鎖。如果緩存失效的情況,只有拿到鎖才可以查詢資料庫,降低了在同一時刻打在資料庫上的請求,防止資料庫打死。當然這樣會導致系統的性能變差。
緩存穿透
什麼是緩存穿透?
我們使用Redis大部分情況都是通過Key查詢對應的值,假如發送的請求傳進來的key是不存在Redis中的,那麼就查不到緩存,查不到緩存就會去資料庫查詢。假如有大量這樣的請求,這些請求像“穿透”了緩存一樣直接打在資料庫上,這種現象就叫做緩存穿透。
分析
關鍵在於在Redis查不到key值,這和緩存擊穿有根本的區別,區別在於緩存穿透的情況是傳進來的key在Redis中是不存在的。假如有黑客傳進大量的不存在的key,那麼大量的請求打在資料庫上是很致命的問題,所以在日常開發中要對參數做好校驗,一些非法的參數,不可能存在的key就直接返回錯誤提示,要對調用方保持這種“不信任”的心態。
解決方案
1、把無效的Key存進Redis中。如果Redis查不到數據,資料庫也查不到,我們把這個Key值保存進Redis,設置value="null",當下次再通過這個Key查詢時就不需要再查詢資料庫。這種處理方式肯定是有問題的,假如傳進來的這個不存在的Key值每次都是隨機的,那存進Redis也沒有意義。
2、使用布隆過濾器。布隆過濾器的作用是某個 key 不存在,那麼就一定不存在,它說某個 key 存在,那麼很大可能是存在(存在一定的誤判率)。於是我們可以在緩存之前再加一層布隆過濾器,在查詢的時候先去布隆過濾器查詢 key 是否存在,如果不存在就直接返回。
突發性熱點緩存重建導致系統壓力暴增問題
大V帶貨,一般是冷門數據,熱點商品一般不需要大v帶貨,由於冷門數據不在緩存,因此會導致大量訪問直接請求到資料庫。
加鎖:
第一個請求進同步代碼塊會新建key,後面的請求排隊過來就不會一直請求資料庫,而是直接從緩存中查找了。寫成單例設計模式中的DCL的形式。
public Product get(Long productId) {
Product product = null;
string productCacheKey = RedisKeyPrefixConst.PRODUCT_CACHE + productId;
product = getProductFromcache(productCacheKey);
if (product !=null ){
return product;
}
RLock hotCacheCreateLock = redisson.getLock( LOCK_HOT_CACHE_CREATE_PREFIX + productTd);
hotCacheCreateLock.lock();
try {
product = getProductFromCache(productCacheKey);
if (product != null) {
return product;
}
product = productDao. get(productId);
if (product != null) {
redisutil.set(productCacheKey,JSON.toJSoNstring(product),
genProductCacheTimeout(0),TimeUnit.SECONDS);
}else {
redisUtil.set(productCacheKey,
EMPTY_CACHE,
genEmptyCacheTimeout(),
TimeUnit.SECONDS);
}finally {
hotCacheCreateLock. unlockO);
}
return product;
}
缺點:
- synchronized鎖是本地jvm的鎖。
- 如果有兩個直播間:A和B,直播間A的商品上鎖,B的商品也需要等待。即synchronized(對象){}
還要維護一個商品id對象,不同的商品用不同(商品id對象)的鎖方式。或者使用分佈鎖,使用分佈鎖,所有問題都解決了
public static final String Lock_HOT_CREATE_PREFIX="lock:hot_cache_create";
redisson.getLock(Lock_HOT_CREATE_PREFIX+productId);
hotCacheCreateLock.lock();
try{
// 邏輯代碼 從緩存中獲取key,如果獲取不到就查資料庫。
// ......
}final{
hotCacheCreateLock.unlock(); // del(lockKey)
}
分散式鎖解決資料庫和緩存雙寫不一致問題
線程3在更新緩存前,線程2執行了寫資料庫和更新緩存操作,結果緩存和資料庫不一致
刪除緩存的方法也不行,因為本來線程3就是緩存為空,就是要查資料庫給key賦值。
解決方案
分散式鎖。問題是由於併發讀寫同一條數據導致的。
讀緩存和查找緩存都使用同一把鎖,一個線程拿到鎖另一個線程不能修改數據
public Product get(Long productId) {
Product product = null;
string productCacheKey = RedisKeyPrefixConst.PRODUCT_CACHE + productId;
product = getProductFromcache(productCacheKey);
if (product !=null ){
return product;
}
RLock hotCacheCreateLock = redisson.getLock( LOCK_HOT_CACHE_CREATE_PREFIX + productTd);
hotCacheCreateLock.lock();
try {
product = getProductFromCache(productCacheKey);
if (product != null) {
return product;
}
RLock updateProductLock =
redisson.getLock( LOCK_PRODUCT_UPDATE_PREFIX + productId);
updateProductLock.lock();
try {
product = productDao. get(productId);
if (product != null) {
redisutil.set(productCacheKey,JSON.toJSoNstring(product),
genProductCacheTimeout(0),TimeUnit.SECONDS);
}else {
redisUtil.set(productCacheKey,
EMPTY_CACHE,
genEmptyCacheTimeout(),
TimeUnit.SECONDS);
} finally {
updateProductLock.unlock();
}finally {
hotCacheCreateLock. unlockO);
}
return product;
}
public Product update(Product product) {
Product productResult = null;
RLock updateProductLock = redisson.getLock( name LOCK_PROOUCT_UPOATE_PREFIX + product.getId())updateProductLock.lock();
try {
productResult =productDao. update(product);
redisUtil.set( key:.RedisKeyPrefixConst.PROOUCT_CACHE + productResult.getTd(),JSOM . to3SONString(productResult)
genProductCacheTimeout(),TimeUnit.SECONDS);
}finally {
updateProductLock.unloek(;
}
return productResult;
}
優化性能
大部分場景讀多寫少
- 基於讀寫鎖做優化。
對同一個key加兩個鎖,一個讀鎖一個寫鎖,都是讀的時候可以並行執行,有寫操作的時候加寫鎖,和讀鎖互斥,讀的線程都要等待
RReadWriteLock readWIiteLock =
nedisson.getReadlWIniteLock(LOCK_PRODUCT_UPDATE_PREFIX + productIo);
RLock writeLock = readWriteLock.writeLock();
- tryLock(time,unit);
- time是最大等待鎖的時間,超過這個時間就不等了
- unit是時間單位
time秒之後就不等了
利用多級緩存架構解決緩存雪崩問題
可以用jvm緩存,比如hashmap
public static HashMap<String,Product> productMap=new HashMap<>();
private Product getProductFromCache(String productCacheKey) {
Product product = null;
product = productMap.get(productCacheKey);if (product != null) {
return product;
}
// 邏輯代碼:使用redis緩存再查數據
}
mysql和redis怎麼保持數據一致性
一般情況下,Redis用來實現應用和資料庫之間讀操作的緩存層,主要目的是減少資料庫IO,還可以提升數據的IO性能。
當應用程式需要去讀取某個數據的時候,首先會先嘗試去 Redis裡面載入,如果命中就直接返回。如果沒有命中,就從資料庫查詢,查詢到數據後再把這個數據緩存到Redis裡面。
在這樣一個架構中,會出現一個問題,就是一份數據,同時保存在資料庫和 Redis裡面,當數據發生變化的時候,需要同時更新Redis和 Mysql,由於更新是有先後順序的,並且它不像Mysql中的多表事務操作,可以滿足ACID特性。所以就會出現數據一致性問題。
在這種情況下,能夠選擇的方法只有幾種。
- 先更新資料庫,再更新緩存
- 先刪除緩存,再更新資料庫
如果先更新資料庫,再更新緩存,如果緩存更新失敗,就會導致資料庫和 Redis中的數據不一致。
如果是先刪除緩存,再更新資料庫,理想情況是應用下次訪問Redis 的時候,發現Redis裡面的數據是空的,就從資料庫載入保存到Redis裡面,那麼數據是一致的。但是在極端情況下,由於刪除Redis和更新資料庫這兩個操作並不是原子的,所以這個過程如果有其他線程來訪問,還是會存在數據不一致問題。
所以,如果需要在極端情況下仍然保證Redis和 Mysql的數據一致性,就只能採用最終一致性方案。
比如基於RocketMQ的可靠性消息通信,來實現最終一致性。
還可以直接通過Canal組件,監控Mysql中 binlog 的日誌,把更新後的數據同步到Redis 裡面。
因為這裡是基於最終一致性來實現的,如果業務場景不能接受數據的短期不一致性,那就不能使用這個方案來做。
以上就是我對這個問題的理解。
RocketMQ是怎麼實現數據一致性的
在面過的幾家大廠中,幾乎每輪的面試官(沒寫錯,幾乎是每輪面試官)都問了同樣一個問題:你們的系統是分散式的系統嗎?
答:是。
面試官:那麼你們分散式的系統是如何解決分散式事務這個問題的呢?也就是如何保證數據的一致性。
答:我們的系統中通過 RocketMQ 的事務消息來保證數據的最終一致性。
面試官:那你說說它是如何來保證數據的最終一致性的?
答:分兩部分來回答,第一部分先回答事務消息的實現流程,第二部分解釋為什麼它能保證數據的最終一致性。
事務消息的實現流程
- 首先服務 A 發送一個半事務消息(也稱 **half **消息)至 MQ 中。
- 為什麼要先發送一個 half 消息呢?這是為了保證服務 A 和 MQ 之間的通信正常,如果無法正常通信,則服務 A 可以直接返回一個異常,也就不用處理後面的邏輯的了。
-
如果 half 消息發送成功,MQ 收到這個 half 消息後,會返回一個 success 響應給服務 A。
-
服務 A 接收到 MQ 返回的 success 響應後,開始處理本地的業務邏輯。
-
提交/commit本地事務
- 如果服務 A 本地事務提交成功,則會向 MQ 中發送 commit,表示將 half 消息提交,MQ 就會執行第 5 步操作;
- 如果服務 A 本地事務提交失敗,則直接回滾本地事務,並向 MQ 中發送 rollback,表示將之前的 half 消息進行回滾,MQ 接收到 rollback 消息後,就會將 half 消息刪除。
- MQ 如果 commit,則將 half 消息寫入到磁碟。.
- 如果 MQ 長時間沒有接收到 commit 或者 rollback 消息,例如:服務 A 在處理本地業務時宕機了,或者發送的 commit、rollback 因為在弱網環境,數據丟失了。那麼 MQ 就會在一定時間後嘗試調用服務 A 提供的一個介面,通過這個介面來判斷 half 消息的狀態。所以服務 A 提供的介面,需要實現的業務邏輯是:通過資料庫中對應數據的狀態來判斷,之前的 half 消息對應的業務是否執行成功。如果 MQ 從這個介面中得知 half 消息執行成功了,那麼 MQ 就會將 half 消息持久化到本地磁碟,如果得知沒有執行成功,那麼就會將 half 消息刪除。
- 服務 B 從 MQ 中消費到對應的消息。
- 服務 B 處理本地業務邏輯,然後提交本地事務。
如何保證數據的最終一致性
實現流程說完了,可能你現在有各種各樣的疑惑?
Q: half 消息是個啥?
A: 它和我們正常發送的普通消息是一樣的,都是存儲在 MQ 中,唯一不同的是 half 在 MQ 中不會立馬被消費者消費到,除非這個 half 消息被 commit 了。(至於為什麼未 commit 的 half 消息無法被消費者讀取到,這是因為在 MQ 內部,對於事務消息而言,在 commit 之前,會先放在一個內部隊列中,只有 commit 了,才會真正將消息放在消費者能讀取到的 topic 隊列中)
Q: 為什麼要先發送 half 消息?
A: 前面已經解釋過了,主要是為了保證服務 A 和 MQ 之間是否能正常通信,如果兩者之間都不能正常通信,後面還玩個錘子,直接返回異常就可以了。
Q: 如果 MQ 接收到了 half 消息,但是在返回 success 響應的時候,因為網路原因,導致服務 A 沒有接收到 success 響應,這個時候是什麼現象?
A: 當服務 A 發送 half 消息後,它會等待 MQ 給自己返回 success 響應,如果沒有接收到,那麼服務 A 也會直接結束,返回異常,不再執行後續邏輯。不執行後續邏輯,這樣服務 A 也就不會提交 commit 消息給 MQ,MQ 長時間沒接收到 commit 消息,那麼它就會主動回調服務 A 的一個介面,服務 A 通過介面,查詢本地數據後,發現這條消息對應的業務並沒有正常執行,那麼就告訴 MQ,這個 half 消息不能 commit,需要 rollback,MQ 知道後,就將 half 消息進行刪除。
Q: 如果服務 A 本地事務執行失敗了,怎麼辦?
A: 服務 A 本地事務執行失敗後,先對自己本地事務進行回滾,然後再向 MQ 發送 rollback 操作。
Q: 服務 A 本地事務提交成功或失敗後,向 MQ 發送的 commit 或者 rollback 消息,因為網路問題丟失了,又該怎麼處理?
A: 和上一個問題一樣,MQ 長時間沒有接收到 half 消息的 commit 或者 rollback 消息,MQ 會主動回調服務 A 的介面,通過這個介面來判斷自己該對這個 half 消息如何處理。
Q: 前面說的全是事務消息的實現流程,這和事務消息如何保證數據的最終一致性有什麼關係呢?
A: 有關係。首先,服務 A 執行本地事務並提交和向 MQ 中發送消息這是兩個寫操作,然後通過 RocketMQ 的事務消息,我們保證了這兩個寫操作要麼都執行成功,要麼都執行失敗。然後讓其他系統,如服務 B 通過消費 MQ 中的消息,然後再去執行自己本地的事務,這樣到最後,服務 A 和服務 B 這兩個系統的數據狀態是不是達到了一致?這就是最終一致性的含義。
而RocketMQ作為一種消息隊列,其本身特點是非同步、解耦,無法保證服務A和服務B在同一時刻的數據強一致性。它只能保證最終一致性。
目前通過可靠消息來保證數據的最終一致性是很多大廠都採用的方案,基本都是通過 MQ 和補償機制來保證數據的一致性。(所謂的可靠消息,就是消息不丟失,如何保證 MQ 的消息不丟失,下篇文章會寫,這也是面試常考題)
Q: 服務 B 本地事務提交失敗了,怎麼辦?
A: 如果服務 B 本地事務提交失敗了,可以進行多次重試,直到成功。如果重試多次後,還是提交失敗,例如此時服務 B 對應的 DB 宕機了,這個時候只要服務 B 不向 MQ 提交本次消息的 offset 即可。如果不提交 offset,那麼 MQ 會在一定時間後,繼續將這條消息推送給服務 B,服務 B 就可以繼續執行本地事務並提交了,直到成功。這樣,依舊是保證了服務 A 和服務 B 數據的最終一致性。
————————————————
版權聲明:本文為CSDN博主「qq_34436819」的原創文章,遵循CC 4.0 BY-SA版權協議,轉載請附上原文出處鏈接及本聲明。
原文鏈接:https://blog.csdn.net/qq_34436819/article/details/114444204