1. 程式人生 > >高並發之緩存

高並發之緩存

ava java 時間計算 ren 熱點 獨立 red 面向對象思想 src

應用需要支撐大量並發量,但數據庫的性能有限,所以使用緩存來減少數據庫壓力與提高訪問性能。

技術分享圖片

緩存的使用可以出現在1到4的各個環節中,每個環節的方案他們都各有特點。

特征

  • 命中率 = 命中數 / (命中數 + 沒有命中數)
  • 最大空間:緩存最大空間一旦緩存中元素數量超過這個值(或者緩存數據所占空間超過其最大支持空間),那麽將會觸發緩存啟動清空策略根據不同的場景合理的設置最大元素值往往可以一定程度上提高緩存的命中率,從而更有效的時候緩存。
  • 清空策略:FIFO/LFU/LRU/過期時間/隨機

FIFO:最先進入緩存的數據,在緩存空間不足時被清除,為了保證最新數據可用,保證實時性

LFU(Least Frequently Used):最近最不常用,基於訪問次數,去除命中次數最少的元素,保證高頻數據有效性

LRU(Least Recently Used):最近最少使用,基於訪問時間,在被訪問過的元素中去除最久未使用的元素,保證熱點數據的有效性

影響緩存命中率的因素:

1.業務場景和業務需求

緩存通常適合讀多寫少的業務場景,反之的使用意義並不多,命中率會很低。業務需求也決定了實時性的要求,直接影響到過期時間和更新策略,實時性要求越低越適合緩存。

2.緩存的設計(策略和粒度)

通常情況下緩存的粒度越小,命中率越高。比如說緩存一個用戶信息的對象,只有當這個用戶的信息發生變化的時候才更新緩存,而如果是緩存一個集合的話,集合中任何一個對象發生變化都要重新更新緩存。

當數據發生變化時,直接更新緩存的值比移除緩存或者讓緩存過期它的命中率更高,不過這個時候系統的復雜度過高。

3.緩存的容量和基礎設施

緩存的容量有限就會容易引起緩存的失效和被淘汰。目前多數的緩存框架和中間件都采用LRU這個算法。同時采用緩存的技術選型也是至關重要的,比如采用本地內置的應用緩存,就比較容易出現單機瓶頸。而采用分布式緩存就更加容易擴展。所以需要做好系統容量規劃,系統是否可擴展。

緩存介質

雖然從硬件介質上來看,無非就是內存和硬盤兩種,但從技術上,可以分成內存、硬盤文件、數據庫。

  • 內存:將緩存存儲於內存中是最快的選擇,無需額外的I/O開銷,但是內存的缺點是沒有持久化落地物理磁盤,一旦應用異常break down而重新啟動,數據很難或者無法復原。
  • 硬盤:一般來說,很多緩存框架會結合使用內存和硬盤,在內存分配空間滿了或是在異常的情況下,可以被動或主動的將內存空間數據持久化到硬盤中,達到釋放空間或備份數據的目的。
  • 數據庫:前面有提到,增加緩存的策略的目的之一就是為了減少數據庫的I/O壓力。現在使用數據庫做緩存介質是不是又回到了老問題上了?其實,數據庫也有很多種類型,像那些不支持SQL,只是簡單的key-value存儲結構的特殊數據庫(如BerkeleyDB和Redis),響應速度和吞吐量都遠遠高於我們常用的關系型數據庫等。

根據緩存和應用的耦合度

技術分享圖片

本地緩存最大的優點在於它在應用進程的內部,請求緩存非常的快速,沒有過多的網絡開銷。在單應用中不需要集群支持,集群的情況下各節點不需要互相通知的情況下使用本地緩存比較合適。

缺點是因為本地緩存跟應用程序耦合,多個應用程序無法直接共享緩存,各應用節點都需要維護自己單獨的緩存,有時也是對內存的一種浪費。

分布式緩存指的是應用分離的緩存服務,最大的優點就是自身就是一個獨立的應用,與本地應用是隔離的,多個應用直接共享緩存。

