分佈式緩存應用場景與redis持久化機制

分佈式緩存應用場景與redis持久化機制

隨着用戶數和訪問量越來越大,我們的系統應用需要支撐更多的併發量。

但是往往我們的應用服務器資源是有限的,數據庫每秒能接受的請求次數也是有限的(或者文件的讀寫也是有限的),如何能夠有效利用有限的資源來提供儘可能大的吞吐量?

一個有效的辦法就是引入緩存,打破標準流程,每個環節中請求可以從緩存中直接獲取目標數據並返回,從而減少計算量,有效提升響應速度,讓有限的資源服務更多的用戶。

 

 

緩存介質

從硬件介質上來看,無非就是內存和硬盤兩種,但從技術上,可以分成內存、硬盤文件、數據庫。

  • 內存:將緩存存儲於內存中是最快的選擇,無需額外的I/O開銷,但是內存的缺點是沒有持久化落地物理磁盤,一旦應用異常break down而重新啓動,數據很難或者無法復原。
  • 硬盤:一般來說,很多緩存框架會結合使用內存和硬盤,在內存分配空間滿了或是在異常的情況下,可以被動或主動的將內存空間數據持久化到硬盤中,達到釋放空間或備份數據的目的。
  • 數據庫:增加緩存的策略的目的之一就是爲了減少數據庫的I/O壓力。現在使用數據庫做緩存介質是不是又回到了老問題上了?其實,數據庫也有很多種類型,像那些不支持SQL,只是簡單的key-value存儲結構的特殊數據庫(如BerkeleyDB和Redis),響應速度和吞吐量都遠遠高於我們常用的關係型數據庫等。

緩存分類和應用場景

緩存有各類特徵,而且有不同介質的區別,那麼實際工程中我們怎麼去對緩存分類呢?在目前的應用服務框架中,比較常見的,時根據緩存雨應用的藕合度,分爲local cache(本地緩存)和remote cache(分佈式緩存):

  • 本地緩存:編程直接實現緩存,指的是在應用中的緩存組件,其最大的優點是應用和cache是在同一個進程內部,請求緩存非常快速,沒有過多的網絡開銷等,在單應用不需要集羣支持或者集羣情況下各節點無需互相通知的場景下使用本地緩存較合適;同時,它的缺點也是應爲緩存跟應用程序耦合,多個應用程序無法直接的共享緩存,各應用或集羣的各節點都需要維護自己的單獨緩存,對內存是一種浪費。

  • 分佈式緩存:常用的是redis 和memcached 。指的是與應用分離的緩存組件或服務,其最大的優點是自身就是一個獨立的應用,與本地應用隔離,多個應用可直接的共享緩存。

目前各種類型的緩存都活躍在成千上萬的應用服務中,還沒有一種緩存方案可以解決一切的業務場景或數據類型,我們需要根據自身的特殊場景和背景,選擇最適合的緩存方案。

好的程序員能根據數據類型、業務場景來準確判斷使用何種類型的緩存,如何使用這種緩存,以最小的成本最快的效率達到最優的目的。

 

 

redis 和memcached 有什麼區別?

  • mc 可以緩存圖片和視頻,redis 支持除更多的數據結構。redis 典型的應用場景是用戶訂單列表,用戶消息,帖子評論等。
  • redis 可以使用虛擬內存,redis 可持久化和aof 災難恢復,支持主從數據備份。如果redis 掛了,內存能夠快速恢復熱數據,不會將壓力瞬間壓在數據庫上,沒有cache 預熱的過程。對於只讀和數據一致性要求不高的場景可以採用持久化存儲。
  • redis 可以做消息隊列。redis 支持集羣,可以實現主動複製,讀寫分離,mc 如果想實現高可用,需要進行二次開發。
  • mc 存儲的vlaue 最大爲1M。
選擇mc 的場景:
  • 1 純kv, 數據量非常大的業務。
    原因是:
  • 1 mc 的內存分配採用的是預分配內存池的管理方式,能夠省去內存分配的時間。redis 是臨時申請空間,可以導致碎片化
  • 2 虛擬內存使用,mc 將所有的數據存儲在物理內存裏,redis 有自己的vm 機制,理論上能夠存儲比物理內在更多的數據,當數據超量時,引發swap, 把冷數據刷新到磁盤上。從這點上看,數據量大時,mc 更快
  • 3 網絡模型。mc 使用非阻塞的io 複用模型,redis 也是使用非阻塞的io 複用模型,但是redis 還提供了一些非kv 存儲之外的排序,聚合功能,複雜的cpu 計算,會阻塞整個io 調度,從這點上由於redis 提供的功能較多,mc 更快一些。
  • 4 線程模型,mc 使用多線程,主線程監聽,worker 子線程接受請求,執行讀寫,這個過程可能存在鎖衝突。redis 使用單線程,雖然無鎖衝突,但是難以利用多核的特性提升吞吐量。
選擇redis 場景:
  • 1 存儲方式上:mc 會把數據全部存儲在內存中,斷電後會掛掉,數據不能超過內存的大小。redis 有部分數據存在硬盤上,這樣能保證數據持久性。
  • 2 數據支持類型上:redis 支持更豐富的數據類型
  • 3 使用底層模型不同:底層實現方式以及客戶端之間通信的應用協議不同。redis 直接構建了vm 機制,因爲一般的系統調用系統函數的話,會浪費一定的時間去移動和請求。
  • 4 value 大小。redis 可以達到1 G,而mc 只有1 M。

mc 多線程模型引入了緩存一致性和鎖,加鎖帶來了性能損耗。

爲什麼 redis 單線程還如此快?

因爲底層有高效數據存儲結構。整個redis 存儲結構是全局哈希表,哈希運算非常快,時間複雜度爲常量。

 

memcached緩存

在服務端,memcached集羣環境實際就是一個個memcached服務器的堆積;cache的分佈式主要是在客戶端實現,通過客戶端的路由處理來達到分佈式解決方案的目的。

客戶端做路由的原理是,應用服務器在每次存取某key的value時,通過某種算法把key映射到某臺memcached服務器nodeA上,因此這個key所有操作都在nodeA上。

結構圖如圖下所示。

圖6 memcached客戶端路由圖

圖 memcached客戶端路由圖

 

圖7 memcached一致性hash示例圖

圖 memcached一致性hash示例圖

 

memcached客戶端採用一致性hash算法作爲路由策略,如上圖,相對於一般hash(如簡單取模)的算法,一致性hash算法除了計算key的hash值外,還會計算每個server對應的hash值,然後將這些hash值映射到一個有限的值域上(比如0~2^32)。

通過尋找hash值大於hash(key)的最小server作爲存儲該key數據的目標server。如果找不到,則直接把具有最小hash值的server作爲目標server。同時,一定程度上,解決了擴容問題,增加或刪除單個節點,對於整個集羣來說,不會有大的影響。最近版本,增加了虛擬節點的設計,進一步提升了可用性。

memcached是一個高效的分佈式內存cache,memcached的內存管理機制,僅支持基礎的key-value鍵值對類型數據存儲。所以在memcached內存結構中有兩個非常重要的概念:slab和chunk。如下圖所示。

圖8 memcached內存結構圖

圖 memcached內存結構圖

 

slab是一個內存塊,它是memcached一次申請內存的最小單位。在啓動memcached的時候一般會使用參數-m指定其可用內存,但是並不是在啓動的那一刻所有的內存就全部分配出去了,只有在需要的時候纔會去申請,而且每次申請一定是一個slab。Slab的大小固定爲1M(1048576 Byte),一個slab由若干個大小相等的chunk組成。每個chunk中都保存了一個item結構體、一對key和value。

雖然在同一個slab中chunk的大小相等的,但是在不同的slab中chunk的大小並不一定相等,在memcached中按照chunk的大小不同,可以把slab分爲很多種類(class),默認情況下memcached把slab分爲40類(class1~class40),在class 1中,chunk的大小爲80字節,由於一個slab的大小是固定的1048576字節(1M),因此在class1中最多可以有13107個chunk(也就是這個slab能存最多13107個小於80字節的key-value數據)。

memcached內存管理採取預分配、分組管理的方式,分組管理就是我們上面提到的slab class,按照chunk的大小slab被分爲很多種類。

內存預分配過程是怎樣的呢?

向memcached添加一個item時候,memcached首先會根據item的大小,來選擇最合適的slab class:例如item的大小爲190字節,默認情況下class 4的chunk大小爲160字節顯然不合適,class 5的chunk大小爲200字節,大於190字節,因此該item將放在class 5中(顯然這裏會有10字節的浪費是不可避免的),計算好所要放入的chunk之後,memcached會去檢查該類大小的chunk還有沒有空閒的,如果沒有,將會申請1M(1個slab)的空間並劃分爲該種類chunk。例如我們第一次向memcached中放入一個190字節的item時,memcached會產生一個slab class 2(也叫一個page),並會用去一個chunk,剩餘5241個chunk供下次有適合大小item時使用,當我們用完這所有的5242個chunk之後,下次再有一個在160~200字節之間的item添加進來時,memcached會再次產生一個class 5的slab(這樣就存在了2個pages)。

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

圖9 Redis數據模型圖

圖Redis數據模型圖

 

Redis內部使用一個redisObject對象來標識所有的key和value數據,redisObject最主要的信息如圖所示:type代表一個value對象具體是何種數據類型,encoding是不同數據類型在Redis內部的存儲方式,比如——type=string代表value存儲的是一個普通字符串,那麼對應的encoding可以是raw或是int,如果是int則代表世界Redis內部是按數值類型存儲和表示這個字符串。

圖左邊的raw列爲對象的編碼方式:字符串可以被編碼爲raw(一般字符串)或Rint(爲了節約內存,Redis會將字符串表示的64位有符號整數編碼爲整數來進行儲存);列表可以被編碼爲ziplist或linkedlist,ziplist是爲節約大小較小的列表空間而作的特殊表示;集合可以被編碼爲intset或者hashtable,intset是隻儲存數字的小集合的特殊表示;hash表可以編碼爲zipmap或者hashtable,zipmap是小hash表的特殊表示;有序集合可以被編碼爲ziplist或者skiplist格式,ziplist用於表示小的有序集合,而skiplist則用於表示任何大小的有序集合。

從網絡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的持久化數據。
  3. 虛擬內存(VM),主要問題是代碼複雜,重啓慢,複製慢等等,目前已經被作者放棄。
  4. Diskstore 方式,也就是傳統的 B-tree 的方式。

 

在設計思路上,前兩種是基於全部數據都在內存中,即小數據量下提供磁盤落地功能,而後兩種持久化方式仍然是在實驗階段,並且 vm 方式基本已經被作者放棄,所以實際能在生產環境用的只有前兩種,換句話說 Redis 目前還只能作爲小數據量存儲(全部數據能夠加載在內存中),海量數據存儲方面並不是 Redis 所擅長的領域。

 

Reids持久化的目的主要還是容災,有時候還需要定時把RDB或者AOF文件通過shell腳本傳遞到其他服務器上。

AOF三種策略:

always:每條Redis寫命令都同步寫入硬盤

everysec:每秒執行一次同步,將多個命令寫入硬盤

no:由操作系統決定何時同步

 

AOF 重寫:

隨着Redis的運行,被執行的寫命令不斷同步到AOF文件中,AOF文件的體積越來越大,極端情況將會佔滿所有的硬盤空間。如果AOF文件體積過大,還原的過程也會相當耗時。爲了解決AOF文件不斷膨脹的問題,需要redis基於當前快照來重寫AOF。

比如 reids在使用過程中通過LUR來淘汰一部分數據,使得redis的內存大小一直在100g,而AOF文件大小可能到達1000g,當達到某個臨界值的時候,我們會移除1000的rof文件,基於現在的100g內存快照重構一個100g的aof文件。

 

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更傾向於在服務端構建分佈式存儲,如圖

 

圖11 Redis分佈式集羣圖2

圖 Redis分佈式集羣圖

 

Redis Cluster是一個實現了分佈式且允許單點故障的Redis高級版本,它沒有中心節點,具有線性可伸縮的功能。如上圖,其中節點與節點之間通過二進制協議進行通信,節點與客戶端之間通過ascii協議進行通信。在數據的放置策略上,Redis Cluster將整個key的數值域分成4096個hash槽,每個節點上可以存儲一個或多個hash槽,也就是說當前Redis Cluster支持的最大節點數就是4096。Redis Cluster使用的分佈式算法也很簡單:crc16( key ) % HASH_SLOTS_NUMBER。整體設計可總結爲:

  • 數據hash分佈在不同的Redis節點實例上;
  • M/S的切換採用Sentinel;
  • 寫:只會寫master Instance,從sentinel獲取當前的master Instance;
  • 讀:從Redis Node中基於權重選取一個Redis Instance讀取,失敗/超時則輪詢其他Instance;Redis本身就很好的支持讀寫分離,在單進程的I/O場景下,可以有效的避免主庫的阻塞風險;
  • 通過RPC服務訪問,RPC server端封裝了Redis客戶端,客戶端基於Jedis開發。

可以看到,通過集羣+主從結合的設計,Redis在擴展和穩定高可用性能方面都是比較成熟的。

但是,在數據一致性問題上,Redis沒有提供CAS操作命令來保障高併發場景下的數據一致性問題,不過它卻提供了事務的功能,Redis的Transactions提供的並不是嚴格的ACID的事務(比如一串用EXEC提交執行的命令,在執行中服務器宕機,那麼會有一部分命令執行了,剩下的沒執行)。但是這個Transactions還是提供了基本的命令打包執行的功能(在服務器不出問題的情況下,可以保證一連串的命令是順序在一起執行的,中間有會有其它客戶端命令插進來執行)。

Redis還提供了一個Watch功能,你可以對一個key進行Watch,然後再執行Transactions,在這過程中,如果這個Watched的值進行了修改,那麼這個Transactions會發現並拒絕執行。在失效策略上,Redis支持多大6種的數據淘汰策略:

  1. volatile-lru:從已設置過期時間的數據集(server.db[i].expires)中挑選最近最少使用的數據淘汰;
  2. volatile-ttl:從已設置過期時間的數據集(server.db[i].expires)中挑選將要過期的數據淘汰;
  3. volatile-random:從已設置過期時間的數據集(server.db[i].expires)中任意選擇數據淘汰 ;
  4. allkeys-lru:從數據集(server.db[i].dict)中挑選最近最少使用的數據淘汰;
  5. allkeys-random:從數據集(server.db[i].dict)中任意選擇數據淘汰;
  6. no-enviction(驅逐):禁止驅逐數據。

 

Redis 複製流程概述

Redis 的複製功能是完全建立在之前我們討論過的基於內存快照的持久化策略基礎上的,也就是說無論你的持久化策略選擇的是什麼,只要用到了 Redis 的複製功能,就一定會有內存快照發生,那麼首先要注意你的系統內存容量規劃,原因可以參考我上一篇文章中提到的 Redis 磁盤 IO 問題。

Redis 複製流程在 Slave 和 Master 端各自是一套狀態機流轉,涉及的狀態信息是:

Slave 端:

REDIS_REPL_NONE
REDIS_REPL_CONNECT
REDIS_REPL_CONNECTED 

Master 端:

REDIS_REPL_WAIT_BGSAVE_START
REDIS_REPL_WAIT_BGSAVE_END
REDIS_REPL_SEND_BULK
REDIS_REPL_ONLINE

 

 
整個狀態機流程過程如下:
  1. Slave 端在配置文件中添加了 slave of 指令,於是 Slave 啓動時讀取配置文件,初始狀態爲 REDIS_REPL_CONNECT。
  2. Slave 端在定時任務 serverCron(Redis 內部的定時器觸發事件) 中連接 Master,發送 sync 命令,然後阻塞等待 master 發送回其內存快照文件 (最新版的 Redis 已經不需要讓 Slave 阻塞)。
  3. Master 端收到 sync 命令簡單判斷是否有正在進行的內存快照子進程,沒有則立即開始內存快照,有則等待其結束,當快照完成後會將該文件發送給 Slave 端。
  4. Slave 端接收 Master 發來的內存快照文件,保存到本地,待接收完成後,清空內存表,重新讀取 Master 發來的內存快照文件,重建整個內存表數據結構,並最終狀態置位爲 REDIS_REPL_CONNECTED 狀態,Slave 狀態機流轉完成。
  5. Master 端在發送快照文件過程中,接收的任何會改變數據集的命令都會暫時先保存在 Slave 網絡連接的發送緩存隊列裏(list 數據結構),待快照完成後,依次發給 Slave, 之後收到的命令相同處理,並將狀態置位爲 REDIS_REPL_ONLINE。

整個複製過程完成,流程如下圖所示:

Redis 複製機制的缺陷

從上面的流程可以看出,Slave 從庫在連接 Master 主庫時,Master 會進行內存快照,然後把整個快照文件發給 Slave,也就是沒有象 MySQL 那樣有複製位置的概念,即無增量複製,這會給整個集羣搭建帶來非常多的問題。

比如一臺線上正在運行的 Master 主庫配置了一臺從庫進行簡單讀寫分離,這時 Slave 由於網絡或者其它原因與 Master 斷開了連接,那麼當 Slave 進行重新連接時,需要重新獲取整個 Master 的內存快照,Slave 所有數據跟着全部清除,然後重新建立整個內存表,一方面 Slave 恢復的時間會非常慢,另一方面也會給主庫帶來壓力。

所以基於上述原因,如果你的 Redis 集羣需要主從複製,那麼最好事先配置好所有的從庫,避免中途再去增加從庫。

Cache 還是 Storage

瞭解Redis 的複製與持久化功能後, 我們知道,實際上 Redis 目前發佈的版本還都是一個單機版的思路,主要的問題集中在,持久化方式不夠成熟,複製機制存在比較大的缺陷,這時我們又開始重新思考 Redis 的定位:Cache 還是 Storage?

如果作爲 Cache 的話,似乎除了有些非常特殊的業務場景,必須要使用 Redis 的某種數據結構之外,我們使用 Memcached 可能更合適,畢竟 Memcached 無論客戶端包和服務器本身更久經考驗。

如果是作爲存儲 Storage 的話,我們面臨的最大的問題是無論是持久化還是複製都沒有辦法解決 Redis 單點問題,即一臺 Redis 掛掉了,沒有太好的辦法能夠快速的恢復,通常幾十 G 的持久化數據,Redis 重啓加載需要幾個小時的時間,而複製又有缺陷,如何解決呢?

Redis 可擴展集羣搭建

1. 主動複製避開 Redis 複製缺陷。

既然 Redis 的複製功能有缺陷,那麼我們不妨放棄 Redis 本身提供的複製功能,我們可以採用主動複製的方式來搭建我們的集羣環境。

所謂主動複製是指由業務端或者通過代理中間件對 Redis 存儲的數據進行雙寫或多寫,通過數據的多份存儲來達到與複製相同的目的,主動複製不僅限於用在 Redis 集羣上,目前很多公司採用主動複製的技術來解決 MySQL 主從之間複製的延遲問題,比如 Twitter 還專門開發了用於複製和分區的中間件 gizzard( https://github.com/twitter/gizzard ) 。

主動複製雖然解決了被動複製的延遲問題,但也帶來了新的問題,就是數據的一致性問題,數據寫 2 次或多次,如何保證多份數據的一致性呢?

如果你的應用對數據一致性要求不高,允許最終一致性的話,那麼通常簡單的解決方案是可以通過時間戳或者 vector clock 等方式,讓客戶端同時取到多份數據並進行校驗,如果你的應用對數據一致性要求非常高,那麼就需要引入一些複雜的一致性算法比如 Paxos 來保證數據的一致性,但是寫入性能也會相應下降很多。

通過主動複製,數據多份存儲我們也就不再擔心 Redis 單點故障的問題了,如果一組 Redis 集羣掛掉,我們可以讓業務快速切換到另一組 Redis 上,降低業務風險。

2. 通過 presharding 進行 Redis 在線擴容。

通過主動複製我們解決了 Redis 單點故障問題,那麼還有一個重要的問題需要解決:容量規劃與在線擴容問題

我們前面分析過 Redis 的適用場景是全部數據存儲在內存中,而內存容量有限,那麼首先需要根據業務數據量進行初步的容量規劃,比如你的業務數據需要 100G 存儲空間,假設服務器內存是 48G,那麼根據上一篇我們討論的 Redis 磁盤 IO 的問題,我們大約需要 3~4 臺服務器來存儲。這個實際是對現有業務情況所做的一個容量規劃,假如業務增長很快,很快就會發現當前的容量已經不夠了,Redis 裏面存儲的數據很快就會超過物理內存大小,那麼如何進行 Redis 的在線擴容呢?

Redis 的作者提出了一種叫做 presharding 的方案來解決動態擴容和數據分區的問題,實際就是在同一臺機器上部署多個 Redis 實例的方式,當容量不夠時將多個實例拆分到不同的機器上,這樣實際就達到了擴容的效果。

拆分過程如下:

  1. 在新機器上啓動好對應端口的 Redis 實例。
  2. 配置新端口爲待遷移端口的從庫。
  3. 待複製完成,與主庫完成同步後,切換所有客戶端配置到新的從庫的端口。
  4. 配置從庫爲新的主庫。
  5. 移除老的端口實例。
  6. 重複上述過程遷移好所有的端口到指定服務器上。

以上拆分流程是 Redis 作者提出的一個平滑遷移的過程,不過該拆分方法還是很依賴 Redis 本身的複製功能的,如果主庫快照數據文件過大,這個複製的過程也會很久,同時會給主庫帶來壓力。所以做這個拆分的過程最好選擇爲業務訪問低峯時段進行。

Redis 複製的改進思路

我們線上的系統使用了我們自己改進版的 Redis, 主要解決了 Redis 沒有增量複製的缺陷,能夠完成類似 Mysql Binlog 那樣可以通過從庫請求日誌位置進行增量複製。

我們的持久化方案是首先寫 Redis 的 AOF 文件,並對這個 AOF 文件按文件大小進行自動分割滾動,同時關閉 Redis 的 Rewrite 命令,然後會在業務低峯時間進行內存快照存儲,並把當前的 AOF 文件位置一起寫入到快照文件中,這樣我們可以使快照文件與 AOF 文件的位置保持一致性,這樣我們得到了系統某一時刻的內存快照,並且同時也能知道這一時刻對應的 AOF 文件的位置,那麼當從庫發送同步命令時,我們首先會把快照文件發送給從庫,然後從庫會取出該快照文件中存儲的 AOF 文件位置,並將該位置發給主庫,主庫會隨後發送該位置之後的所有命令,以後的複製就都是這個位置之後的增量信息了。

Redis 與 MySQL 的結合

目前大部分互聯網公司使用 MySQL 作爲數據的主要持久化存儲,那麼如何讓 Redis 與 MySQL 很好的結合在一起呢?我們主要使用了一種基於 MySQL 作爲主庫,Redis 作爲高速數據查詢從庫的異構讀寫分離的方案。

爲此我們專門開發了自己的 MySQL 複製工具,MySQL-Redis 異構讀寫分離,可以方便的實時同步 MySQL 中的數據到 Redis 上。

 

總結:

  1. Redis 的複製功能沒有增量複製,每次重連都會把主庫整個內存快照發給從庫,所以需要避免向在線服務的壓力較大的主庫上增加從庫。
  2. Redis 的複製由於會使用快照持久化方式,所以如果你的 Redis 持久化方式選擇的是日誌追加方式 (aof), 那麼系統有可能在同一時刻既做 aof 日誌文件的同步刷寫磁盤,又做快照寫磁盤操作,這個時候 Redis 的響應能力會受到影響。所以如果選用 aof 持久化,則加從庫需要更加謹慎。
  3. 可以使用主動複製和 presharding 方法進行 Redis 集羣搭建與在線擴容。

 

Redis的豐富的數據結構特性及一般使用場景小結:

  • 在主頁中顯示最新的項目列表:Redis使用的是常駐內存的緩存,速度非常快。LPUSH用來插入一個內容ID,作爲關鍵字存儲在列表頭部。LTRIM用來限制列表中的項目數最多爲5000。如果用戶需要的檢索的數據量超越這個緩存容量,這時才需要把請求發送到數據庫。
  • 刪除和過濾:如果一篇文章被刪除,可以使用LREM從緩存中徹底清除掉。
  • 排行榜及相關問題:排行榜(leader board)按照得分進行排序。ZADD命令可以直接實現這個功能,而ZREVRANGE命令可以用來按照得分來獲取前100名的用戶,ZRANK可以用來獲取用戶排名,非常直接而且操作容易。
  • 按照用戶投票和時間排序:排行榜,得分會隨着時間變化。LPUSH和LTRIM命令結合運用,把文章添加到一個列表中。一項後臺任務用來獲取列表,並重新計算列表的排序,ZADD命令用來按照新的順序填充生成列表。列表可以實現非常快速的檢索,即使是負載很重的站點。
  • 過期項目處理:使用Unix時間作爲關鍵字,用來保持列表能夠按時間排序。對current_time和time_to_live進行檢索,完成查找過期項目的艱鉅任務。另一項後臺任務使用ZRANGE…WITHSCORES進行查詢,刪除過期的條目。
  • 計數:進行各種數據統計的用途是非常廣泛的,比如想知道什麼時候封鎖一個IP地址。INCRBY命令讓這些變得很容易,通過原子遞增保持計數;GETSET用來重置計數器;過期屬性用來確認一個關鍵字什麼時候應該刪除。
  • 特定時間內的特定項目:這是特定訪問者的問題,可以通過給每次頁面瀏覽使用SADD命令來解決。SADD不會將已經存在的成員添加到一個集合。
  • Pub/Sub:在更新中保持用戶對數據的映射是系統中的一個普遍任務。Redis的pub/sub功能使用了SUBSCRIBE、UNSUBSCRIBE和PUBLISH命令,讓這個變得更加容易。
  • 隊列:在當前的編程中隊列隨處可見。除了push和pop類型的命令之外,Redis還有阻塞隊列的命令,能夠讓一個程序在執行時被另一個程序添加到隊列。

 

redis 使用總結:

  1. 根據業務需要選擇合適的數據類型,併爲不同的應用場景設置相應的緊湊存儲參數。
  2. 當業務場景不需要數據持久化時,關閉所有的持久化方式可以獲得最佳的性能以及最大的內存使用量。
  3. 如果需要使用持久化,根據是否可以容忍重啓丟失部分數據在快照方式與語句追加方式之間選擇其一,不要使用虛擬內存以及 diskstore 方式。
  4. 不要讓你的 Redis 所在機器物理內存使用超過實際內存總量的 3/5。

 

redis 常見性能問題和解決方案:

  • 1 master 最好不要做持久化工作,如RDB 內存快照和AOF 日誌文件
  • 2 如果數據比較重要,某個slave 開啓AOF 備份,策略設置成每秒同步一次
  • 3 爲了主從複製的速度和連接的穩定性,master 和slave 最好在一個局域網內
  • 4 儘量避免在壓力大得主庫上增加從庫
  • 主從複製不要採用網狀結構,儘量是線性結構。

redis set key value, 其中key 類型都是string 類型。五種基本類型都是對應的value。

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章