螞蟻技術專家:同爲分佈式緩存,爲什麼 Redis 更勝一籌?

 

現在,市面上的緩存解決方案已經逐步成熟了,今天我將選取其中一些表明性的方案包括Redis、Memcached和Tair進行對比,幫助你們 在生產實踐中更好地進行技術選型。算法

1、經常使用的分佈式緩存的對比spring

經常使用的分佈式緩存包括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進行初步介紹,給初學者一個初體驗式的學習引導。

2、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分別是12三、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的哈希槽,經過這個值找到對應的插槽所對應的節點,而後直接自動跳轉到這個對應的節點上進行存取操做。可是這些都是由集羣的內部機制實現的,咱們不須要手工實現。

注:關注做者微信公衆號,瞭解更多分佈式架構、微服務、netty、MySQL、spring、、性能優化、等知識點。

公衆號:《 Java大蝸牛 

相關文章
相關標籤/搜索