本地緩存Guava Cache

Guava Cache是Google開源的Java重用工具集庫Guava裏的一款緩存工具,它的設計靈感是CuncurentHashMap

技術分享圖片

Guava Cache繼承了ConcurrentHashMap的思路,使用多個segments方式的細粒度鎖,在保證線程安全的同時,支持高並發場景需求。Cache類似於Map,它是存儲鍵值對的集合,不同的是它還需要處理evict、expire、dynamic load等算法邏輯,需要一些額外信息來實現這些操作。對此,根據面向對象思想,需要做方法與數據的關聯封裝.

其主要實現的緩存功能有:

  • 自動將entry節點加載進緩存結構中;
  • 當緩存的數據超過設置的最大值時,使用LRU算法移除;
  • 具備根據entry節點上次被訪問或者寫入時間計算它的過期機制;
  • 緩存的key被封裝在WeakReference引用內;
  • 緩存的Value被封裝在WeakReference或SoftReference引用內;
  • 統計緩存使用過程中命中率、異常率、未命中率等統計數據。

memcached是應用較廣的開源分布式緩存產品之一,它本身其實不提供分布式解決方案。在服務端,memcached集群環境實際就是一個個memcached服務器的堆積,環境搭建較為簡單;cache的分布式主要是在客戶端實現,通過客戶端的路由處理來達到分布式解決方案的目的。客戶端做路由的原理非常簡單,應用服務器在每次存取某key的value時,通過某種算法把key映射到某臺memcached服務器nodeA上,因此這個key所有操作都在nodeA上,結構圖如圖6、圖7所示。

技術分享圖片

memcached客戶端路由圖

技術分享圖片

memcached一致性hash示例圖

memcached客戶端采用一致性hash算法作為路由策略,如圖7,相對於一般hash(如簡單取模)的算法,一致性hash算法除了計算key的hash值外,還會計算每個server對應的hash值,然後將這些hash值映射到一個有限的值域上(比如0~2^32)。通過尋找hash值大於hash(key)的最小server作為存儲該key數據的目標server。如果找不到,則直接把具有最小hash值的server作為目標server。同時,一定程度上,解決了擴容問題,增加或刪除單個節點,對於整個集群來說,不會有大的影響。最近版本,增加了虛擬節點的設計,進一步提升了可用性。

memcached是一個高效的分布式內存cache,了解memcached的內存管理機制,才能更好的掌握memcached,讓我們可以針對我們數據特點進行調優,讓其更好的為我所用。我們知道memcached僅支持基礎的key-value鍵值對類型數據存儲。在memcached內存結構中有兩個非常重要的概念:slab和chunk。

技術分享圖片

每個page的默認大小是1M,trunk是真正存放數據的地方,memcache會根據value值的大小找到接近大小的slab

Redis緩存

Redis是一個遠程內存數據庫(非關系型數據庫),性能強勁,具有復制特性以及解決問題而生的獨一無二的數據模型。它可以存儲鍵值對與5種不同類型的值之間的映射,可以將存儲在內存的鍵值對數據持久化到硬盤,可以使用復制特性來擴展讀性能,還可以使用客戶端分片來擴展寫性能。

技術分享圖片

Redis具備以下特點:

  • 異常快速: Redis數據庫完全在內存中,因此處理速度非常快,每秒能執行約11萬集合,每秒約81000+條記錄。
  • 數據持久化: redis支持數據持久化,可以將內存中的數據存儲到磁盤上,方便在宕機等突發情況下快速恢復。
  • 支持豐富的數據類型: 相比許多其他的鍵值對存儲數據庫,Redis擁有一套較為豐富的數據類型。
  • 數據一致性: 所有Redis操作是原子的,這保證了如果兩個客戶端同時訪問的Redis服務器將獲得更新後的值。
  • 多功能實用工具: Redis是一個多實用的工具,可以在多個用例如緩存,消息,隊列使用(Redis原生支持發布/訂閱),任何短暫的數據,應用程序,如 Web應用程序會話,網頁命中計數等。

適用場景:

1.取最新N個數據的操作

2.排行榜類似的應用

3.精準設定過期時間的應用

4.計數器的應用

5.唯一性檢查

6.實時系統,隊列系統,最基礎的緩存功能

技術分享圖片

1緩存一致性問題

當數據時效性要求很高時,需要保證緩存中的數據與數據庫中的保持一致,而且需要保證緩存節點和副本中的數據也保持一致,不能出現差異現象。這就比較依賴緩存的過期和更新策略。一般會在數據發生更改的時,主動更新緩存中的數據或者移除對應的緩存。

技術分享圖片

2.緩存並發問題

緩存過期後將嘗試從後端數據庫獲取數據,這是一個看似合理的流程。但是,在高並發場景下,有可能多個請求並發的去從數據庫獲取數據,對後端數據庫造成極大的沖擊,甚至導致 “雪崩”現象。此外,當某個緩存key在被更新時,同時也可能被大量請求在獲取,這也會導致一致性的問題。那如何避免類似問題呢?我們會想到類似“鎖”的機制,在緩存更新或者過期的情況下,先嘗試獲取到鎖,當更新或者從數據庫獲取完成後再釋放鎖,其他的請求只需要犧牲一定的等待時間,即可直接從緩存中繼續獲取數據。

技術分享圖片

3.緩存穿透問題

緩存穿透在有些地方也稱為“擊穿”。很多朋友對緩存穿透的理解是:由於緩存故障或者緩存過期導致大量請求穿透到後端數據庫服務器,從而對數據庫造成巨大沖擊。

這其實是一種誤解。真正的緩存穿透應該是這樣的:

在高並發場景下,如果某一個key被高並發訪問,沒有被命中,出於對容錯性考慮,會嘗試去從後端數據庫中獲取,從而導致了大量請求達到數據庫,而當該key對應的數據本身就是空的情況下,這就導致數據庫中並發的去執行了很多不必要的查詢操作,從而導致巨大沖擊和壓力。

技術分享圖片

可以通過下面的幾種常用方式來避免緩存傳統問題:

緩存空對象

對查詢結果為空的對象也進行緩存,如果是集合,可以緩存一個空的集合(非null),如果是緩存單個對象,可以通過字段標識來區分。這樣避免請求穿透到後端數據庫。同時,也需要保證緩存數據的時效性。這種方式實現起來成本較低,比較適合命中不高,但可能被頻繁更新的數據。

單獨過濾處理

對所有可能對應數據為空的key進行統一的存放,並在請求前做攔截,這樣避免請求穿透到後端數據庫。這種方式實現起來相對復雜,比較適合命中不高,但是更新不頻繁的數據。

4.緩存顛簸問題

緩存的顛簸問題,有些地方可能被成為“緩存抖動”,可以看做是一種比“雪崩”更輕微的故障,但是也會在一段時間內對系統造成沖擊和性能影響。一般是由於緩存節點故障導致。業內推薦的做法是通過一致性Hash算法來解決。這裏不做過多闡述。

5.緩存的雪崩現象

緩存雪崩就是指由於緩存的原因,導致大量請求到達後端數據庫,從而導致數據庫崩潰,整個系統崩潰,發生災難。導致這種現象的原因有很多種,上面提到的“緩存並發”,“緩存穿透”,“緩存顛簸”等問題,其實都可能會導致緩存雪崩現象發生。這些問題也可能會被惡意攻擊者所利用。還有一種情況,例如某個時間點內,系統預加載的緩存周期性集中失效了,也可能會導致雪崩。為了避免這種周期性失效,可以通過設置不同的過期時間,來錯開緩存過期,從而避免緩存集中失效。

從應用架構角度,我們可以通過限流、降級、熔斷等手段來降低影響,也可以通過多級緩存來避免這種災難。

此外,從整個研發體系流程的角度,應該加強壓力測試,盡量模擬真實場景,盡早的暴露問題從而防範。

技術分享圖片

高並發之緩存