1. 程式人生 > >詳解Memcached、Redis等快取的特徵、原理、應用

詳解Memcached、Redis等快取的特徵、原理、應用

詳解Memcached、Redis等快取的特徵、原理、應用

http://youzhixueyuan.com/explain-the-principles-of-memcached-and-redis.html

http://youzhixueyuan.com/advanced-architect-application-scenarios-selection-comparison-problems-of-distributed-caching.html

快取特徵

快取也是一個數據模型物件,那麼必然有它的一些特徵:

1.命中率

命中率=返回正確結果數/請求快取次數,命中率問題是快取中的一個非常重要的問題,它是衡量快取有效性的重要指標。命中率越高,表明快取的使用率越高。

2.最大元素(或最大空間)

快取中可以存放的最大元素的數量,一旦快取中元素數量超過這個值(或者快取資料所佔空間超過其最大支援空間),那麼將會觸發快取啟動清空策略根據不同的場景合理的設定最大元素值往往可以一定程度上提高快取的命中率,從而更有效的時候快取。

3.清空策略

如上描述,快取的儲存空間有限制,當快取空間被用滿時,如何保證在穩定服務的同時有效提升命中率?這就由快取清空策略來處理,設計適合自身資料特徵的清空策略能有效提升命中率。常見的一般策略有:

  •  FIFO(first in first out)
  •  先進先出策略,最先進入快取的資料在快取空間不夠的情況下(超出最大元素限制)會被優先被清除掉,以騰出新的空間接受新的資料。策略演算法主要比較快取元素的建立時間。在資料實效性要求場景下可選擇該類策略,優先保障最新資料可用。
  •  LFU(less frequently used)
  •  最少使用策略,無論是否過期,根據元素的被使用次數判斷,清除使用次數較少的元素釋放空間。策略演算法主要比較元素的hitCount(命中次數)。在保證高頻資料有效性場景下,可選擇這類策略。
  •  LRU(least recently used)
  •  最近最少使用策略,無論是否過期,根據元素最後一次被使用的時間戳,清除最遠使用時間戳的元素釋放空間。策略演算法主要比較元素最近一次被get使用時間。在熱點資料場景下較適用,優先保證熱點資料的有效性。

除此之外,還有一些簡單策略比如:

  •  根據過期時間判斷,清理過期時間最長的元素;
  •  根據過期時間判斷,清理最近要過期的元素;
  •  隨機清理;
  •  根據關鍵字(或元素內容)長短清理等。

快取介質

雖然從硬體介質上來看,無非就是記憶體和硬碟兩種,但從技術上,可以分成記憶體、硬碟檔案、資料庫。

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

快取分類和應用場景

快取有各類特徵,而且有不同介質的區別,那麼實際工程中我們怎麼去對快取分類呢?在目前的應用服務框架中,比較常見的,時根據快取雨應用的藕合度,分為local cache(本地快取)和remote cache(分散式快取):

  •  本地快取:指的是在應用中的快取元件,其最大的優點是應用和cache是在同一個程序內部,請求快取非常快速,沒有過多的網路開銷等,在單應用不需要叢集支援或者叢集情況下各節點無需互相通知的場景下使用本地快取較合適;同時,它的缺點也是應為快取跟應用程式耦合,多個應用程式無法直接的共享快取,各應用或叢集的各節點都需要維護自己的單獨快取,對記憶體是一種浪費。
  •  分散式快取:指的是與應用分離的快取元件或服務,其最大的優點是自身就是一個獨立的應用,與本地應用隔離,多個應用可直接的共享快取。

目前各種型別的快取都活躍在成千上萬的應用服務中,還沒有一種快取方案可以解決一切的業務場景或資料型別,我們需要根據自身的特殊場景和背景,選擇最適合的快取方案。快取的使用是程式設計師、架構師的必備技能,好的程式設計師能根據資料型別、業務場景來準確判斷使用何種型別的快取,如何使用這種快取,以最小的成本最快的效率達到最優的目的。

 

Ehcache

Ehcache是現在最流行的純Java開源快取框架,配置簡單、結構清晰、功能強大,是一個非常輕量級的快取實現,我們常用的Hibernate裡面就集成了相關快取功能。

 

Ehcache框架圖

從圖3中我們可以瞭解到,Ehcache的核心定義主要包括:

  •  cache manager:快取管理器,以前是隻允許單例的,不過現在也可以多例項了。
  •  cache:快取管理器內可以放置若干cache,存放資料的實質,所有cache都實現了Ehcache介面,這是一個真正使用的快取例項;通過快取管理器的模式,可以在單個應用中輕鬆隔離多個快取例項,獨立服務於不同業務場景需求,快取資料物理隔離,同時需要時又可共享使用。
  •  element:單條快取資料的組成單位。
  •  system of record(SOR):可以取到真實資料的元件,可以是真正的業務邏輯、外部介面呼叫、存放真實資料的資料庫等,快取就是從SOR中讀取或者寫入到SOR中去的。

