高級JAVA開發 Redis部分

Redis

參考和摘自:
中華石杉 《Java工程師面試突擊第1季》
Redis線程模型(簡單易懂)
Redis線程模型(詳細)
分佈式緩存技術redis系列

緩存的作用、爲什麼要用緩存

高性能、高併發

不加緩存每個請求都會去訪問數據庫,數據庫的性能成了整個系統的瓶頸。如果在業務層加入緩存,相同的數據請求第一次訪問數據庫後放入緩存中,再次請求就從緩存中取得數據,不再深入到數據庫去拿數據。緩存取得數據會比數據庫快得多,這是高性能。如果併發量很大,數據庫承載不住大量的請求,緩存存取速度很快,可以減少數據庫訪問,能適當支撐住大量請求,這是高併發。

Redis和Memcached區別

以下摘自 中華石杉 《Java工程師面試突擊第1季》

Redis支持服務器端的數據操作:Redis相比Memcached來說,擁有更多的數據結構和並支持更豐富的數據操作,通常在Memcached裏,你需要將數據拿到客戶端來進行類似的修改再set回去。這大大增加了網絡IO的次數和數據體積。在Redis中,這些複雜的操作通常和一般的GET/SET一樣高效。所以,如果需要緩存能夠支持更復雜的結構和操作,那麼Redis會是不錯的選擇。

內存使用效率對比:使用簡單的key-value存儲的話,Memcached的內存利用率更高,而如果Redis採用hash結構來做key-value存儲,由於其組合式的壓縮,其內存利用率會高於Memcached。
性能對比:由於Redis只使用單核,而Memcached可以使用多核,所以平均每一個核上Redis在存儲小數據時比Memcached性能更高。而在100k以上的數據中,Memcached性能要高於Redis,雖然Redis最近也在存儲大數據的性能上進行優化,但是比起Memcached,還是稍有遜色。

集羣模式:memcached沒有原生的集羣模式,需要依靠客戶端來實現往集羣中分片寫入數據;但是redis目前是原生支持cluster模式的,redis官方就是支持redis cluster集羣模式的,比memcached來說要更好

Redis五種數據類型和使用場景

  1. String:簡單的String類型,用key取value。
  2. Hash:可以想象成是一個Map,存儲對象,比如一個學生等等。可以取出或修改整個對象,也可以單獨取出或者修改對象中的一個字段。
  3. list:有序列表。可以做通過lrange等指令做分頁。可以在兩端存取元素,基於這個可以實現消息隊列。
  4. set:無序集合,自動去重。可以求兩個set的交集、並集、差集等操作。
  5. sorted set:排序的set,放入元素時候可以指定元素的score,它會按照score順序自動排序。

Redis線程模型(爲什麼Redis是單線程的但是還可以支撐高併發)

純內存、NIO、單線程
純內存操作
核心是基於非阻塞的 IO 多路複用機制
單線程避免了多線程上下文切換帶來的消耗

文件事件處理器(file event handler):
在這裏插入圖片描述
圖引自Redis線程模型 稍加修改

總結:客戶端通過多個Socket發起的連接、寫入、讀取等事件均被IO多路複用程序監聽到,IO多路複用程序把事件放入隊列,文件事件分派器將事件取出後和相應的“處理器”(連接應答處理器、命令請求處理器、命令回覆處理器、複製處理器(主從複製)等等…)關聯並處理,一次只處理一個事件。

I/O 多路複用程序允許服務器同時監聽Socket的 AE_READABLE 事件和 AE_WRITABLE 事件, 如果一個Socket同時產生了這兩種事件, 那麼文件事件分派器會優先處理 AE_READABLE 事件, 等到 AE_READABLE 事件處理完之後, 再處理 AE_WRITABLE 事件。這也就是說, 如果一個Socket即可讀又可寫的話, 那麼服務器將先讀後寫

說明:Redis針對於每個實際操作都是在內存中的,超級快,此處不是Redis瓶頸。單線程每次從隊列中取得一個任務處理避免了使用多線程上下文切換而帶來的不必要消耗,同時也規避了多線程併發帶來的競爭問題(多線程讀寫同一條數據)。Redis瓶頸是網絡IO讀寫,此處採用NIO非阻塞多路複用模型,最大化IO效率。

Redis過期策略、內存回收策略(內存淘汰機制)

定期刪除 + 惰性刪除

  1. 惰性刪除:讀取到帶有過期時間的key時,如果數據已經過期那麼將刪除掉數據並返回空。
  2. 定時任務刪除:Redis內部默認維護了一個1秒10次的定時任務(可以通過配置修改執行頻率)定時任務中刪除過期鍵邏輯採用了自適應算法,根據鍵的過期比例,使用快慢兩種速率模式回收鍵。
    比如:

定時任務在每個數據庫空間隨機檢查20個鍵,當發現過期時刪除對應的鍵。
如果超過檢查數25%的鍵過期,循環執行回收邏輯直到不足25%或運行超時爲止,慢模式下超時時間爲25ms。
如果之前回收鍵邏輯超時,則在Redis觸發內部事件之前再次以快模式運行回收過期鍵任務,快模式下超時時間爲1ms且2s內只能運行1次。
快慢兩種模式內部刪除邏輯相同,只是執行的超時時間不同。

maxmemory-policy參數控制的內存淘汰機制

noeviction:默認策略,當內存不足以容納新寫入數據時,新寫入操作會報錯。
allkeys-lru:當內存不足以容納新寫入數據時,在鍵空間中,移除最近最少使用的 Key。推薦使用。最常用
allkeys-random:當內存不足以容納新寫入數據時,在鍵空間中,隨機移除某個 Key。
volatile-lru:當內存不足以容納新寫入數據時,在設置了過期時間的鍵空間中,移除最近最少使用的 Key。
volatile-random:當內存不足以容納新寫入數據時,在設置了過期時間的鍵空間中,隨機移除某個 Key。
volatile-ttl:當內存不足以容納新寫入數據時,在設置了過期時間的鍵空間中,有更早過期時間的 Key 優先移除。

參考自: 內存溢出控制策略

Redis的高可用架構(主從replication、哨兵、cluster集羣)

待補充

Redis的持久化機制(RDB、AOF)

待補充

使用緩存帶來的問題以及處理辦法

緩存與數據庫雙寫一致性問題

Cache Aside Pattern 原則有以下幾個步驟:

  1. 讀的時候,①先讀緩存,緩存命中則直接返回,緩存沒命中那麼就②讀數據庫,取出數據後③放入緩存,最後返回響應。
  2. 更新的時候,先④刪除緩存,然後再⑤更新數據庫。

在低併發時,上述原則可用。如果高併發,在⑤處產生讀併發,寫請求還沒來得及更新數據庫,讀請求讀取數據庫並把數據放到緩存後,寫請求修改了數據庫。這時由於併發問題導致了數據庫和緩存不一致問題。

解決方案:

  1. 低要求方案(自己想的):更新的時候,先④刪除緩存,再⑤更新數據庫,⑥再刪除一次緩存。對緩存一致性要求不是很高的情況可以採用。在⑥前併發的讀請求拿到的是舊數據,而等⑥刪除緩存後, 讀請求拿到的數據是新的。
  2. 高要求方案:
    思路:由於沒保證操作的原子性而引發了問題。競爭問題解決辦法就是一次只能有一個任務執行,一般兩種思路:
    • 第一種:用鎖控制,拿到鎖的執行,沒拿到的等待。
      Redisson分佈式讀寫鎖可以解決,把②③用讀鎖包裹起來,把④⑤用寫鎖包裹起來。但是存在兩個問題:

      1. Redisson實現的讀寫鎖不是公平的(用一個分佈式公平讀寫鎖,自認爲這個方案是比較完美的~),這裏寫線程數量會大大少於讀線程數量,寫線程數量得到鎖的概率很小很小,修改操作成功率非常低(要麼一直死等下去,要麼超時返回錯誤,再來請求還是超時),這麼做可以保證數據的一致性。
      2. 寫鎖釋放後,此時已經積壓了大量的讀請求,所有讀請求一同拿到鎖後一起查詢數據庫後一起填充緩存,解決這個問題可以再引入一個分佈式互斥鎖,用tryLock方式嘗試獲取鎖,獲取到了執行②③,沒獲取到就自旋訪問緩存,直到緩存被拿到互斥鎖的線程填充。需要考慮一個完整訪問數據庫的操作需要多長時間,在這段時間內有多少請求壓在自旋中,會不會大量佔用服務器cpu資源。個人總結的辦法,由於不是公平讀寫鎖,不是很可行
    • 第二種:把任務放入隊列串行執行。
      建立多個內存隊列,用hash取模的方式將同唯一標識(訂單號、商品ID等)的請求路由到同一個內存隊列中等待執行。如果訂單服務是集羣,需要考慮把同訂單號請求路由到同一個服務器再路由到同一隊列。在隊列中排隊的服務需要有過期時間,防止等待過長時間。在入隊前檢查隊列中前一個同訂單號操作是否是讀操作,若果是直接返回並自旋等待緩存更新。(思路和加鎖相同,這種方式可以讓寫請求有公平的機會執行)比較可行

Redis併發競爭問題

三個服務節點查詢DB後持有三個版本的數據併發更新Redis同一個key,最後的結果由於競爭保證不了最終一致性
解決方案:DB設置數據版本字段(時間戳 等),寫入Redis前先獲取分佈式鎖保證接下來操作的原子性:比較Redis中的時間戳,並寫入比較新版本的數據。
最終所得結果總是最新版本的數據。

緩存雪崩問題

場景:Redis集羣掛掉了或者由於網絡原因部分不可用,導致大量請求落到數據庫上,數據庫被壓垮,導致整個系統不可用。

解決辦法:

  1. 保證Redis採用高可用架構,主從+哨兵,redis cluster,避免全盤崩潰
  2. 系統採用多級緩存,比如用ehcache做應用內緩存。
  3. 用 hystrix 限流、熔斷、降級,避免Mysql被壓死。
  4. Redis配置持久化,如果出故障可以快速恢復。

熱點數據集中失效(另一種雪崩)

場景:緩存中大量key同一時間失效,大量請求同時落到數據庫上。

解決辦法:

  1. 設置過期時間時,在過期時間後加上一個隨機值,避免時間相同。
  2. 同一條數據加互斥鎖,第一個請求可以通過,之後的請求稍等一下並輪訓等待、檢查緩存是否存在。這樣吞吐量會下降,需要考慮場景。

緩存穿透問題

場景:大量請求用不存在的ID訪問,比如查找ID爲-1000的商品。這時候請求先查找緩存,緩存未命中,之後查數據庫,數據庫也未命中,之後返回空結果。 如果再次用這個ID訪問,請求最終還是會落到數據庫上,造成“緩存穿透”。

解決辦法:

  1. 不存在的ID第一次請求後寫入Redis特殊標識返回結果爲UNKNOWN,第二次請求拿到特殊標識後返回。這樣請求不會落到數據庫上了。
  2. 非法key用程序過濾掉,儘量減少。
  3. 利用BloomFilter擋掉不存在的請求。將存在的key計算後存入BloomFilter,取值時先檢查key在BloomFilter是否存在,一定不存在的直接擋掉。

參考:BloomFilter(大數據去重)+Redis(持久化)策略

緩存擊穿問題

場景:對同一個緩存key大量併發,恰巧這個key失效了,請求落庫,壓力劇增。

解決辦法:

  1. 加互斥鎖。第一個請求更新緩存,後續請求等待過程中檢查緩存。比如Redis分佈式鎖。
  2. 不給key設置有效期,永久有效。用定時任務去更新緩存。
  3. 邏輯過期,把過期時間設置在數據中。設置的過期時間略小於實際過期時間。取到值時實際數據並未過期。取值時校驗,過期就另起異步線程更新緩存。當前數據直接返回給前端。

緩存預熱

場景:剛剛啓動項目時沒有緩存,大量請求直接落庫。

解決辦法:

  1. 啓動時候先加載緩存,加載成功後再提供服務。
  2. 寫個定時任務,定時刷新緩存。
  3. 寫定時任務,把頁面刷一遍。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章