快取問題:
1.快取穿透
查詢一個不存在的資料,MySQL中也查詢不到而且也寫不進快取,就導致每次請求都查詢資料庫
解決方案
- 快取空資料,查詢返回資料為空時把空結果快取:缺點是記憶體開銷變大
- 布隆過濾器:透過雜湊檢測一個元素是否存在於集合中:在快取預熱時同時預熱過濾器,將傳進來的key透過相應的hash將資料存入bitmap中,在查詢時使用相同的hash檢視對應位置。缺點是可能會存在誤判的情況。
2.快取擊穿
熱點key突然失效,大量併發請求過來瞬間壓垮DB
解決方案
- 互斥鎖:查詢未命中會建立互斥鎖,然後再寫入快取,寫期間其他執行緒不可以搶奪鎖,需要等待,完成後釋放鎖。其他執行緒等待休眠重試(強一致性,低可用性)
- 邏輯過期:熱點key不設定實際過期時間,但設定邏輯過期時間。當key邏輯過期後,第一個請求會獲取互斥鎖,新開闢一個執行緒來重寫快取並重置過期時間。重寫過程中原執行緒會直接返回過期資料,其他執行緒請求時,由於獲取互斥鎖失敗,也會直接返回過期資料。當寫快取執行緒完成時會釋放互斥鎖,這時其他執行緒可正常訪問(高可用,效能好)
3.快取雪崩
同一時段內大量key同時失效,或者redis當機了,導致請求都打在DB上
解決方案
給不同key的ttl設隨機值
設計redis叢集:哨兵模式叢集模式
給快取新增降級限流策略
新增多級快取
4.雙寫一致性(這裡要根據簡歷上的業務來選擇一致性再選擇下列方案)
強一致性:
- 寫操作採用延遲雙刪:刪除快取------>修改資料庫------>(延時)-------->刪除快取:在高併發的情況下,不論是先刪資料庫還是先刪快取都會出現髒資料。延時雙刪能有效控制髒資料產生的問題
- 採用分散式鎖:redisson提供了讀寫鎖,在寫操作的時候上寫鎖
最終一致性:
- 採用非同步通知
- 透過阿里的canal
5.持久化
RDB,就是把記憶體中的所有資料都記錄在磁碟中。採用bgsave命令,使用子程序非同步完成
AOF,記錄每一次執行命令,相對於RDB記錄較為,但是磁碟空間佔的多,資料安全性更高
6.資料過期策略
惰性刪除:訪問key的時候再看是否過期,如果過期就刪除
定期刪除:定期檢查一定量key是否過期(分為fast和slow兩種,fast頻率不固定,耗時和間隔都極短;slow則是定時任務,具有預設頻率)一般情況下是使用fast+slow模式
7.資料淘汰策略
預設是不淘汰任何key,但是記憶體滿了之後不會允許新資料寫入。allkeys / volatile-lru 對所有key/有ttl的key基於LRU淘汰,allkeys / volatile-lfu 對所有key/有ttl的key基於LFU淘汰
分散式鎖問題:
傳統的synchronized只能管控當前程序,一旦進入分散式叢集就會容易失效;分散式鎖的解決了這個問題
- setnx:(set if not exists)獲取鎖:SET lock value NX EX 10(NX:只有不存在的時候才可以set,EX為過期時間);釋放鎖:del lock。
- 為什麼要加過期時間:防止死鎖(業務超時或者服務當機)
- 為什麼要用一條指令而不是setnx+過期時間:為了保證原子性
- 鎖的有效時長如何控制:1.根據業務評估鎖的時間(較難)2.給鎖續期(需要新開一個執行緒,比較麻煩)
- redisson:透過setnx加鎖,另開一個執行緒watchdog進行監控,他會不斷監控鎖給鎖續期。當釋放鎖的時候也會通知watchdog,告訴他鎖釋放了。當新執行緒企圖獲取鎖時,他會一直while迴圈等待鎖釋放再加鎖,不過迴圈也是有閾值的
- 如何使用redisson:
- 透過RLock lock=redissonClient.getLock("key")獲取鎖
- 透過boolean isLock=lock.tryLock("等待時間","失效時間","時間的單位") (如果這裡不傳任何值,那麼就預設用watchdog續期)
- isLock為true的時候就是鎖獲取成功了
- 注意:這裡的加鎖,過期時間都是透過lua指令碼實現的,使用lua指令碼是為了保持原子性
- redisson支援在同一個執行緒內可重入鎖(鎖裡還能再加相同的鎖)
- redisson的主從一致性:redisson透過紅鎖redlock實現了主從一致性,他要求至少要在n/2+1的節點上有建立鎖。但是紅鎖效能差,執行復雜,官方都不建議使用。redis主要是基於AP思想,追求的是高可用;如果需要強一致性那麼需要選擇基於CP思想的zookeeper
- 如何使用redisson:
叢集問題:
主從複製:提高讀寫效能。主負責寫操作,從負責讀操作:讀寫分離。一般情況下1主1從+哨兵叢集就夠用了
- 如何保證資料同步:兩個前提概念:replication_id:master有唯一的replicationId,而slave會複製master的id,因此同一個叢集的ID肯定是一樣的,offset:偏移量
- 全量同步(第一次請求,replicationID不一致):從節點執行replicaof與主節點建立連線,如果是第一次同步則返回主節點版本資訊,此時主節點執行bgsave將生成的rdb傳送給從節點,從節點刪除本地資料後載入rdb。執行期間的命令主節點也會打包成日誌,傳送給從節點執行。
- 增量同步(非第一次請求,攜帶的replicationID一致):從節點重啟或資料變化會發生增量同步:攜帶replication和offset,只有在replicationId一致的時候才會繼續。在repl_baklog取出offset後的資料,傳送命令讓從節點執行
哨兵模式:保證主從叢集的高可用,實現叢集的自動恢復並且會給客戶端實時推送故障轉移的訊息
- sentinel透過redis的pingpong來對節點進行主觀下線檢測,如果超過配置數量的redis都檢測不到對應節點,那就是客觀下線,說明redis掛了
- 恢復:判斷主從節點斷開的時長,超過指定值則排除該節點;判斷從節點的slave-priority,值越小優先順序越高,相同的則判斷offset,越大優先順序越高;最後是判斷slave的執行id,越小優先順序越高
- 腦裂:master,slave,sentinel處於不同的網路分割槽:主節點沒掛,還能正常用;但是由於網路問題被sentinel判定為掛了,這時會出現兩個master並且資料依舊會寫入老master。當網路恢復後,老master會成為slave,由於主從複製,新寫入的資料會全部丟失(被新master裡的舊資料覆蓋了)
- 有兩個配置可以解決該問題:
- min-replicas-to-writes 1 #表示至少要有一個slave,master才能寫
- min-replicas-max-lag 5 #表示資料複製和同步延時不能超過5秒
叢集模式:
- 海量資料儲存與高併發寫:叢集中有多個master,每個master可以有多個slave,master互為sentinel監測是否存活
- 透過引入雜湊槽的概念,每個節點負責一部分雜湊,對應的key透過計算hash值後會放入對應的master
redis為什麼這麼快:
1.redis是純記憶體操作
2.採用單執行緒,避免了上下文切換,而且不用考慮執行緒安全問題
3.使用I/O多路複用模型+事件派發
redis的效能瓶頸不在於執行速度,而是網路延遲,而IO多路複用模型主要是實現了高效的網路請求
使用者空間和核心空間:使用者空間:只能執行部分命令; 核心空間:執行特權命令,呼叫一切系統資源。
三種常見的IO模型
- 阻塞IO模型(BIO):
- 一階段:使用者程序嘗試讀取資料,資料尚未到達核心需要阻塞等待
- 二階段:資料到達並複製到核心緩衝區,核心緩衝區再複製到使用者緩衝區,複製完成後程序才會停止阻塞,處理資料
- 非阻塞IO模型(NIO):
- 一階段:使用者程序嘗試讀取資料,資料尚未到達核心,會直接給使用者返回異常,使用者會開啟輪詢
- 二階段:核心資料複製到使用者緩衝區,複製完成後程序停止阻塞,處理資料
- 第一階段雖然是非阻塞的,但是由於忙等機制會使CPU使用率暴增
- IO多路複用模型:
- 利用單執行緒監聽多個socket,在某個socket可讀可寫時通知,避免無效等待。
- 其中select和poll模式只會通知使用者程序有socket就緒,需要使用者程序逐個遍歷socket來確認;而epoll會在通知的同時把已就緒的socket寫入使用者空間,下面以select為例
- 一階段:使用者程序呼叫select,指定要監聽的socket集合,有socket資料就緒即返回readable,此過程中使用者程序阻塞;
- 二階段:使用者找到就緒的socket,依次呼叫revcvfrom讀取資料,核心將資料複製至使用者空間
事件派發:
redis中提供了多個事件處理器,處理器分別用於實現不同的網路通訊請求。
最經典的三個是:
- 連線應答處理器(處理客戶端請求應答)
- 命令回覆處理器(處理客戶端響應)
- 命令請求處理器(接收資料,把資料轉為redis命令,再把結果寫入緩衝佇列,再透過命令回覆處理器處理客戶端響應)
redis6.0引入了多執行緒,為了命令回覆處理器以及命令請求處理器的解析部分新增了多執行緒。