在上層可以看到,整個Ehcache提供了對JSR、JMX等的標準支援,能夠較好的相容和移植,同時對各類物件有較完善的監控管理機制。它的快取介質涵蓋堆記憶體(heap)、堆外記憶體(BigMemory商用版本支援)和磁碟,各介質可獨立設定屬性和策略。Ehcache最初是獨立的本地快取框架元件,在後期的發展中,結合Terracotta服務陣列模型,可以支援分散式快取叢集,主要有RMI、JGroups、JMS和Cache Server等傳播方式進行節點間通訊,如圖3的左側部分描述。

Ehcache主要特性:

  •  快速,針對大型高併發系統場景,Ehcache的多執行緒機制有相應的優化改善。
  •  簡單,很小的jar包,簡單配置就可直接使用,單機場景下無需過多的其他服務依賴。
  •  支援多種的快取策略,靈活。
  •  快取資料有兩級:記憶體和磁碟,與一般的本地記憶體快取相比,有了磁碟的儲存空間,將可以支援更大量的資料快取需求。
  •  具有快取和快取管理器的偵聽介面,能更簡單方便的進行快取例項的監控管理。
  •  支援多快取管理器例項,以及一個例項的多個快取區域。

注意:Ehcache的超時設定主要是針對整個cache例項設定整體的超時策略,而沒有較好的處理針對單獨的key的個性的超時設定(有策略設定,但是比較複雜,就不描述了),因此,在使用中要注意過期失效的快取元素無法被GC回收,時間越長快取越多,記憶體佔用也就越大,記憶體洩露的概率也越大。

Guava Cache

Guava Cache是Google開源的Java重用工具集庫Guava裡的一款快取工具,其主要實現的快取功能有:

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

Guava Cache的架構設計靈感來源於ConcurrentHashMap,我們前面也提到過,簡單場景下可以自行編碼通過hashmap來做少量資料的快取,但是,如果結果可能隨時間改變或者是希望儲存的資料空間可控的話,自己實現這種資料結構還是有必要的。

Guava Cache繼承了ConcurrentHashMap的思路,使用多個segments方式的細粒度鎖,在保證執行緒安全的同時,支援高併發場景需求。Cache類似於Map,它是儲存鍵值對的集合,不同的是它還需要處理evict、expire、dynamic load等演算法邏輯,需要一些額外資訊來實現這些操作。對此,根據面向物件思想,需要做方法與資料的關聯封裝。如圖5所示cache的記憶體資料模型,可以看到,使用ReferenceEntry介面來封裝一個鍵值對,而用ValueReference來封裝Value值,之所以用Reference命令,是因為Cache要支援WeakReference Key和SoftReference、WeakReference value。

 

圖5 Guava Cache資料結構圖

總體來看,Guava Cache基於ConcurrentHashMap的優秀設計借鑑,在高併發場景支援和執行緒安全上都有相應的改進策略,使用Reference引用命令,提升高併發下的資料……訪問速度並保持了GC的可回收,有效節省空間;同時,write鏈和access鏈的設計,能更靈活、高效的實現多種型別的快取清理策略,包括基於容量的清理、基於時間的清理、基於引用的清理等;程式設計式的build生成器管理,讓使用者有更多的自由度,能夠根據不同場景設定合適的模式。

分散式快取:memcached快取

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

 

圖6 memcached客戶端路由圖

 

圖7 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。如圖8所示。

 

圖8 memcached記憶體結構圖

總結來看,memcached記憶體管理需要注意的幾個方面:

  •  chunk是在page裡面劃分的,而page固定為1m,所以chunk最大不能超過1m。
  •  chunk實際佔用記憶體要加48B,因為chunk資料結構本身需要佔用48B。
  •  如果使用者資料大於1m,則memcached會將其切割,放到多個chunk內。
  •  已分配出去的page不能回收。

對於key-value資訊,最好不要超過1m的大小;同時資訊長度最好相對是比較均衡穩定的,這樣能夠保障最大限度的使用記憶體;同時,memcached採用的LRU清理策略,合理甚至過期時間,提高命中率。

無特殊場景下,key-value能滿足需求的前提下,使用memcached分散式叢集是較好的選擇,搭建與操作使用都比較簡單;分散式叢集在單點故障時,隻影響小部分資料異常,目前還可以通過Magent快取代理模式,做單點備份,提升高可用;整個快取都是基於記憶體的,因此響應時間是很快,不需要額外的序列化、反序列化的程式,但同時由於基於記憶體,資料沒有持久化,叢集故障重啟資料無法恢復。高版本的memcached已經支援CAS模式的原子操作,可以低成本的解決併發控制問題。

Redis快取

Redis是一個遠端記憶體資料庫(非關係型資料庫),效能強勁,具有複製特性以及解決問題而生的獨一無二的資料模型。它可以儲存鍵值對與5種不同型別的值之間的對映,可以將儲存在記憶體的鍵值對資料持久化到硬碟,可以使用複製特性來擴充套件讀效能,還可以使用客戶端分片來擴充套件寫效能。

 

圖9 Redis資料模型圖

如圖9,Redis內部使用一個redisObject物件來標識所有的key和value資料,redisObject最主要的資訊如圖所示:type代表一個value物件具體是何種資料型別,encoding是不同資料型別在Redis內部的儲存方式,比如——type=string代表value儲存的是一個普通字串,那麼對應的encoding可以是raw或是int,如果是int則代表世界Redis內部是按數值型別儲存和表示這個字串。

從網路I/O模型上看,Redis使用單執行緒的I/O複用模型,自己封裝了一個簡單的AeEvent事件處理框架,主要實現了epoll、kqueue和select。對於單純只有I/O操作來說,單執行緒可以將速度優勢發揮到最大,但是Redis也提供了一些簡單的計算功能,比如排序、聚合等,對於這些操作,單執行緒模型實際會嚴重影響整體吞吐量,CPU計算過程中,整個I/O排程都是被阻塞住的,在這些特殊場景的使用中,需要額外的考慮。

相較於memcached的預分配記憶體管理,Redis使用現場申請記憶體的方式來儲存資料,並且很少使用free-list等方式來優化記憶體分配,會在一定程度上存在記憶體碎片。Redis跟據儲存命令引數,會把帶過期時間的資料單獨存放在一起,並把它們稱為臨時資料,非臨時資料是永遠不會被剔除的,即便實體記憶體不夠,導致swap也不會剔除任何非臨時資料(但會嘗試剔除部分臨時資料)。

我們描述Redis為記憶體資料庫,作為快取服務,大量使用記憶體間的資料快速讀寫,支援高併發大吞吐;而作為資料庫,則是指Redis對快取的持久化支援。Redis由於支援了非常豐富的記憶體資料庫結構型別,如何把這些複雜的記憶體組織方式持久化到磁碟上?Redis的持久化與傳統資料庫的方式差異較大,Redis一共支援四種持久化方式,主要使用的兩種:

  1.  定時快照方式(snapshot)該持久化方式實際是在Redis內部一個定時器事件,每隔固定時間去檢查當前資料發生的改變次數與時間是否滿足配置的持久化觸發的條件,如果滿足則通過作業系統fork呼叫來創建出一個子程序,這個子程序預設會與父程序共享相同的地址空間,這時就可以通過子程序來遍歷整個記憶體來進行儲存操作,而主程序則仍然可以提供服務,當有寫入時由作業系統按照記憶體頁(page)為單位來進行copy-on-write保證父子程序之間不會互相影響。它的缺點是快照只是代表一段時間內的記憶體映像,所以系統重啟會丟失上次快照與重啟之間所有的資料。
  2.  基於語句追加檔案的方式(aof)aof方式實際類似MySQl的基於語句的binlog方式,即每條會使Redis記憶體資料發生改變的命令都會追加到一個log檔案中,也就是說這個log檔案就是Redis的持久化資料。

aof的方式的主要缺點是追加log檔案可能導致體積過大,當系統重啟恢復資料時如果是aof的方式則載入資料會非常慢,幾十G的資料可能需要幾小時才能載入完,當然這個耗時並不是因為磁碟檔案讀取速度慢,而是由於讀取的所有命令都要在記憶體中執行一遍。另外由於每條命令都要寫log,所以使用aof的方式,Redis的讀寫效能也會有所下降。

Redis的持久化使用了Buffer I/O,所謂Buffer I/O是指Redis對持久化檔案的寫入和讀取操作都會使用實體記憶體的Page Cache,而大多數資料庫系統會使用Direct I/O來繞過這層Page Cache並自行維護一個數據的Cache。而當Redis的持久化檔案過大(尤其是快照檔案),並對其進行讀寫時,磁碟檔案中的資料都會被載入到實體記憶體中作為作業系統對該檔案的一層Cache,而這層Cache的資料與Redis記憶體中管理的資料實際是重複儲存的。雖然核心在實體記憶體緊張時會做Page Cache的剔除工作,但核心很可能認為某塊Page Cache更重要,而讓你的程序開始Swap,這時你的系統就會開始出現不穩定或者崩潰了,因此在持久化配置後,針對記憶體使用需要實時監控觀察。

與memcached客戶端支援分散式方案不同,Redis更傾向於在服務端構建分散式儲存,如圖Redis分散式叢集圖: