一、引子html
通常而言,如今互聯網應用(網站或App)的總體流程,以下圖所示,用戶請求從界面(瀏覽器或App界面)到網絡轉發、應用服務再到存儲(數據庫或文件系統),而後返回到界面呈現內容。前端
隨着互聯網的普及,內容信息愈來愈複雜,用戶數和訪問量愈來愈大,咱們的應用須要支撐更多的併發量,同時應用服務器和數據庫服務器所作的計算也愈來愈多。可是每每咱們的應用服務器資源是有限的,且技術變革是緩慢的,數據庫每秒能接受的請求次數也是有限的(或者文件的讀寫也是有限的),如何可以有效利用有限的資源來提供儘量大的吞吐量?一個有效的辦法就是引入緩存,打破標準流程,每一個環節中請求能夠從緩存中直接獲取目標數據並返回,從而減小計算量,有效提高響應速度,讓有限的資源服務更多的用戶。java
二、概述node
2.一、緩存特徵mysql
命中率程序員
命中率=返回正確結果數/請求緩存次數,命中率問題是緩存中的一個很是重要的問題,它是衡量緩存有效性的重要指標。命中率越高,代表緩存的使用率越高。web
最大元素(或最大空間)redis
緩存中能夠存放的最大元素的數量,一旦緩存中元素數量超過這個值(或者緩存數據所佔空間超過其最大支持空間),那麼將會觸發緩存啓動清空策略根據不一樣的場景合理的設置最大元素值每每能夠必定程度上提升緩存的命中率,從而更有效的時候緩存。算法
清空策略sql
如上描述,緩存的存儲空間有限制,當緩存空間被用滿時,如何保證在穩定服務的同時有效提高命中率?這就由緩存清空策略來處理,設計適合自身數據特徵的清空策略能有效提高命中率。常見的通常策略有:
FIFO(first in first out)
先進先出策略,最早進入緩存的數據在緩存空間不夠的狀況下(超出最大元素限制)會被優先被清除掉,以騰出新的空間接受新的數據。策略算法主要比較緩存元素的建立時間。在數據實效性要求場景下可選擇該類策略,優先保障最新數據可用。
LFU(less frequently used)
最少使用策略,不管是否過時,根據元素的被使用次數判斷,清除使用次數較少的元素釋放空間。策略算法主要比較元素的hitCount(命中次數)。在保證高頻數據有效性場景下,可選擇這類策略。
LRU(least recently used)
最近最少使用策略,不管是否過時,根據元素最後一次被使用的時間戳,清除最遠使用時間戳的元素釋放空間。策略算法主要比較元素最近一次被get使用時間。在熱點數據場景下較適用,優先保證熱點數據的有效性。
除此以外,還有一些簡單策略好比:
根據過時時間判斷,清理過時時間最長的元素;根據過時時間判斷,清理最近要過時的元素;隨機清理;根據關鍵字(或元素內容)長短清理等。
2.二、緩存介質
雖然從硬件介質上來看,無非就是內存和硬盤兩種,但從技術上,能夠分紅內存、硬盤文件、數據庫。
1內存:將緩存存儲於內存中是最快的選擇,無需額外的I/O開銷,可是內存的缺點是沒有持久化到物理磁盤,一旦應用異常break down而從新啓動,數據很難或者沒法復原。
2硬盤:通常來講,不少緩存框架會結合使用內存和硬盤,在內存分配空間滿了或是在異常的狀況下,能夠被動或主動的將內存空間數據持久化到硬盤中,達到釋放空間或備份數據的目的。
3數據庫:前面有提到,增長緩存的策略的目的之一就是爲了減小數據庫的I/O壓力。如今使用數據庫作緩存介質是否是又回到了老問題上了?其實,數據庫也有不少種類型,像那些不支持SQL,只是簡單的key-value存儲結構的特殊數據庫(如BerkeleyDB和Redis),響應速度和吞吐量都遠遠高於咱們經常使用的關係型數據庫等。
2.三、緩存分類和應用場景
緩存有各種特徵,並且有不一樣介質的區別,那麼實際工程中咱們怎麼去對緩存分類呢?在目前的應用服務框架中,比較常見的,是根據緩存與應用的藕合度,分爲local cache(本地緩存)和remote cache(分佈式緩存):
本地緩存:指的是在應用中的緩存組件,其最大的優勢是應用和cache是在同一個進程內部,請求緩存很是快速,沒有過多的網絡開銷等,在單應用不須要集羣支持或者集羣狀況下各節點無需互相通知的場景下使用本地緩存較合適;同時,它的缺點也是應爲緩存跟應用程序耦合,多個應用程序沒法直接的共享緩存,各應用或集羣的各節點都須要維護本身的單獨緩存,對內存是一種浪費。
分佈式緩存:指的是與應用分離的緩存組件或服務,其最大的優勢是自身就是一個獨立的應用,與本地應用隔離,多個應用可直接的共享緩存。
目前各類類型的緩存都活躍在成千上萬的應用服務中,尚未一種緩存方案能夠解決一切的業務場景或數據類型,咱們須要根據自身的特殊場景和背景,選擇最適合的緩存方案。緩存的使用是程序員、架構師的必備技能,好的程序員能根據數據類型、業務場景來準確判斷使用何種類型的緩存,如何使用這種緩存,以最小的成本最快的效率達到最優的目的。
三、瀏覽器緩存
3.一、簡介:
HTTP報文就是瀏覽器和服務器間通訊時發送及響應的數據塊。
瀏覽器向服務器請求數據,發送請求(request)報文;服務器向瀏覽器返回數據,返回響應(response)報文。
報文信息主要分爲兩部分
1.包含屬性的首部(header)--------------------------附加信息(cookie,緩存信息等)與緩存相關的規則信息,均包含在header中
2.包含數據的主體部分(body)-----------------------HTTP請求真正想要傳輸的部分。
瀏覽器緩存也被稱爲客戶端緩存,分爲強緩存和協商緩存。
3.二、強緩存
強緩存是利用Expires和Cache-Control這兩個http response header來實現的,它們都用來表示資源在客戶端緩存的有效期。
Expries的值是一個GMT格式的絕對時間,表明着資源失效的時間,缺點就是當瀏覽器和服務器的時間相差較大時,會致使緩存混亂。另外Expires 是HTTP 1.0的東西,如今默認瀏覽器均默認使用HTTP 1.1,因此它的做用基本忽略。另外一個問題是,到期時間是由服務端生成的,可是客戶端時間可能跟服務端時間有偏差,這就會致使緩存命中的偏差。
Cache-Control的max-age是相對時間,能夠和Expries同時啓用,同時啓用的時候Cache-Control的優先級高。
Cache-Control 有重要的規則,常見的取值有private、public、no-cache、max-age,no-store,默認爲private。
private: 客戶端能夠緩存
public: 客戶端和代理服務器均可緩存(前端的同窗,能夠認爲public和private是同樣的)
max-age=xxx: 緩存的內容將在 xxx 秒後失效
no-cache: 須要使用對比緩存來驗證緩存數據(後面介紹)
no-store: 全部內容都不會緩存,強制緩存,對比緩存都不會觸發(對於前端開發來講,緩存越多越好,so...基本上和它說886)
javaweb裏面,咱們能夠使用相似下面的代碼設置強緩存:
java.util.Date date = new java.util.Date();
response.setDateHeader("Expires",date.getTime()+20000); //Expires:過期期限值
response.setHeader("Cache-Control", "public"); //Cache-Control來控制頁面的緩存與否,public:瀏覽器和緩存服務器均可以緩存頁面信息;
response.setHeader("Pragma", "Pragma"); //Pragma:設置頁面是否緩存,爲Pragma則緩存,no-cache則不緩存
還能夠經過相似下面的java代碼設置不啓用強緩存:
response.setHeader( "Pragma", "no-cache" );
response.setDateHeader("Expires", 0);
response.addHeader( "Cache-Control", "no-cache" );//瀏覽器和緩存服務器都不該該緩存頁面信息。
3.三、協商緩存
當瀏覽器對某個資源的請求沒有命中強緩存,就會發一個請求到服務器,驗證協商緩存是否命中,若是協商緩存命中,請求響應返回的http狀態爲304而且會顯示一個Not Modified的字符串。
協商緩存是利用的是【Last-Modified,If-Modified-Since】和【ETag、If-None-Match】這兩對Header來管理的。
【Last-Modified,If-Modified-Since】的控制緩存的原理是:
1)瀏覽器第一次跟服務器請求一個資源,服務器在返回這個資源的同時,在respone的header加上Last-Modified的header,這個header表示這個資源在服務器上的最後修改時間:
2)瀏覽器再次跟服務器請求這個資源時,在request的header上加上If-Modified-Since的header,這個header的值就是上一次請求時返回的Last-Modified的值:
3)服務器再次收到資源請求時,根據瀏覽器傳過來If-Modified-Since和資源在服務器上的最後修改時間判斷資源是否有變化,若是沒有變化則返回304 Not Modified,可是不會返回資源內容;若是有變化,就正常返回資源內容。當服務器返回304 Not Modified的響應時,response header中不會再添加Last-Modified的header,由於既然資源沒有變化,那麼Last-Modified也就不會改變,這是服務器返回304時的response header:
4)瀏覽器收到304的響應後,就會從緩存中加載資源。
5)若是協商緩存沒有命中,瀏覽器直接從服務器加載資源時,Last-Modified Header在從新加載的時候會被更新,下次請求時,If-Modified-Since會啓用上次返回的Last-Modified值。
【Last-Modified,If-Modified-Since】都是根據服務器時間返回的header,通常來講,在沒有調整服務器時間和篡改客戶端緩存的狀況下,這兩個header配合起來管理協商緩存是很是可靠的,可是有時候也會服務器上資源其實有變化,可是最後修改時間卻沒有變化的狀況,而這種問題又很不容易被定位出來,而當這種狀況出現的時候,就會影響協商緩存的可靠性。因此就有了另一對header來管理協商緩存,這對header就是【ETag、If-None-Match】。
【ETag、If-None-Match】,它們的緩存管理的方式是:
1)瀏覽器第一次跟服務器請求一個資源,服務器在返回這個資源的同時,在respone的header加上ETag的header,這個header是服務器根據當前請求的資源生成的一個惟一標識,這個惟一標識是一個字符串,只要資源有變化這個串就不一樣,跟最後修改時間沒有關係,因此能很好的補充Last-Modified的問題:
2)瀏覽器再次跟服務器請求這個資源時,在request的header上加上If-None-Match的header,這個header的值就是上一次請求時返回的ETag的值:
3)服務器再次收到資源請求時,根據瀏覽器傳過來If-None-Match和而後再根據資源生成一個新的ETag,若是這兩個值相同就說明資源沒有變化,不然就是有變化;若是沒有變化則返回304 Not Modified,可是不會返回資源內容;若是有變化,就正常返回資源內容。與Last-Modified不同的是,當服務器返回304 Not Modified的響應時,因爲ETag從新生成過,response header中還會把這個ETag返回,即便這個ETag跟以前的沒有變化:
4)瀏覽器收到304的響應後,就會從緩存中加載資源。
【Last-Modified,If-Modified-Since】和【ETag、If-None-Match】通常都是同時啓用,這是爲了處理Last-Modified不可靠的狀況。有一種場景須要注意:
分佈式系統裏多臺機器間文件的Last-Modified必須保持一致,以避免負載均衡到不一樣機器致使比對失敗;
分佈式系統儘可能關閉掉ETag(每臺機器生成的ETag都會不同)
1)當ctrl+f5強制刷新網頁時,直接從服務器加載,跳過強緩存和協商緩存;
2)當f5刷新網頁時,跳過強緩存,可是會檢查協商緩存;
四、數據庫緩存
4.一、應用場景
針對數據庫的增、刪、查、改,數據庫緩存技術應用場景絕大部分針對的是「查」的場景。好比,一篇常常訪問的帖子/文章/新聞、熱門商品的描述信息、好友評論/留言等。由於在常見的應用中,數據庫層次的壓力有80%的是查詢,20%的纔是數據的變動操做。因此絕大部分的應用場景的仍是「查」緩存。固然,「增、刪、改」的場景也是有的。好比,一篇文章訪問的次數,不可能每訪問一次,咱們就去數據庫裏面加一次吧?這種時候,咱們通常「增」場景的緩存就必不可少。不然,一篇文章被訪問了十萬次,代碼層次不會還去作十萬次的數據庫操做吧。
常見的數據庫,好比oracle、mysql等,數據都是存放在磁盤中。雖然在數據庫層也作了對應的緩存,但這種數據庫層次的緩存通常針對的是查詢內容,並且粒度也過小,通常只有表中數據沒有變動的時候,數據庫對應的cache才發揮了做用。但這並不能減小業務系統對數據庫產生的增、刪、查、改的龐大IO壓力。因此數據庫緩存技術在此誕生,實現熱點數據的高速緩存,提升應用的響應速度,極大緩解後端數據庫的壓力。
要說用於數據庫緩存場景的開源技術,那必然是memcache和redis這兩個中間件:
由於都是專一於內存緩存領域,memcache和redis向來都有爭議。好比性能,究竟是memcache性能好,仍是redis性能更好等。一樣都是內存緩存技術,它們都有本身的技術特性。沒有更好的技術,只有更合適的技術。我的總結一下,有持久化需求或者對數據結構和處理有高級要求的應用,選擇redis。其餘簡單的key/value存儲,選擇memcache。因此根據自身業務特性,數據庫緩存來選擇適合本身的技術。
4.二、memcache
MemCache的工做流程以下:先檢查客戶端的請求數據是否在memcached中,若有,直接把請求數據返回,再也不對數據庫進行任何操做;若是請求的數據不在memcached中,就去查數據庫,把從數據庫中獲取的數據返回給客戶端,同時把數據緩存一份到memcached中(memcached客戶端不負責,須要程序明確實現);每次更新數據庫的同時更新memcached中的數據,保證一致性;當分配給memcached內存空間用完以後,會使用LRU(Least Recently Used,最近最少使用)策略加上到期失效策略,失效數據首先被替換,而後再替換掉最近未使用的數據。
memcache是一套高性能的開源的分佈式的高速內存對象緩存系統。由服務端和客戶端組成,以守護程序(監聽)方式運行於一個或多個服務器中,隨時會接收客戶端的鏈接和操做。memcache主要把數據對象緩存到內存中,它可以用來存儲各類格式的數據,包括圖像、視頻、文件以及數據庫檢索的結果等,經過在內存裏維護一個統一的巨大的hash表。簡單的說就是將數據調用到內存中,而後從內存中讀取,從而大大提升讀取速度。memcache基於一個存儲鍵/值對的hashmap進行存儲對象到內存中。memcache是用C寫的,可是客戶端能夠用任何語言來編寫,並經過memcached協議與守護進程通訊。
特性:
在 Memcached中能夠保存的item數據量是沒有限制的,只要內存足夠 。
Memcached單進程在32位系統中最大使用內存爲2G,若在64位系統則沒有限制,這是因爲32位系統限制單進程最多可以使用2G內存,要使用更多內存,能夠分多個端口開啓多個Memcached進程 。
最大30天的數據過時時間,設置爲永久的也會在這個時間過時,常量REALTIME_MAXDELTA
單個item最大數據是1MB,超過1MB數據不予存儲,常量POWER_BLOCK 1048576進行控制。
另解:
memcached是應用較廣的開源分佈式緩存產品之一,它自己其實不提供分佈式解決方案。在服務端,memcached集羣環境實際就是一個個memcached服務器的堆積,環境搭建較爲簡單;cache的分佈式主要是在客戶端實現,經過客戶端的路由處理來達到分佈式解決方案的目的。客戶端作路由的原理很是簡單,應用服務器在每次存取某key的value時,經過某種算法把key映射到某臺memcached服務器nodeA上,所以這個key全部操做都在nodeA上。
memcached客戶端採用一致性hash算法做爲路由策略,相對於通常hash(如簡單取模)的算法,一致性hash算法除了計算key的hash值外,還會計算每一個server對應的hash值,而後將這些hash值映射到一個有限的值域上(好比0~2^32)。經過尋找hash值大於hash(key)的最小server做爲存儲該key數據的目標server。若是找不到,則直接把具備最小hash值的server做爲目標server。同時,必定程度上,解決了擴容問題,增長或刪除單個節點,對於整個集羣來講,不會有大的影響。最近版本,增長了虛擬節點的設計,進一步提高了可用性。
對於key-value信息,最好不要超過1m的大小;同時信息長度最好相對是比較均衡穩定的,這樣可以保障最大限度的使用內存;同時,memcached採用的LRU清理策略,合理設置過時時間,提升命中率。
無特殊場景下,key-value能知足需求的前提下,使用memcached分佈式集羣是較好的選擇,搭建與操做使用都比較簡單;分佈式集羣在單點故障時,隻影響小部分數據異常,目前還能夠經過Magent緩存代理模式,作單點備份,提高高可用;整個緩存都是基於內存的,所以響應時間是很快,不須要額外的序列化、反序列化的程序,但同時因爲基於內存,數據沒有持久化,集羣故障重啓數據沒法恢復。高版本的memcached已經支持CAS模式的原子操做,能夠低成本的解決併發控制問題。
4.三、Redis
Redis是一個開源的使用ANSI C語言編寫、支持網絡、可基於內存亦可持久化的日誌型、Key-Value、遠程內存數據庫(非關係型數據庫),和Memcached相似,它支持存儲的value類型相對更多,包括string(字符串)、list(鏈表)、set(集合)、zset(sorted set --有序集合)和hash(哈希類型)。在此基礎上,redis支持各類不一樣方式的排序。與memcached同樣,爲了保證效率,數據都是緩存在內存中。區別的是redis會週期性的把更新的數據寫入磁盤或者把修改操做寫入追加的記錄文件,而且在此基礎上實現了master-slave(主從)同步。
4.3.一、持久化
Redis支持主從同步。數據能夠從主服務器向任意數量的從服務器上同步,從服務器能夠是關聯其餘從服務器的主服務器。這使得Redis可執行單層樹複製。存盤能夠有意無心的對數據進行寫操做。
Redis支持兩種持久化方式:
(1):snapshotting(快照)也是默認方式。(把數據作一個備份,將數據存儲到文件)
(2)Append-only file(縮寫aof)的方式。
快照是默認的持久化方式,這種方式是將內存中數據以快照的方式寫到二進制文件中,默認的文件名稱爲dump。rdb。能夠經過配置設置自動作快照持久化的方式。咱們能夠配置redis在n秒內若是超過m個key鍵修改就自動作快照。
aof方式:因爲快照方式是在必定間隔時間作一次的,因此若是redis意外down掉的話,就會丟失最後一次快照後的全部修改。aof比快照方式有更好的持久化性,是因爲在使用aof時,redis會將每個收到的寫命令都經過write函數追加到文件中,當redis重啓時會經過從新執行文件中保存的寫命令來在內存中重建整個數據庫的內容。
Redis一共支持四種持久化方式,主要使用的兩種:
定時快照方式(snapshot):該持久化方式實際是在Redis內部一個定時器事件,每隔固定時間去檢查當前數據發生的改變次數與時間是否知足配置的持久化觸發的條件,若是知足則經過操做系統fork調用來建立出一個子進程,這個子進程默認會與父進程共享相同的地址空間,這時就能夠經過子進程來遍歷整個內存來進行存儲操做,而主進程則仍然能夠提供服務,當有寫入時由操做系統按照內存頁(page)爲單位來進行copy-on-write保證父子進程之間不會互相影響。它的缺點是快照只是表明一段時間內的內存映像,因此係統重啓會丟失上次快照與重啓之間全部的數據。
基於語句追加文件的方式(aof):aof方式實際相似MySQl的基於語句的binlog方式,即每條會使Redis內存數據發生改變的命令都會追加到一個log文件中,也就是說這個log文件就是Redis的持久化數據。aof的方式的主要缺點是追加log文件可能致使體積過大,當系統重啓恢復數據時若是是aof的方式則加載數據會很是慢,幾十G的數據可能須要幾小時才能加載完,固然這個耗時並非由於磁盤文件讀取速度慢,而是因爲讀取的全部命令都要在內存中執行一遍。另外因爲每條命令都要寫log,因此使用aof的方式,Redis的讀寫性能也會有所降低。
4.3.二、Redis同步
總體過程概述以下:
一、 初始化
配置好主從後,不管slave是初次仍是從新鏈接到master, slave都會發送PSYNC命令到master。若是是從新鏈接,且知足增量同步的條件,那麼redis會將內存緩存隊列中的命令發給slave, 完成增量同步(Partial resynchronization)。不然進行全量同步。
二、正常同步開始
任何對master的寫操做都會以redis命令的方式,經過網絡發送給slave。
全量同步(full resynchronization)
Redis全量複製通常發生在Slave初始化階段,這時Slave須要將Master上的全部數據都複製一份。具體步驟以下:
1)從服務器鏈接主服務器,發送SYNC命令;
2)主服務器接收到SYNC命名後,開始執行BGSAVE命令生成RDB文件並使用緩衝區記錄此後執行的全部寫命令;
3)主服務器BGSAVE執行完後,向全部從服務器發送快照文件,並在發送期間繼續記錄被執行的寫命令;
4)從服務器收到快照文件後丟棄全部舊數據,載入收到的快照;
5)主服務器快照發送完畢後開始向從服務器發送緩衝區中的寫命令;
6)從服務器完成對快照的載入,開始接收命令請求,並執行來自主服務器緩衝區的寫命令;
增量同步:
Redis增量複製是指Slave初始化後開始正常工做時主服務器發生的寫操做同步到從服務器的過程。
增量複製的過程主要是主服務器每執行一個寫命令就會向從服務器發送相同的寫命令,從服務器接收並執行收到的寫命令。
幾個重要概念:
內存緩存隊列(in-memory backlog):用於記錄鏈接斷開時master收到的寫操做
複製偏移量(replication offset):master, slave都有一個偏移,記錄當前同步記錄的位置
master服務器id(master run ID):master惟一標識。
增量同步的條件:現網絡鏈接斷開後,slave將嘗試重連master。當知足下列條件時,重連後會進行增量同步:
一、slave記錄的master服務器id和當前要鏈接的master服務器id相同
二、slave的複製偏移量比master的偏移量靠前。好比slave是1000, master是1100
三、slave的複製偏移量所指定的數據仍然保存在主服務器的內存緩存隊列中
4.3.三、
Redis的主要功能都基於單線程模型實現,也就是說Redis使用一個線程來服務全部的客戶端請求,同時Redis採用了非阻塞式IO,並精細地優化各類命令的算法時間複雜度,這些信息意味着:
Redis是線程安全的(由於只有一個線程),其全部操做都是原子的,不會因併發產生數據異常;
Redis的速度很是快(由於使用非阻塞式IO,且大部分命令的算法時間複雜度都是O(1));
使用高耗時的Redis命令是很危險的,會佔用惟一的一個線程的大量處理時間,致使全部的請求都被拖慢。(例如時間複雜度爲O(N)的KEYS命令,嚴格禁止在生產環境中使用)
關於Key的一些注意事項:
不要使用過長的Key。例如使用一個1024字節的key就不是一個好主意,不只會消耗更多的內存,還會致使查找的效率下降
Key短到缺失了可讀性也是很差的,例如」u1000flw」比起」user:1000:followers」來講,節省了寥寥的存儲空間,卻引起了可讀性和可維護性上的麻煩
最好使用統一的規範來設計Key,好比」object-type:id:attr」,以這一規範設計出的Key多是」user:1000」或」comment:1234:reply-to」
Redis容許的最大Key長度是512MB(對Value的長度限制也是512MB)
Redis的基本數據類型只有String,但Redis能夠把String做爲整型或浮點型數字來使用,主要體如今INCR、DECR類的命令上:
INCR:將key對應的value值自增1,並返回自增後的值。只對能夠轉換爲整型的String數據起做用。時間複雜度O(1)
INCRBY:將key對應的value值自增指定的整型數值,並返回自增後的值。只對能夠轉換爲整型的String數據起做用。時間複雜度O(1)
DECR/DECRBY:同INCR/INCRBY,自增改成自減。
INCR/DECR系列命令要求操做的value類型爲String,並能夠轉換爲64位帶符號的整型數字,不然會返回錯誤。
也就是說,進行INCR/DECR系列命令的value,必須在[-2^63 ~ 2^63 - 1]範圍內。
前文提到過,Redis採用單線程模型,自然是線程安全的,這使得INCR/DECR命令能夠很是便利的實現高併發場景下的精確控制。
Hash即哈希表,Redis的Hash和傳統的哈希表同樣,是一種field-value型的數據結構,能夠理解成將HashMap搬入Redis。
Hash很是適合用於表現對象類型的數據,用Hash中的field對應對象的field便可。
Hash的優勢包括:
能夠實現二元查找,如」查找ID爲1000的用戶的年齡」;
比起將整個對象序列化後做爲String存儲的方法,Hash可以有效地減小網絡傳輸的消耗;
當使用Hash維護一個集合時,提供了比List效率高得多的隨機訪問命令。
針對Redis的性能優化,主要從下面幾個層面入手:
最初的也是最重要的,確保沒有讓Redis執行耗時長的命令;
使用pipelining將連續執行的命令組合執行;
操做系統的Transparent huge pages功能必須關閉:
echo never > /sys/kernel/mm/transparent_hugepage/enabled
若是在虛擬機中運行Redis,可能自然就有虛擬機環境帶來的固有延遲。能夠經過。/redis-cli —intrinsic-latency 100命令查看固有延遲。同時若是對Redis的性能有較高要求的話,應儘量在物理機上直接部署Redis。
檢查數據持久化策略;
考慮引入讀寫分離機制;
長耗時命令;
Redis絕大多數讀寫命令的時間複雜度都在O(1)到O(N)之間,在文本和官方文檔中均對每一個命令的時間複雜度有說明。
一般來講,O(1)的命令是安全的,O(N)命令在使用時須要注意,若是N的數量級不可預知,則應避免使用。例如對一個field數未知的Hash數據執行HGETALL/HKEYS/HVALS命令,一般來講這些命令執行的很快,但若是這個Hash中的field數量極多,耗時就會成倍增加。
又如使用SUNION對兩個Set執行Union操做,或使用SORT對List/Set執行排序操做等時,都應該嚴加註意。
避免在使用這些O(N)命令時發生問題主要有幾個辦法:
不要把List當作列表使用,僅當作隊列來使用;
經過機制嚴格控制Hash、Set、Sorted Set的大小;
可能的話,將排序、並集、交集等操做放在客戶端執行;
絕對禁止使用KEYS命令;
避免一次性遍歷集合類型的全部成員,而應使用SCAN類的命令進行分批的,遊標式的遍歷。
Redis提供了SCAN命令,能夠對Redis中存儲的全部key進行遊標式的遍歷,避免使用KEYS命令帶來的性能問題。同時還有SSCAN/HSCAN/ZSCAN等命令,分別用於對Set/Hash/Sorted Set中的元素進行遊標式遍歷。SCAN類命令的使用請參考官方文檔:https://redis。io/commands/scan
Redis提供了Slow Log功能,能夠自動記錄耗時較長的命令。相關的配置參數有兩個:
slowlog-log-slower-than xxxms #執行時間慢於xxx毫秒的命令計入Slow Logslowlog-max-len xxx #Slow Log的長度,即最大紀錄多少條Slow Log;
使用SLOWLOG GET [number]命令,能夠輸出最近進入Slow Log的number條命令。
使用SLOWLOG RESET命令,能夠重置Slow Log;
Redis延遲
一、網絡引起的延遲
儘量使用長鏈接或鏈接池,避免頻繁建立銷燬鏈接;
客戶端進行的批量數據操做,應使用Pipeline特性在一次交互中完成。
二、數據持久化引起的延遲
Redis的數據持久化工做自己就會帶來延遲,須要根據數據的安全級別和性能要求制定合理的持久化策略:
AOF + fsync always的設置雖然可以絕對確保數據安全,但每一個操做都會觸發一次fsync,會對Redis的性能有比較明顯的影響
AOF + fsync every second是比較好的折中方案,每秒fsync一次
AOF + fsync never會提供AOF持久化方案下的最優性能
使用RDB持久化一般會提供比使用AOF更高的性能,但須要注意RDB的策略配置
每一次RDB快照和AOF Rewrite都須要Redis主進程進行fork操做。fork操做自己可能會產生較高的耗時,與CPU和Redis佔用的內存大小有關。根據具體的狀況合理配置RDB快照和AOF Rewrite時機,避免過於頻繁的fork帶來的延遲;
Redis在fork子進程時須要將內存分頁表拷貝至子進程,以佔用了24GB內存的Redis實例爲例,共須要拷貝24GB / 4kB * 8 = 48MB的數據。在使用單Xeon 2。27Ghz的物理機上,這一fork操做耗時216ms。
能夠經過INFO命令返回的latest_fork_usec字段查看上一次fork操做的耗時(微秒)
Swap引起的延遲
當Linux將Redis所用的內存分頁移至swap空間時,將會阻塞Redis進程,致使Redis出現不正常的延遲。Swap一般在物理內存不足或一些進程在進行大量I/O操做時發生,應儘量避免上述兩種狀況的出現。
/proc//smaps文件中會保存進程的swap記錄,經過查看這個文件,可以判斷Redis的延遲是否由Swap產生。若是這個文件中記錄了較大的Swap size,則說明延遲頗有多是Swap形成的。
三、數據淘汰引起的延遲
當同一秒內有大量key過時時,也會引起Redis的延遲。在使用時應儘可能將key的失效時間錯開。
引入讀寫分離機制
Redis的主從複製能力能夠實現一主多從的多節點架構,在這一架構下,主節點接收全部寫請求,並將數據同步給多個從節點。
在這一基礎上,咱們可讓從節點提供對實時性要求不高的讀請求服務,以減少主節點的壓力。
尤爲是針對一些使用了長耗時命令的統計類任務,徹底能夠指定在一個或多個從節點上執行,避免這些長耗時命令影響其餘請求的響應。
主從複製與集羣分片
主從複製:
Redis支持一主多從的主從複製架構。一個Master實例負責處理全部的寫請求,Master將寫操做同步至全部Slave。
藉助Redis的主從複製,能夠實現讀寫分離和高可用:
實時性要求不是特別高的讀請求,能夠在Slave上完成,提高效率。特別是一些週期性執行的統計任務,這些任務可能須要執行一些長耗時的Redis命令,能夠專門規劃出1個或幾個Slave用於服務這些統計任務
藉助Redis Sentinel能夠實現高可用,當Master crash後,Redis Sentinel可以自動將一個Slave晉升爲Master,繼續提供服務
啓用主從複製很是簡單,只須要配置多個Redis實例,在做爲Slave的Redis實例中配置:
slaveof 192。168。1。1 6379 #指定Master的IP和端口
當Slave啓動後,會從Master進行一次冷啓動數據同步,由Master觸發BGSAVE生成RDB文件推送給Slave進行導入,導入完成後Master再將增量數據經過Redis Protocol同步給Slave。以後主從之間的數據便一直以Redis Protocol進行同步;
使用Sentinel作自動failover
Redis的主從複製功能自己只是作數據同步,並不提供監控和自動failover能力,要經過主從複製功能來實現Redis的高可用,還須要引入一個組件:Redis Sentinel
Redis Sentinel是Redis官方開發的監控組件,能夠監控Redis實例的狀態,經過Master節點自動發現Slave節點,並在監測到Master節點失效時選舉出一個新的Master,並向全部Redis實例推送新的主從配置。
Redis Sentinel須要至少部署3個實例才能造成選舉關係。
關鍵配置:
sentinel monitor mymaster 127。0。0。1 6379 2 #Master實例的IP、端口,以及選舉須要的同意票數sentinel down-after-milliseconds mymaster 60000 #多長時間沒有響應視爲Master失效sentinel failover-timeout mymaster 180000 #兩次failover嘗試間的間隔時長sentinel parallel-syncs mymaster 1 #若是有多個Slave,能夠經過此配置指定同時重新Master進行數據同步的Slave數,避免全部Slave同時進行數據同步致使查詢服務也不可用
另外須要注意的是,Redis Sentinel實現的自動failover不是在同一個IP和端口上完成的,也就是說自動failover產生的新Master提供服務的IP和端口與以前的Master是不同的,因此要實現HA,還要求客戶端必須支持Sentinel,可以與Sentinel交互得到新Master的信息才行。
集羣分片:
爲什麼要作集羣分片:
Redis中存儲的數據量大,一臺主機的物理內存已經沒法容納
Redis的寫請求併發量大,一個Redis實例以沒法承載
當上述兩個問題出現時,就必需要對Redis進行分片了。
Redis的分片方案有不少種,例如不少Redis的客戶端都自行實現了分片功能,也有向Twemproxy這樣的以代理方式實現的Redis分片方案。然而首選的方案還應該是Redis官方在3。0版本中推出的Redis Cluster分片方案。
本文不會對Redis Cluster的具體安裝和部署細節進行介紹,重點介紹Redis Cluster帶來的好處與弊端。
Redis Cluster的能力
可以自動將數據分散在多個節點上
當訪問的key不在當前分片上時,可以自動將請求轉發至正確的分片
當集羣中部分節點失效時仍能提供服務
其中第三點是基於主從複製來實現的,Redis Cluster的每一個數據分片都採用了主從複製的結構,原理和前文所述的主從複製徹底一致,惟一的區別是省去了Redis Sentinel這一額外的組件,由Redis Cluster負責進行一個分片內部的節點監控和自動failover。
Redis Cluster分片原理
Redis Cluster中共有16384個hash slot,Redis會計算每一個key的CRC16,將結果與16384取模,來決定該key存儲在哪個hash slot中,同時須要指定Redis Cluster中每一個數據分片負責的Slot數。Slot的分配在任什麼時候間點均可以進行從新分配。
客戶端在對key進行讀寫操做時,能夠鏈接Cluster中的任意一個分片,若是操做的key不在此分片負責的Slot範圍內,Redis Cluster會自動將請求重定向到正確的分片上。
hash tags
在基礎的分片原則上,Redis還支持hash tags功能,以hash tags要求的格式明明的key,將會確保進入同一個Slot中。例如:{uiv}user:1000和{uiv}user:1001擁有一樣的hash tag {uiv},會保存在同一個Slot中。
使用Redis Cluster時,pipelining、事務和LUA 功能涉及的key必須在同一個數據分片上,不然將會返回錯誤。如要在Redis Cluster中使用上述功能,就必須經過hash tags來確保一個pipeline或一個事務中操做的全部key都位於同一個Slot中。
有一些客戶端(如Redisson)實現了集羣化的pipelining操做,能夠自動將一個pipeline裏的命令按key所在的分片進行分組,分別發到不一樣的分片上執行。可是Redis不支持跨分片的事務,事務和LUA 仍是必須遵循全部key在一個分片上的規則要求。
主從複製 vs 集羣分片
在設計軟件架構時,要如何在主從複製和集羣分片兩種部署方案中取捨呢?
從各個方面看,Redis Cluster都是優於主從複製的方案
Redis Cluster可以解決單節點上數據量過大的問題
Redis Cluster可以解決單節點訪問壓力過大的問題
Redis Cluster包含了主從複製的能力
那是否是表明Redis Cluster永遠是優於主從複製的選擇呢?
並非。
軟件架構永遠不是越複雜越好,複雜的架構在帶來顯著好處的同時,必定也會帶來相應的弊端。採用Redis Cluster的弊端包括:
1 維護難度增長。在使用Redis Cluster時,須要維護的Redis實例數倍增,須要監控的主機數量也相應增長,數據備份/持久化的複雜度也會增長。同時在進行分片的增減操做時,還須要進行reshard操做,遠比主從模式下增長一個Slave的複雜度要高。
2客戶端資源消耗增長。當客戶端使用鏈接池時,須要爲每個數據分片維護一個鏈接池,客戶端同時須要保持的鏈接數成倍增多,加大了客戶端自己和操做系統資源的消耗。
3性能優化難度增長。你可能須要在多個分片上查看Slow Log和Swap日誌才能定位性能問題。
4事務和LUA 的使用成本增長。在Redis Cluster中使用事務和LUA 特性有嚴格的限制條件,事務和中操做的key必須位於同一個分片上,這就使得在開發時必須對相應場景下涉及的key進行額外的規劃和規範要求。若是應用的場景中大量涉及事務和的使用,如何在保證這兩個功能的正常運做前提下把數據平均分到多個數據分片中就會成爲難點。
因此說,在主從複製和集羣分片兩個方案中作出選擇時,應該從應用軟件的功能特性、數據和訪問量級、將來發展規劃等方面綜合考慮,只在確實有必要引入數據分片時再使用Redis Cluster。
下面是一些建議:
須要在Redis中存儲的數據有多大?將來2年內可能發展爲多大?這些數據是否都須要長期保存?是否能夠使用LRU算法進行非熱點數據的淘汰?綜合考慮前面幾個因素,評估出Redis須要使用的物理內存。
用於部署Redis的主機物理內存有多大?有多少能夠分配給Redis使用?對比(1)中的內存需求評估,是否足夠用?
Redis面臨的併發寫壓力會有多大?在不使用pipelining時,Redis的寫性能能夠超過10萬次/秒(更多的benchmark能夠參考 https://redis。io/topics/benchmarks )
在使用Redis時,是否會使用到pipelining和事務功能?使用的場景多很少?
綜合上面幾點考慮,若是單臺主機的可用物理內存徹底足以支撐對Redis的容量需求,且Redis面臨的併發寫壓力距離Benchmark值還尚有距離,建議採用主從複製的架構,能夠省去不少沒必要要的麻煩。同時,若是應用中大量使用pipelining和事務,也建議儘量選擇主從複製架構,能夠減小設計和開發時的複雜度。
Redis Java客戶端的選擇
Redis的Java客戶端不少,官方推薦的有三種:Jedis、Redisson和lettuce。
在這裏對Jedis和Redisson進行對比介紹
Jedis:
輕量,簡潔,便於集成和改造
支持鏈接池
支持pipelining、事務、LUA ing、Redis Sentinel、Redis Cluster
不支持讀寫分離,須要本身實現
文檔差(真的不好,幾乎沒有……)
Redisson:
基於Netty實現,採用非阻塞IO,性能高
支持異步請求
支持鏈接池
支持pipelining、LUA ing、Redis Sentinel、Redis Cluster
不支持事務,官方建議以LUA ing代替事務
支持在Redis Cluster架構下使用pipelining
支持讀寫分離,支持讀負載均衡,在主從複製和Redis Cluster架構下均可以使用
內建Tomcat Session Manager,爲Tomcat 6/7/8提供了會話共享功能
能夠與Spring Session集成,實現基於Redis的會話共享
文檔較豐富,有中文文檔
對於Jedis和Redisson的選擇,一樣應遵循前述的原理,儘管Jedis比起Redisson有各類各樣的不足,但也應該在須要使用Redisson的高級特性時再選用Redisson,避免形成沒必要要的程序複雜度提高。
五、緩存在高併發場景下的常見問題
5.一、緩存一致性問題
當數據時效性要求很高時,須要保證緩存中的數據與數據庫中的保持一致,並且須要保證緩存節點和副本中的數據也保持一致,不能出現差別現象。這就比較依賴緩存的過時和更新策略。通常會在數據發生更改的時,主動更新緩存中的數據或者移除對應的緩存。
在不少應用場景中,當一個數據發生變動的時候,不少人在考慮怎麼樣確保緩存數據和數據庫中數據保存一致性,確保從緩存讀取的數據是最新的。甚至,有人在對應數據變動的時候,先更新數據庫,而後再去更新緩存。我以爲這個考慮不太現實,一方面這會致使代碼層次邏輯變得複雜,另一方面也真想不明白還要緩存幹什麼了。在絕大多數的應用中,緩存中的數據和數據庫中的數據是不一致的。即,咱們犧牲了實時性換回了訪問速度。好比,一篇常常訪問的帖子,可能這篇帖子已經在數據庫層次進行了變動。而咱們每次訪問的時候,讀取的都是緩存中的數據(帖子)。既然是緩存,那麼必然是對實時性能夠有必定的容忍度的數據,容忍度的時間能夠是5分鐘,也能夠是5小時,取決於業務場景的要求。相反,必定要求是實時性的數據庫,就不該該從緩存裏讀取,好比庫存,再好比價格。
5.二、緩存併發問題
緩存過時後將嘗試從後端數據庫獲取數據,這是一個看似合理的流程。可是,在高併發場景下,有可能多個請求併發的去從數據庫獲取數據,對後端數據庫形成極大的衝擊,甚至致使 「雪崩」現象。此外,當某個緩存key在被更新時,同時也可能被大量請求在獲取,這也會致使一致性的問題。那如何避免相似問題呢?咱們會想到相似「鎖」的機制,在緩存更新或者過時的狀況下,先嚐試獲取到鎖,當更新或者從數據庫獲取完成後再釋放鎖,其餘的請求只須要犧牲必定的等待時間,便可直接從緩存中繼續獲取數據。
5.三、緩存穿透問題
緩存穿透在有些地方也稱爲「擊穿」。不少朋友對緩存穿透的理解是:因爲緩存故障或者緩存過時致使大量請求穿透到後端數據庫服務器,從而對數據庫形成巨大沖擊。
這實際上是一種誤解。真正的緩存穿透應該是這樣的:
在高併發場景下,若是某一個key被高併發訪問,沒有被命中,出於對容錯性考慮,會嘗試去從後端數據庫中獲取,從而致使了大量請求達到數據庫,而當該key對應的數據自己就是空的狀況下,這就致使數據庫中併發的去執行了不少沒必要要的查詢操做,從而致使巨大沖擊和壓力。
能夠經過下面的幾種經常使用方式來避免緩存傳統問題:
一、緩存空對象
對查詢結果爲空的對象也進行緩存,若是是集合,能夠緩存一個空的集合(非null),若是是緩存單個對象,能夠經過字段標識來區分。這樣避免請求穿透到後端數據庫。同時,也須要保證緩存數據的時效性。這種方式實現起來成本較低,比較適合命中不高,但可能被頻繁更新的數據。
二、單獨過濾處理
對全部可能對應數據爲空的key進行統一的存放,並在請求前作攔截,這樣避免請求穿透到後端數據庫。這種方式實現起來相對複雜,比較適合命中不高,可是更新不頻繁的數據。
5.四、緩存顛簸問題
緩存的顛簸問題,有些地方可能被成爲「緩存抖動」,能夠看作是一種比「雪崩」更輕微的故障,可是也會在一段時間內對系統形成衝擊和性能影響。通常是因爲緩存節點故障致使。業內推薦的作法是經過一致性Hash算法來解決。
5.五、緩存的雪崩現象
緩存雪崩就是指因爲緩存的緣由,致使大量請求到達後端數據庫,從而致使數據庫崩潰,整個系統崩潰,發生災難。致使這種現象的緣由有不少種,上面提到的「緩存併發」,「緩存穿透」,「緩存顛簸」等問題,其實均可能會致使緩存雪崩現象發生。這些問題也可能會被惡意攻擊者所利用。還有一種狀況,例如某個時間點內,系統預加載的緩存週期性集中失效了,也可能會致使雪崩。爲了不這種週期性失效,能夠經過設置不一樣的過時時間,來錯開緩存過時,從而避免緩存集中失效。
從應用架構角度,咱們能夠經過限流、降級、熔斷等手段來下降影響,也能夠經過多級緩存來避免這種災難。此外,從整個研發體系流程的角度,應該增強壓力測試,儘可能模擬真實場景,儘早的暴露問題從而防範。
緩存雪崩是因爲原有緩存失效(過時),新緩存未到期間。全部請求都去查詢數據庫,而對數據庫CPU和內存形成巨大壓力,嚴重的會形成數據庫宕機。從而造成一系列連鎖反應,形成整個系統崩潰。
一、碰到這種狀況,通常併發量不是特別多的時候,使用最多的解決方案是加鎖排隊。
二、加鎖排隊只是爲了減輕數據庫的壓力,並無提升系統吞吐量。假設在高併發下,緩存重建期間key是鎖着的,這是過來1000個請求999個都在阻塞的。一樣會致使用戶等待超時,這是個治標不治本的方法。還有一個解決辦法解決方案是:給每個緩存數據增長相應的緩存標記,記錄緩存的是否失效,若是緩存標記失效,則更新數據緩存。
緩存標記:記錄緩存數據是否過時,若是過時會觸發通知另外的線程在後臺去更新實際key的緩存。
緩存數據:它的過時時間比緩存標記的時間延長1倍,例:標記緩存時間30分鐘,數據緩存設置爲60分鐘。 這樣,當緩存標記key過時後,實際緩存還能把舊數據返回給調用端,直到另外的線程在後臺更新完成後,纔會返回新緩存。這樣作後,就能夠必定程度上提升系統吞吐量。
緩存穿透是指用戶查詢數據,在數據庫沒有,天然在緩存中也不會有。這樣就致使用戶查詢的時候,在緩存中找不到,每次都要去數據庫再查詢一遍,而後返回空。這樣請求就繞過緩存直接查數據庫,這也是常常提的緩存命中率問題。
解決的辦法就是:若是查詢數據庫也爲空,直接設置一個默認值存放到緩存,這樣第二次到緩衝中獲取就有值了,而不會繼續訪問數據庫,這種辦法最簡單粗暴。
把空結果也給緩存起來,這樣下次一樣的請求就能夠直接返回空了,便可以免當查詢的值爲空時引發的緩存穿透。同時也能夠單獨設置個緩存區域存儲空值,對要查詢的key進行預先校驗,而後再放行給後面的正常緩存處理邏輯。
緩存預熱就是系統上線後,將相關的緩存數據直接加載到緩存系統。這樣避免,用戶請求的時候,再去加載相關的數據。
解決思路:
1,直接寫個緩存刷新頁面,上線時手工操做下。
2,數據量不大,能夠在WEB系統啓動的時候加載。
3,定時刷新緩存。
5.五、緩存更新
緩存淘汰的策略有兩種:
(1) 定時去清理過時的緩存。
(2)當有用戶請求過來時,再判斷這個請求所用到的緩存是否過時,過時的話就去底層系統獲得新數據並更新緩存。
二者各有優劣,第一種的缺點是維護大量緩存的key是比較麻煩的,第二種的缺點就是每次用戶請求過來都要判斷緩存失效,邏輯相對比較複雜,具體用哪一種方案,能夠根據本身的應用場景來權衡。
一、預估失效時間 二、 版本號(必須單調遞增,時間戳是最好的選擇)三、 提供手動清理緩存的接口。
六、提升緩存命中率
6.一、緩存命中率的介紹
命中:能夠直接經過緩存獲取到須要的數據。
不命中:沒法直接經過緩存獲取到想要的數據,須要再次查詢數據庫或者執行其它的操做。緣由多是因爲緩存中根本不存在,或者緩存已通過期。
一般來說,緩存的命中率越高則表示使用緩存的收益越高,應用的性能越好(響應時間越短、吞吐量越高),抗併發的能力越強。因而可知,在高併發的互聯網系統中,緩存的命中率是相當重要的指標。
6.二、影響緩存命中率的幾個因素
1業務場景和業務需求
緩存適合「讀多寫少」的業務場景,反之,使用緩存的意義其實並不大,命中率會很低。
業務需求決定了對時效性的要求,直接影響到緩存的過時時間和更新策略。時效性要求越低,就越適合緩存。在相同key和相同請求數的狀況下,緩存時間越長,命中率會越高。
互聯網應用的大多數業務場景下都是很適合使用緩存的。
2緩存的設計(粒度和策略)
一般狀況下,緩存的粒度越小,命中率會越高。舉個實際的例子說明:
當緩存單個對象的時候(例如:單個用戶信息),只有當該對象對應的數據發生變化時,咱們才須要更新緩存或者讓移除緩存。而當緩存一個集合的時候(例如:全部用戶數據),其中任何一個對象對應的數據發生變化時,都須要更新或移除緩存。
還有另外一種狀況,假設其餘地方也須要獲取該對象對應的數據時(好比其餘地方也須要獲取單個用戶信息),若是緩存的是單個對象,則能夠直接命中緩存,反之,則沒法直接命中。這樣更加靈活,緩存命中率會更高。
此外,緩存的更新/過時策略也直接影響到緩存的命中率。當數據發生變化時,直接更新緩存的值會比移除緩存(或者讓緩存過時)的命中率更高,固然,系統複雜度也會更高。
3緩存容量和基礎設施
緩存的容量有限,則容易引發緩存失效和被淘汰(目前多數的緩存框架或中間件都採用了LRU算法)。同時,緩存的技術選型也是相當重要的,好比採用應用內置的本地緩存就比較容易出現單機瓶頸,而採用分佈式緩存則畢竟容易擴展。因此須要作好系統容量規劃,並考慮是否可擴展。此外,不一樣的緩存框架或中間件,其效率和穩定性也是存在差別的。
4其餘因素
當緩存節點發生故障時,須要避免緩存失效並最大程度下降影響,這種特殊狀況也是架構師須要考慮的。業內比較典型的作法就是經過一致性Hash算法,或者經過節點冗餘的方式。
有些朋友可能會有這樣的理解誤區:既然業務需求對數據時效性要求很高,而緩存時間又會影響到緩存命中率,那麼系統就別使用緩存了。其實這忽略了一個重要因素--併發。一般來說,在相同緩存時間和key的狀況下,併發越高,緩存的收益會越高,即使緩存時間很短。
6.三、提升緩存命中率的方法
從架構師的角度,須要應用盡量的經過緩存直接獲取數據,並避免緩存失效。這也是比較考驗架構師能力的,須要在業務需求,緩存粒度,緩存策略,技術選型等各個方面去通盤考慮並作權衡。儘量的聚焦在高頻訪問且時效性要求不高的熱點業務上,經過緩存預加載(預熱)、增長存儲容量、調整緩存粒度、更新緩存等手段來提升命中率。
對於時效性很高(或緩存空間有限),內容跨度很大(或訪問很隨機),而且訪問量不高的應用來講緩存命中率可能長期很低,可能預熱後的緩存還沒來得被訪問就已通過期了。
一致性Hash算法
對於分佈式緩存,不一樣機器上存儲不一樣對象的數據。爲了實現這些緩存機器的負載均衡,能夠採起一致性hash算法來實現。
一致性hash算法經過一個叫做一致性hash環的數據結構實現。這個環的起點是0,終點是2^32 - 1,而且起點與終點鏈接,環的中間的整數按逆時針分佈,故這個環的整數分佈範圍是[0, 2^32-1]。
假設如今咱們有4個對象,分別爲o1,o2,o3,o4,使用hash函數計算這4個對象的hash值(範圍爲0 ~ 2^32-1):
hash(o1) = m1
hash(o2) = m2
hash(o3) = m3
hash(o4) = m4
把m1,m2,m3,m4這4個值放置到hash環上,獲得以下圖:
使用一樣的hash函數,咱們將機器也放置到hash環上。假設咱們有三臺緩存機器,分別爲 c1,c2,c3,使用hash函數計算這3臺機器的hash值:
hash(c1) = t1
hash(c2) = t2
hash(c3) = t3
把t1,t2,t3 這3個值放置到hash環上,獲得以下圖:
爲對象選擇機器
將對象和機器都放置到同一個hash環後,在hash環上順時針查找距離這個對象的hash值最近的機器,便是這個對象所屬的機器。
例如,對於對象o2,順序針找到最近的機器是c1,故機器c1會緩存對象o2。而機器c2則緩存o3,o4,機器c3則緩存對象o1。
新加入的機器c4只分擔了機器c2的負載,機器c1與c3的負載並無由於機器c4的加入而減小負載壓力。若是4臺機器的性能是同樣的,那麼這種結果並非咱們想要的。
爲此,咱們引入虛擬節點來解決負載不均衡的問題。
將每臺物理機器虛擬爲一組虛擬機器,將虛擬機器放置到hash環上,若是須要肯定對象的機器,先肯定對象的虛擬機器,再由虛擬機器肯定物理機器。
引用: