作者介紹
楊彪 , 螞蟻金服技術專家,《分散式服務架構:原理、設計與實戰》和《可伸縮服務架構:框架與中介軟體》作者。近10年網際網路和遊戲行業工作經驗,曾在酷我音樂盒、人人遊戲和掌趣科技等上市公司擔任核心研發職位,做過日活躍使用者量達千萬的專案,也做過多款月流水千萬以上的遊戲。
本文節選自即將出版的《可伸縮服務架構:框架與中介軟體》一書,作者:李豔鵬、楊彪、李海亮、賈博巖、劉淏
如今,市面上的快取解決方案已經逐步成熟了,今天我將選取其中一些代表性的方案包括Redis、Memcached和Tair進行對比,幫助大家 在生產實踐中更好地進行技術選型。
一、常用的分散式快取的對比
常用的分散式快取包括Redis、Memcached和阿里巴巴的Tair(見下表),因為Redis提供的資料結構比較豐富且簡單易用,所以Redis的使用廣泛。
請點選此處輸入圖片描述
下面我們從9個大方面來對比最常用的Redis和Memcached。
1.資料型別
Redis一共支援5種資料型別,每種資料型別對應不同的資料結構,有簡單的String型別、壓縮串、字典、跳躍表等。 跳躍表是比較新型的資料結構,常用於高效能的查詢,可以達到log2N的查詢速度,而且跳躍表相對於紅黑樹,在更新時變更的節點較少,更易於實現併發操作。
Memcache只支援對鍵值對的儲存,並不支援其它資料結構。
2.執行緒模型
Redis使用單執行緒實現,Memcache等使用多執行緒實現,因此我們不推薦在Redis中儲存太大的內容,否則會阻塞其它請求。
因為快取操作都是記憶體操作,只有很少的計算操作,所以在單執行緒下效能很好。Redis實現的單執行緒的非阻塞網路I/O模型,適合快速地操作邏輯,有複雜的長邏輯時會影響效能。 對於長邏輯應該配置多個例項來提高多核CPU的利用率,也就是說,可以使用單機器多埠來配置多個例項,官方的推薦是一臺機器使用8個例項。
它實現的非阻塞I/O模型基於Libevent庫中關於Epoll的兩個檔案加上自己簡單實現的事件通知模型,簡單小巧,作者的思想就是保持實現簡單、減少依賴。由於在伺服器中只有一個執行緒,因此提供了管道來合併請求和批量執行,縮短了通訊消耗的時間。
Memcache也使用了非阻塞I/O模型,但是使用了多執行緒,可以應用於多種場景,請求的邏輯可大可小、可長可短,不會出現一個邏輯複雜的請求阻塞對其它請求的響應的場景。它直接依賴Libevent庫實現,依賴比較複雜,損失了在一些特定環境下的高效能。
3.持久機制
Redis提供了兩種持久機制,包括RDB和AOF,前者是定時的持久機制,但在出現當機時可能會出現資料丟失,後者是基於操作日誌的持久機制。
Memcahe並不提供持久機制,因為Memache的設計理念就是設計一個單純的快取,快取的資料都是臨時的,不應該是持久的,也不應該是一個大資料的資料庫,快取未命中時回源查詢資料庫是天經地義的,但可以通過第三方庫MemcacheDB來支援它的永續性。
4.客戶端
常見的Redis Java客戶端Jedis使用阻塞I/O,但可以配置連線池,並提供了一致性雜湊分片的邏輯,也可以使用開源的客戶端分片框架Redic。
Memecache的客戶端包括Memcache Java Client、Spy Client、XMemcache等,Memcache Java Client使用阻塞I/O,而Spy Client/XMemcache使用非阻塞I/O。
我們知道,阻塞I/O不需要額外的執行緒,非阻塞I/O會開啟額外的請求執行緒(在Boss執行緒池裡)監聽埠,一個請求在處理後就釋放工作者執行緒(在Worker執行緒池中),請求執行緒在監聽到有返回結果時,一旦有I/O返回結果就被喚醒,然後開始處理響應資料並寫回網路Socket連線,所以從理論上來講,非阻塞I/O的吞吐量和響應能力會更高。
5.高可用
Redis支援主從節點複製配置,從節點可使用RDB和快取的AOF命令進行同步和恢復。Redis還支援Sentinel和Cluster(從3.0版本開始)等高可用叢集方案。
Memecache不支援高可用模型,可使用第三方Megagent代理,當一個例項當機時,可以連線另外一個例項來實現。
6.對佇列的支援
Redis本身支援lpush/brpop、publish/subscribe/psubscribe等佇列和訂閱模式。
Memcache不支援佇列,可通過第三方MemcachQ來實現。
7.事務
Redis提供了一些在一定程度上支援執行緒安全和事務的命令,例如:multi/exec、watch、inc等。由於Redis伺服器是單執行緒的,任何單一請求的伺服器操作命令都是原子的,但跨客戶端的操作並不保證原子性,所以對於同一個連線的多個操作序列也不保證事務。
Memcached的單個命令也是執行緒安全的,單個連線的多個命令序列不是執行緒安全的,它也提供了inc等執行緒安全的自加命令,並提供了gets/cas保證執行緒安全。
8.資料淘汰策略
Redis提供了豐富的淘汰策略,包括maxmemory、maxmemory-policy、volatile-lru、allkeys-lru、volatile-random、allkeys-random、volatile-ttl、noeviction(return error)等。
Memecache在容量達到指定值後,就基於LRU(Least Recently Used)演算法自動刪除不使用的快取。在某些情況下LRU機制反倒會帶來麻煩,會將不期待的資料從記憶體中清除,在這種情況下啟動Memcache時,可以通過“M”引數禁止LRU演算法。
9.記憶體分配
Redis為了遮蔽不同平臺之間的差異及統計記憶體佔用量等,對記憶體分配函式進行了一層封裝,在程式中統一使用zmalloc、zfree系列函式,這些函式位於zmalloc.h/zmalloc.c檔案中。封裝就是為了遮蔽底層平臺的差異,同時方便自己實現相關的統計函式。具體的實現方式如下:
若系統中存在Google的TC_MALLOC庫,則使用tc_malloc一族的函式代替原本的malloc一族的函式。
若當前系統是Mac系統,則使用系統的記憶體分配函式。
對於其它情況,在每一段分配好的空間前面同時多分配一個定長的欄位,用來記錄分配的空間大小,通過這種方式來實現簡單有效的記憶體分配。
Memcache採用slab table的方式分配記憶體,首先把可得的記憶體按照不同的大小來分類,在使用時根據需求找到最接近於需求大小的塊分配,來減少記憶體碎片,但是這需要進行合理配置才能達到效果。
從上面的對比可以看到,Redis在實現和使用上更簡單,但是功能更強大,效率更高,應用也更廣泛。下面將對Redis進行初步介紹,給初學者一個初體驗式的學習引導。
二、Redis初體驗
Redis是一個能夠儲存多種資料物件的開源Key-Value儲存系統,使用ANSI C語言編寫,可以僅僅當作記憶體資料庫使用,也可以作為以日誌為儲存方式的資料庫系統,並提供多種語言的API。
1.使用場景
我們通常把Redis當作一個非本地快取來使用,很少用到它的一些高階功能。在使用中最容易出問題的是用Redis來儲存JSON資料,因為Redis不像Elasticsearch或者PostgreSQL那樣可以很好地支援JSON資料。 所以我們經常把JSON當作一個大的String直接放到Redis中,但現在的JSON資料都是連環巢狀的,每次更新時都要先獲取整個JSON,然後更改其中一個欄位再放上去。
一個常見的JSON資料的Java物件定義如下:
public class Commodity {
private long price;
private String title;
……
}
在海量請求的前提下,在Redis中每次更新一個欄位,比如銷量欄位,都會產生較大的流量。在實際情況下,JSON字串往往非常複雜,體積達到數百KB都是有可能的,導致在頻繁更新資料時使網路I/O跑滿,甚至導致系統超時、崩潰。
因此,Redis官方推薦採用雜湊來儲存物件,比如有3個商品物件,ID分別是123、124和12345,我們通過雜湊把它們儲存在Redis中,在更新其中的欄位時可以這樣做:
HSET commodity:123 price 100
HSET commodity:124 price 101
HSET commodity:12345 price 101
HSET commodity:123 title banana
HSET commodity:124 title apple
HSET commodity:12345 title orange
也就是說,用商品的型別名和ID組成一個Redis雜湊物件的KEY。在獲取某一屬性時只需這樣做就可以獲取單獨的屬性: HGET commodity: 12345。
2.Redis的高可用方案:哨兵
Redis官方推出了一個叢集管理工具,叫作哨兵(Sentinel),負責在節點中選出主節點,按照分散式叢集的管理辦法來操作叢集節點的上線、下線、監控、提醒、自動故障切換(主備切換),且實現了著名的RAFT選主協議,從而保證了系統選主的一致性。
這裡給出一個哨兵的通用部署方案。哨兵節點一般至少要部署3份,可以和被監控的節點放在一個虛擬機器中,常見的哨兵部署如圖所示。
請點選此處輸入圖片描述
在這個系統中,初始狀態下的機器A是主節點,機器B和機器C是從節點。
由於有3個哨兵節點,每個機器執行1個哨兵節點,所以這裡設定quorum = 2,也就是在主節點無響應後,有至少兩個哨兵無法與主節點通訊,則認為主節點當機,然後在從節點中選舉新的主節點來使用。
在發生網路分割槽時,若機器A所在的主機網路不可用,則機器B和機器C上的兩個Sentinel例項會啟動failover並把機器B選舉為主節點。
Sentinel叢集的特性保證了機器B和機器C上的兩個Sentinel例項得到了關於主節點的最新配置。但機器A上的Sentinel節點依然持有舊的配置,因為它與外界隔離了。
在 網路恢復後,我們知道 機器 A 上的 Sentinel 例項 將會更新它的配置。但是,如果客戶端所連線的 主機節點也 被網路隔離, 則 客戶端將依然可以向 機器 A 的 Redis 節點 寫資料,但 在 網路恢復後, 機器 A 的 Redis 節點 就會變成一個 從節點 ,那麼在網路隔離期間,客戶端向 機器 A的 Redis 節點寫入 的資料將會丟失 ,這是不可避免的。
如果把 Redis 當作 快取來使用,那麼 我們 也許能容忍這部分資料的丟失 ,但若 把 Redis 當作一個儲存系統來使用,就無法容忍這部分資料的丟失了 , 因為 Redis 採用的是非同步複製,在這樣的場景下 無法 避免資料的丟失。
在這裡,我們可以通過以下配置來配置每個Redis例項,使得資料不會丟失:
min-slaves-to-write 1
min-slaves-max-lag 10
通過上面的配置,當一個Redis是主節點時,如果它不能向至少一個從節點寫資料(上面的min-slaves-to-write指定了slave的數量),則它將會拒絕接收客戶端的寫請求。由於複製是非同步的,所以主節點無法向從節點寫資料就意味著從節點要麼斷開了連線,要麼沒在指定的時間內向主節點傳送同步資料的請求。
所以,採用這樣的配置可排除網路分割槽後主節點被孤立但仍然寫入資料,從而導致資料丟失的場景。
3.Redis叢集
Redis在3.0中也引入了叢集的概念,用於解決一些大資料量和高可用的問題,但是,為了達到高效能的目的,叢集不是強一致性的,使用的是非同步複製,在資料到主節點後,主節點返回成功,資料被非同步地複製給從節點。
首先,我們來學習Redis的叢集分片機制。Redis使用CRC16(key) mod 16384進行分片,一共分16384個雜湊槽,比如若叢集有3個節點,則我們按照如下規則分配雜湊槽:
A節點包含0-5500的雜湊槽;
B節點包含5500-11000的雜湊槽;
C節點包含11000-16384的雜湊槽。
這裡設定了3個主節點和3個從節點,叢集分片如圖所示。
請點選此處輸入圖片描述
圖中共有3個Redis主從伺服器的複製節點,其中任意兩個節點之間都是相互連通的,客戶端可以與其中任意一個節點相連線,然後訪問叢集中的任意一個節點,對其進行存取和其他操作。
那Redis是怎麼做到的呢?首先,在Redis的每個節點上都會儲存雜湊槽資訊,我們可以將它理解為是一個可以儲存兩個數值的變數,這個變數的取值範圍是0-16383。根據這些資訊,我們就可以找到每個節點負責的雜湊槽,進而找到資料所在的節點。
Redis叢集實際上是一個叢集管理的外掛,當我們提供一個存取的關鍵字時,就會根據CRC16的演算法得出一個結果,然後把結果除以16384求餘數,這樣每個關鍵字都會對應一個編號為0-16383的雜湊槽,通過這個值找到對應的插槽所對應的節點,然後直接自動跳轉到這個對應的節點上進行存取操作。但是這些都是由叢集的內部機制實現的,我們不需要手工實現。