阿里P8架構師談:詳解Memcached、Redis等緩存的特徵、原理、應用

阿里P8架構師談:詳解Memcached、Redis等緩存的特徵、原理、應用

隨著互聯網的普及,內容信息越來越複雜,用戶數和訪問量越來越大,我們的應用需要支撐更多的併發量,同時我們的應用服務器和數據庫服務器所做的計算也越來越多。但是往往我們的應用服務器資源是有限的,且技術變革是緩慢的,數據庫每秒能接受的請求次數也是有限的(或者文件的讀寫也是有限的),如何能夠有效利用有限的資源來提供儘可能大的吞吐量?一個有效的辦法就是引入緩存,打破標準流程,每個環節中請求可以從緩存中直接獲取目標數據並返回,從而減少計算量,有效提升響應速度,讓有限的資源服務更多的用戶。

如圖所示,緩存的使用可以出現在1~4的各個環節中,每個環節的緩存方案與使用各有特點。

阿里P8架構師談:詳解Memcached、Redis等緩存的特徵、原理、應用


上圖: 現在互聯網應用(網站或App)的整體流程

緩存特徵

緩存也是一個數據模型對象,那麼必然有它的一些特徵:

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裡面就集成了相關緩存功能。

阿里P8架構師談:詳解Memcached、Redis等緩存的特徵、原理、應用


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。

阿里P8架構師談:詳解Memcached、Redis等緩存的特徵、原理、應用


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

阿里P8架構師談:詳解Memcached、Redis等緩存的特徵、原理、應用

圖6 memcached客戶端路由圖

阿里P8架構師談:詳解Memcached、Redis等緩存的特徵、原理、應用

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

阿里P8架構師談:詳解Memcached、Redis等緩存的特徵、原理、應用


圖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種不同類型的值之間的映射,可以將存儲在內存的鍵值對數據持久化到硬盤,可以使用複製特性來擴展讀性能,還可以使用客戶端分片來擴展寫性能。

阿里P8架構師談:詳解Memcached、Redis等緩存的特徵、原理、應用


圖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分佈式集群圖:

阿里P8架構師談:詳解Memcached、Redis等緩存的特徵、原理、應用

更多阿里P8架構師談:架構設計16精講(關注後即可查看)

阿里P8架構師談:詳解Memcached、Redis等緩存的特徵、原理、應用

阿里P8架構師談:詳解Memcached、Redis等緩存的特徵、原理、應用


分享到:


相關文章: