Redis系列:詳解Memcached、Redis等緩存的特征、原理、應(yīng)用
隨著互聯(lián)網(wǎng)的普及,內(nèi)容信息越來越復(fù)雜,用戶數(shù)和訪問量越來越大,我們的應(yīng)用需要支撐更多的并發(fā)量,同時我們的應(yīng)用服務(wù)器和數(shù)據(jù)庫服務(wù)器所做的計算也越來越多。但是往往我們的應(yīng)用服務(wù)器資源是有限的,且技術(shù)變革是緩慢的,數(shù)據(jù)庫每秒能接受的請求次數(shù)也是有限的(或者文件的讀寫也是有限的),如何能夠有效利用有限的資源來提供盡可能大的吞吐量?一個有效的辦法就是引入緩存,打破標(biāo)準(zhǔn)流程,每個環(huán)節(jié)中請求可以從緩存中直接獲取目標(biāo)數(shù)據(jù)并返回,從而減少計算量,有效提升響應(yīng)速度,讓有限的資源服務(wù)更多的用戶。
如圖所示,緩存的使用可以出現(xiàn)在1~4的各個環(huán)節(jié)中,每個環(huán)節(jié)的緩存方案與使用各有特點(diǎn)。
上圖: 現(xiàn)在互聯(lián)網(wǎng)應(yīng)用(網(wǎng)站或App)的整體流程
一、緩存特征
緩存也是一個數(shù)據(jù)模型對象,那么必然有它的一些特征:
1.命中率
命中率=返回正確結(jié)果數(shù)/請求緩存次數(shù),命中率問題是緩存中的一個非常重要的問題,它是衡量緩存有效性的重要指標(biāo)。命中率越高,表明緩存的使用率越高。
2.最大元素(或最大空間)
緩存中可以存放的最大元素的數(shù)量,一旦緩存中元素數(shù)量超過這個值(或者緩存數(shù)據(jù)所占空間超過其最大支持空間),那么將會觸發(fā)緩存啟動清空策略根據(jù)不同的場景合理的設(shè)置最大元素值往往可以一定程度上提高緩存的命中率,從而更有效的時候緩存。
3.清空策略
如上描述,緩存的存儲空間有限制,當(dāng)緩存空間被用滿時,如何保證在穩(wěn)定服務(wù)的同時有效提升命中率?這就由緩存清空策略來處理,設(shè)計適合自身數(shù)據(jù)特征的清空策略能有效提升命中率。常見的一般策略有:
- FIFO(first in first out)先進(jìn)先出策略,最先進(jìn)入緩存的數(shù)據(jù)在緩存空間不夠的情況下(超出最大元素限制)會被優(yōu)先被清除掉,以騰出新的空間接受新的數(shù)據(jù)。策略算法主要比較緩存元素的創(chuàng)建時間。在數(shù)據(jù)實(shí)效性要求場景下可選擇該類策略,優(yōu)先保障最新數(shù)據(jù)可用。
- LFU(less frequently used)最少使用策略,無論是否過期,根據(jù)元素的被使用次數(shù)判斷,清除使用次數(shù)較少的元素釋放空間。策略算法主要比較元素的hitCount(命中次數(shù))。在保證高頻數(shù)據(jù)有效性場景下,可選擇這類策略。
- LRU(least recently used)最近最少使用策略,無論是否過期,根據(jù)元素最后一次被使用的時間戳,清除最遠(yuǎn)使用時間戳的元素釋放空間。策略算法主要比較元素最近一次被get使用時間。在熱點(diǎn)數(shù)據(jù)場景下較適用,優(yōu)先保證熱點(diǎn)數(shù)據(jù)的有效性。
除此之外,還有一些簡單策略比如:
- 根據(jù)過期時間判斷,清理過期時間最長的元素;
- 根據(jù)過期時間判斷,清理最近要過期的元素;
- 隨機(jī)清理;
- 根據(jù)關(guān)鍵字(或元素內(nèi)容)長短清理等。
二、緩存介質(zhì)
雖然從硬件介質(zhì)上來看,無非就是內(nèi)存和硬盤兩種,但從技術(shù)上,可以分成內(nèi)存、硬盤文件、數(shù)據(jù)庫。
- 內(nèi)存:將緩存存儲于內(nèi)存中是最快的選擇,無需額外的I/O開銷,但是內(nèi)存的缺點(diǎn)是沒有持久化落地物理磁盤,一旦應(yīng)用異常break down而重新啟動,數(shù)據(jù)很難或者無法復(fù)原。
- 硬盤:一般來說,很多緩存框架會結(jié)合使用內(nèi)存和硬盤,在內(nèi)存分配空間滿了或是在異常的情況下,可以被動或主動的將內(nèi)存空間數(shù)據(jù)持久化到硬盤中,達(dá)到釋放空間或備份數(shù)據(jù)的目的。
- 數(shù)據(jù)庫:前面有提到,增加緩存的策略的目的之一就是為了減少數(shù)據(jù)庫的I/O壓力。現(xiàn)在使用數(shù)據(jù)庫做緩存介質(zhì)是不是又回到了老問題上了?其實(shí),數(shù)據(jù)庫也有很多種類型,像那些不支持SQL,只是簡單的key-value存儲結(jié)構(gòu)的特殊數(shù)據(jù)庫(如BerkeleyDB和Redis),響應(yīng)速度和吞吐量都遠(yuǎn)遠(yuǎn)高于我們常用的關(guān)系型數(shù)據(jù)庫等。
三、緩存分類和應(yīng)用場景
緩存有各類特征,而且有不同介質(zhì)的區(qū)別,那么實(shí)際工程中我們怎么去對緩存分類呢?在目前的應(yīng)用服務(wù)框架中,比較常見的,時根據(jù)緩存雨應(yīng)用的藕合度,分為local cache(本地緩存)和remote cache(分布式緩存):
- 本地緩存:指的是在應(yīng)用中的緩存組件,其最大的優(yōu)點(diǎn)是應(yīng)用和cache是在同一個進(jìn)程內(nèi)部,請求緩存非常快速,沒有過多的網(wǎng)絡(luò)開銷等,在單應(yīng)用不需要集群支持或者集群情況下各節(jié)點(diǎn)無需互相通知的場景下使用本地緩存較合適;同時,它的缺點(diǎn)也是應(yīng)為緩存跟應(yīng)用程序耦合,多個應(yīng)用程序無法直接的共享緩存,各應(yīng)用或集群的各節(jié)點(diǎn)都需要維護(hù)自己的單獨(dú)緩存,對內(nèi)存是一種浪費(fèi)。
- 分布式緩存:指的是與應(yīng)用分離的緩存組件或服務(wù),其最大的優(yōu)點(diǎn)是自身就是一個獨(dú)立的應(yīng)用,與本地應(yīng)用隔離,多個應(yīng)用可直接的共享緩存。
目前各種類型的緩存都活躍在成千上萬的應(yīng)用服務(wù)中,還沒有一種緩存方案可以解決一切的業(yè)務(wù)場景或數(shù)據(jù)類型,我們需要根據(jù)自身的特殊場景和背景,選擇最適合的緩存方案。緩存的使用是程序員、架構(gòu)師的必備技能,好的程序員能根據(jù)數(shù)據(jù)類型、業(yè)務(wù)場景來準(zhǔn)確判斷使用何種類型的緩存,如何使用這種緩存,以最小的成本最快的效率達(dá)到最優(yōu)的目的。
- Ehcache
Ehcache是現(xiàn)在最流行的純Java開源緩存框架,配置簡單、結(jié)構(gòu)清晰、功能強(qiáng)大,是一個非常輕量級的緩存實(shí)現(xiàn),我們常用的Hibernate里面就集成了相關(guān)緩存功能。
Ehcache框架圖
從圖3中我們可以了解到,Ehcache的核心定義主要包括:
- cache manager:緩存管理器,以前是只允許單例的,不過現(xiàn)在也可以多實(shí)例了。
- cache:緩存管理器內(nèi)可以放置若干cache,存放數(shù)據(jù)的實(shí)質(zhì),所有cache都實(shí)現(xiàn)了Ehcache接口,這是一個真正使用的緩存實(shí)例;通過緩存管理器的模式,可以在單個應(yīng)用中輕松隔離多個緩存實(shí)例,獨(dú)立服務(wù)于不同業(yè)務(wù)場景需求,緩存數(shù)據(jù)物理隔離,同時需要時又可共享使用。
- element:單條緩存數(shù)據(jù)的組成單位。
- system of record(SOR):可以取到真實(shí)數(shù)據(jù)的組件,可以是真正的業(yè)務(wù)邏輯、外部接口調(diào)用、存放真實(shí)數(shù)據(jù)的數(shù)據(jù)庫等,緩存就是從SOR中讀取或者寫入到SOR中去的。
在上層可以看到,整個Ehcache提供了對JSR、JMX等的標(biāo)準(zhǔn)支持,能夠較好的兼容和移植,同時對各類對象有較完善的監(jiān)控管理機(jī)制。它的緩存介質(zhì)涵蓋堆內(nèi)存(heap)、堆外內(nèi)存(BigMemory商用版本支持)和磁盤,各介質(zhì)可獨(dú)立設(shè)置屬性和策略。Ehcache最初是獨(dú)立的本地緩存框架組件,在后期的發(fā)展中,結(jié)合Terracotta服務(wù)陣列模型,可以支持分布式緩存集群,主要有RMI、JGroups、JMS和Cache Server等傳播方式進(jìn)行節(jié)點(diǎn)間通信,如圖3的左側(cè)部分描述。
Ehcache主要特性:
- 快速,針對大型高并發(fā)系統(tǒng)場景,Ehcache的多線程機(jī)制有相應(yīng)的優(yōu)化改善。
- 簡單,很小的jar包,簡單配置就可直接使用,單機(jī)場景下無需過多的其他服務(wù)依賴。
- 支持多種的緩存策略,靈活。
- 緩存數(shù)據(jù)有兩級:內(nèi)存和磁盤,與一般的本地內(nèi)存緩存相比,有了磁盤的存儲空間,將可以支持更大量的數(shù)據(jù)緩存需求。
- 具有緩存和緩存管理器的偵聽接口,能更簡單方便的進(jìn)行緩存實(shí)例的監(jiān)控管理。
- 支持多緩存管理器實(shí)例,以及一個實(shí)例的多個緩存區(qū)域。
注意:Ehcache的超時設(shè)置主要是針對整個cache實(shí)例設(shè)置整體的超時策略,而沒有較好的處理針對單獨(dú)的key的個性的超時設(shè)置(有策略設(shè)置,但是比較復(fù)雜,就不描述了),因此,在使用中要注意過期失效的緩存元素?zé)o法被GC回收,時間越長緩存越多,內(nèi)存占用也就越大,內(nèi)存泄露的概率也越大。
- Guava Cache
Guava Cache是Google開源的Java重用工具集庫Guava里的一款緩存工具,其主要實(shí)現(xiàn)的緩存功能有:
- 自動將entry節(jié)點(diǎn)加載進(jìn)緩存結(jié)構(gòu)中;
- 當(dāng)緩存的數(shù)據(jù)超過設(shè)置的最大值時,使用LRU算法移除;
- 具備根據(jù)entry節(jié)點(diǎn)上次被訪問或者寫入時間計算它的過期機(jī)制;
- 緩存的key被封裝在WeakReference引用內(nèi);
- 緩存的Value被封裝在WeakReference或SoftReference引用內(nèi);
- 統(tǒng)計緩存使用過程中命中率、異常率、未命中率等統(tǒng)計數(shù)據(jù)。
Guava Cache的架構(gòu)設(shè)計靈感來源于ConcurrentHashMap,我們前面也提到過,簡單場景下可以自行編碼通過hashmap來做少量數(shù)據(jù)的緩存,但是,如果結(jié)果可能隨時間改變或者是希望存儲的數(shù)據(jù)空間可控的話,自己實(shí)現(xiàn)這種數(shù)據(jù)結(jié)構(gòu)還是有必要的。
Guava Cache繼承了ConcurrentHashMap的思路,使用多個segments方式的細(xì)粒度鎖,在保證線程安全的同時,支持高并發(fā)場景需求。Cache類似于Map,它是存儲鍵值對的集合,不同的是它還需要處理evict、expire、dynamic load等算法邏輯,需要一些額外信息來實(shí)現(xiàn)這些操作。對此,根據(jù)面向?qū)ο笏枷耄枰龇椒ㄅc數(shù)據(jù)的關(guān)聯(lián)封裝。如圖5所示cache的內(nèi)存數(shù)據(jù)模型,可以看到,使用ReferenceEntry接口來封裝一個鍵值對,而用ValueReference來封裝Value值,之所以用Reference命令,是因?yàn)镃ache要支持WeakReference Key和SoftReference、WeakReference value。
圖5 Guava Cache數(shù)據(jù)結(jié)構(gòu)圖
總體來看,Guava Cache基于ConcurrentHashMap的優(yōu)秀設(shè)計借鑒,在高并發(fā)場景支持和線程安全上都有相應(yīng)的改進(jìn)策略,使用Reference引用命令,提升高并發(fā)下的數(shù)據(jù)……訪問速度并保持了GC的可回收,有效節(jié)省空間;同時,write鏈和access鏈的設(shè)計,能更靈活、高效的實(shí)現(xiàn)多種類型的緩存清理策略,包括基于容量的清理、基于時間的清理、基于引用的清理等;編程式的build生成器管理,讓使用者有更多的自由度,能夠根據(jù)不同場景設(shè)置合適的模式。
四、分布式緩存:memcached緩存
memcached是應(yīng)用較廣的開源分布式緩存產(chǎn)品之一,它本身其實(shí)不提供分布式解決方案。在服務(wù)端,memcached集群環(huán)境實(shí)際就是一個個memcached服務(wù)器的堆積,環(huán)境搭建較為簡單;cache的分布式主要是在客戶端實(shí)現(xiàn),通過客戶端的路由處理來達(dá)到分布式解決方案的目的??蛻舳俗雎酚傻脑矸浅:唵?,應(yīng)用服務(wù)器在每次存取某key的value時,通過某種算法把key映射到某臺memcached服務(wù)器nodeA上,因此這個key所有操作都在nodeA上,結(jié)構(gòu)圖如圖6、圖7所示。
圖6 memcached客戶端路由圖
圖7 memcached一致性hash示例圖
memcached客戶端采用一致性hash算法作為路由策略,如圖7,相對于一般hash(如簡單取模)的算法,一致性hash算法除了計算key的hash值外,還會計算每個server對應(yīng)的hash值,然后將這些hash值映射到一個有限的值域上(比如0~2^32)。通過尋找hash值大于hash(key)的最小server作為存儲該key數(shù)據(jù)的目標(biāo)server。如果找不到,則直接把具有最小hash值的server作為目標(biāo)server。同時,一定程度上,解決了擴(kuò)容問題,增加或刪除單個節(jié)點(diǎn),對于整個集群來說,不會有大的影響。最近版本,增加了虛擬節(jié)點(diǎn)的設(shè)計,進(jìn)一步提升了可用性。
memcached是一個高效的分布式內(nèi)存cache,了解memcached的內(nèi)存管理機(jī)制,才能更好的掌握memcached,讓我們可以針對我們數(shù)據(jù)特點(diǎn)進(jìn)行調(diào)優(yōu),讓其更好的為我所用。我們知道m(xù)emcached僅支持基礎(chǔ)的key-value鍵值對類型數(shù)據(jù)存儲。在memcached內(nèi)存結(jié)構(gòu)中有兩個非常重要的概念:slab和chunk。如圖8所示。
圖8 memcached內(nèi)存結(jié)構(gòu)圖
總結(jié)來看,memcached內(nèi)存管理需要注意的幾個方面:
- chunk是在page里面劃分的,而page固定為1m,所以chunk最大不能超過1m。
- chunk實(shí)際占用內(nèi)存要加48B,因?yàn)閏hunk數(shù)據(jù)結(jié)構(gòu)本身需要占用48B。
- 如果用戶數(shù)據(jù)大于1m,則memcached會將其切割,放到多個chunk內(nèi)。
- 已分配出去的page不能回收。
對于key-value信息,最好不要超過1m的大??;同時信息長度最好相對是比較均衡穩(wěn)定的,這樣能夠保障最大限度的使用內(nèi)存;同時,memcached采用的LRU清理策略,合理甚至過期時間,提高命中率。
無特殊場景下,key-value能滿足需求的前提下,使用memcached分布式集群是較好的選擇,搭建與操作使用都比較簡單;分布式集群在單點(diǎn)故障時,只影響小部分?jǐn)?shù)據(jù)異常,目前還可以通過Magent緩存代理模式,做單點(diǎn)備份,提升高可用;整個緩存都是基于內(nèi)存的,因此響應(yīng)時間是很快,不需要額外的序列化、反序列化的程序,但同時由于基于內(nèi)存,數(shù)據(jù)沒有持久化,集群故障重啟數(shù)據(jù)無法恢復(fù)。高版本的memcached已經(jīng)支持CAS模式的原子操作,可以低成本的解決并發(fā)控制問題。
五、Redis緩存
Redis是一個遠(yuǎn)程內(nèi)存數(shù)據(jù)庫(非關(guān)系型數(shù)據(jù)庫),性能強(qiáng)勁,具有復(fù)制特性以及解決問題而生的獨(dú)一無二的數(shù)據(jù)模型。它可以存儲鍵值對與5種不同類型的值之間的映射,可以將存儲在內(nèi)存的鍵值對數(shù)據(jù)持久化到硬盤,可以使用復(fù)制特性來擴(kuò)展讀性能,還可以使用客戶端分片來擴(kuò)展寫性能。
圖9 Redis數(shù)據(jù)模型圖
如圖9,Redis內(nèi)部使用一個redisObject對象來標(biāo)識所有的key和value數(shù)據(jù),redisObject最主要的信息如圖所示:type代表一個value對象具體是何種數(shù)據(jù)類型,encoding是不同數(shù)據(jù)類型在Redis內(nèi)部的存儲方式,比如——type=string代表value存儲的是一個普通字符串,那么對應(yīng)的encoding可以是raw或是int,如果是int則代表世界Redis內(nèi)部是按數(shù)值類型存儲和表示這個字符串。
從網(wǎng)絡(luò)I/O模型上看,Redis使用單線程的I/O復(fù)用模型,自己封裝了一個簡單的AeEvent事件處理框架,主要實(shí)現(xiàn)了epoll、kqueue和select。對于單純只有I/O操作來說,單線程可以將速度優(yōu)勢發(fā)揮到最大,但是Redis也提供了一些簡單的計算功能,比如排序、聚合等,對于這些操作,單線程模型實(shí)際會嚴(yán)重影響整體吞吐量,CPU計算過程中,整個I/O調(diào)度都是被阻塞住的,在這些特殊場景的使用中,需要額外的考慮。
相較于memcached的預(yù)分配內(nèi)存管理,Redis使用現(xiàn)場申請內(nèi)存的方式來存儲數(shù)據(jù),并且很少使用free-list等方式來優(yōu)化內(nèi)存分配,會在一定程度上存在內(nèi)存碎片。Redis跟據(jù)存儲命令參數(shù),會把帶過期時間的數(shù)據(jù)單獨(dú)存放在一起,并把它們稱為臨時數(shù)據(jù),非臨時數(shù)據(jù)是永遠(yuǎn)不會被剔除的,即便物理內(nèi)存不夠,導(dǎo)致swap也不會剔除任何非臨時數(shù)據(jù)(但會嘗試剔除部分臨時數(shù)據(jù))。
我們描述Redis為內(nèi)存數(shù)據(jù)庫,作為緩存服務(wù),大量使用內(nèi)存間的數(shù)據(jù)快速讀寫,支持高并發(fā)大吞吐;而作為數(shù)據(jù)庫,則是指Redis對緩存的持久化支持。Redis由于支持了非常豐富的內(nèi)存數(shù)據(jù)庫結(jié)構(gòu)類型,如何把這些復(fù)雜的內(nèi)存組織方式持久化到磁盤上?Redis的持久化與傳統(tǒng)數(shù)據(jù)庫的方式差異較大,Redis一共支持四種持久化方式,主要使用的兩種:
- 定時快照方式(snapshot):該持久化方式實(shí)際是在Redis內(nèi)部一個定時器事件,每隔固定時間去檢查當(dāng)前數(shù)據(jù)發(fā)生的改變次數(shù)與時間是否滿足配置的持久化觸發(fā)的條件,如果滿足則通過操作系統(tǒng)fork調(diào)用來創(chuàng)建出一個子進(jìn)程,這個子進(jìn)程默認(rèn)會與父進(jìn)程共享相同的地址空間,這時就可以通過子進(jìn)程來遍歷整個內(nèi)存來進(jìn)行存儲操作,而主進(jìn)程則仍然可以提供服務(wù),當(dāng)有寫入時由操作系統(tǒng)按照內(nèi)存頁(page)為單位來進(jìn)行copy-on-write保證父子進(jìn)程之間不會互相影響。它的缺點(diǎn)是快照只是代表一段時間內(nèi)的內(nèi)存映像,所以系統(tǒng)重啟會丟失上次快照與重啟之間所有的數(shù)據(jù)。
- 基于語句追加文件的方式(aof):aof方式實(shí)際類似MySQl的基于語句的binlog方式,即每條會使Redis內(nèi)存數(shù)據(jù)發(fā)生改變的命令都會追加到一個log文件中,也就是說這個log文件就是Redis的持久化數(shù)據(jù)。
aof的方式的主要缺點(diǎn)是追加log文件可能導(dǎo)致體積過大,當(dāng)系統(tǒng)重啟恢復(fù)數(shù)據(jù)時如果是aof的方式則加載數(shù)據(jù)會非常慢,幾十G的數(shù)據(jù)可能需要幾小時才能加載完,當(dāng)然這個耗時并不是因?yàn)榇疟P文件讀取速度慢,而是由于讀取的所有命令都要在內(nèi)存中執(zhí)行一遍。另外由于每條命令都要寫log,所以使用aof的方式,Redis的讀寫性能也會有所下降。
Redis的持久化使用了Buffer I/O,所謂Buffer I/O是指Redis對持久化文件的寫入和讀取操作都會使用物理內(nèi)存的Page Cache,而大多數(shù)數(shù)據(jù)庫系統(tǒng)會使用Direct I/O來繞過這層Page Cache并自行維護(hù)一個數(shù)據(jù)的Cache。而當(dāng)Redis的持久化文件過大(尤其是快照文件),并對其進(jìn)行讀寫時,磁盤文件中的數(shù)據(jù)都會被加載到物理內(nèi)存中作為操作系統(tǒng)對該文件的一層Cache,而這層Cache的數(shù)據(jù)與Redis內(nèi)存中管理的數(shù)據(jù)實(shí)際是重復(fù)存儲的。雖然內(nèi)核在物理內(nèi)存緊張時會做Page Cache的剔除工作,但內(nèi)核很可能認(rèn)為某塊Page Cache更重要,而讓你的進(jìn)程開始Swap,這時你的系統(tǒng)就會開始出現(xiàn)不穩(wěn)定或者崩潰了,因此在持久化配置后,針對內(nèi)存使用需要實(shí)時監(jiān)控觀察。
與memcached客戶端支持分布式方案不同,Redis更傾向于在服務(wù)端構(gòu)建分布式存儲,如圖Redis分布式集群圖: