Redis 到底能解決哪些問題

閱讀本文大概須要 7 分鐘。

做者:blackheartgit

www.cnblogs.com/linianhui程序員

先看一下Redis是一個什麼東西

官方簡介解釋到:Redis 是一個基於 BSD 開源的項目,是一個把結構化的數據放在內存中的一個存儲系統,你能夠把它做爲數據庫,緩存和消息中間件來使用。github


同時支持strings,lists,hashes,sets,sorted sets,bitmaps,hyperloglogs 和 geospatial indexes 等數據類型。redis


它還內建了複製,lua 腳本,LRU,事務等功能,經過 redis sentinel實現高可用,經過 redis cluster 實現了自動分片。算法


以及事務,發佈/訂閱,自動故障轉移等等。數據庫

綜上所述,redis 提供了豐富的功能,初次見到可能會感受眼花繚亂,後端

這些功能都是幹嗎用的?api

都解決了什麼問題?緩存

什麼狀況下才會用到相應的功能?安全

那麼下面從零開始,一步一步的演進來粗略的解釋下。

1 從零開始

最初的需求很是簡單,咱們有一個提供熱點新聞列表的API:http://api.xxx.com/hot-news,API 的消費者抱怨說每次請求都要 2 秒左右才能返回結果。

隨後咱們就着手於如何提高一下 API 消費者感知的性能,很快最簡單粗暴的第一個方案就出來了:

爲 API 的響應加上基於 HTTP 的緩存控制 cache-control:max-age=600 ,即讓消費者能夠緩存這個響應十分鐘

若是api消費者若是有效的利用了響應中的緩存控制信息,則能夠有效的改善其感知的性能(10 分鐘之內)。

可是還有 2 個弊端

第一個是在緩存生效的 10 分鐘內,API 消費者可能會獲得舊的數據;

第二個是若是 API 的客戶端無視緩存直接訪問 API 依然是須要 2 秒,治標不治本吶。

2 基於本機內存的緩存

爲了解決調用 API 依然須要 2 秒的問題,通過排查,其主要緣由在於使用 SQL 獲取熱點新聞的過程當中消耗了將近 2 秒的時間

因而乎,咱們又想到了一個簡單粗暴的解決方案,即把 SQL 查詢的結果直接緩存在當前 API 服務器的內存中(設置緩存有效時間爲 1 分鐘)。

後續 1 分鐘內的請求直接讀緩存,再也不花費 2 秒去執行 SQL 了。

假如這個 API 每秒接收到的請求時 100 個,那麼一分鐘就是 6000 個,也就是隻有前 2 秒擁擠過來的請求會耗時 2 秒,後續的 58 秒中的全部請求均可以作到即便響應,而無需再等 2 秒的時間。

其餘 API 的小夥伴發現這是個好辦法,因而很快咱們就發現 API 服務器的內存要爆滿了

3 服務端的 Redis

在 API 服務器的內存都被緩存塞滿的時候,咱們發現不得不另想解決方案了。

最直接的想法就是咱們把這些緩存都丟到一個專門的服務器上吧,把它的內存配置的大大的。

而後咱們就盯上了 redis。

至於如何配置部署 redis 這裏不解釋了,redis 官方有詳細的介紹。

隨後咱們就用上了一臺單獨的服務器做爲 redis 的服務器,API 服務器的內存壓力得以解決。

3.1 持久化(Persistence)

單臺的 redis 服務器一個月總有那麼幾天心情很差,心情很差就罷工了,致使全部的緩存都丟失了(redis 的數據是存儲在內存的嘛)。

雖然能夠把 redis 服務器從新上線,可是因爲內存的數據丟失,形成了緩存雪崩,API 服務器和數據庫的壓力仍是一會兒就上來了。

因此這個時候 redis 的持久化功能就派上用場了,能夠緩解一下緩存雪崩帶來的影響。

Redis 的持久化指的是 redis 會把內存的中的數據寫入到硬盤中,在 redis 從新啓動的時候加載這些數據,從而最大限度的下降緩存丟失帶來的影響。

3.2 哨兵(Sentinel)和複製(Replication)

Redis 服務器毫無徵兆的罷工是個麻煩事。

那麼怎麼辦?

答曰:備份一臺,你掛了它上。

那麼如何得知某一臺 redis 服務器掛了,如何切換,如何保證備份的機器是原始服務器的完整備份呢?

這時候就須要 Sentinel 和 Replication 出場了

Sentinel 能夠管理多個 redis 服務器,它提供了監控,提醒以及自動的故障轉移的功能;Replication 則是負責讓一個 redis 服務器能夠配備多個備份的服務器。

Redis 也是利用這兩個功能來保證 redis 的高可用的。

此外,Sentinel 功能則是對 redis 的發佈和訂閱功能的一個利用。

3.3 集羣(Cluster)

單臺服務器資源的老是有上限的,CPU 資源和 IO 資源咱們能夠經過主從複製,進行讀寫分離,把一部分 CPU 和 IO 的壓力轉移到從服務器上

可是內存資源怎麼辦,主從模式作到的只是相同數據的備份,並不能橫向擴充內存;單臺機器的內存也只能進行加大處理,可是總有上限的。

因此咱們就須要一種解決方案,可讓咱們橫向擴展

最終的目的既是把每臺服務器只負責其中的一部分,讓這些全部的服務器構成一個總體,對外界的消費者而言,這一組分佈式的服務器就像是一個集中式的服務器同樣。

在 redis 官方的分佈式方案出來以前,有 twemproxy 和 codis 兩種方案,這兩個方案整體上來講都是依賴 proxy 來進行分佈式的,也就是說 redis 自己並不關心分佈式的事情,而是交由 twemproxy 和 codis 來負責。

而 redis 官方給出的 cluster 方案則是把分佈式的這部分事情作到了每個 redis 服務器中,使其再也不須要其餘的組件就能夠獨立的完成分佈式的要求。

咱們這裏不關心這些方案的優略,咱們關注一下這裏的分佈式究竟是要處理那些事情?

也就是 twemproxy 和 codis 獨立處理的處理分佈式的這部分邏輯和 cluster 集成到 redis 服務的這部分邏輯到底在解決什麼問題?

如咱們前面所說的,一個分佈式的服務在外界看來就像是一個集中式的服務同樣

那麼要作到這一點就面臨着有一個問題須要解決:

既是增長或減小分佈式服務中的服務器的數量,對消費這個服務的客戶端而言應該是無感的;

那麼也就意味着客戶端不能穿透分佈式服務,把本身綁死到某一個臺的服務器上去,由於一旦如此,你就再也沒法新增服務器,也沒法進行故障替換。

解決這個問題有兩個路子

第一個路子最直接,那就是我加一箇中間層來隔離這種具體的依賴

即 twemproxy 採用的方式,讓全部的客戶端只能經過它來消費 redis 服務,經過它來隔離這種依賴(可是你會發現 twermproxy 會成爲一個單點),這種狀況下每臺 redis 服務器都是獨立的,它們之間彼此不知對方的存在;

第二個路子是讓 redis 服務器知道彼此的存在,經過重定向的機制來引導客戶端來完成本身所須要的操做

好比客戶端連接到了某一個 redis 服務器,說我要執行這個操做,redis 服務器發現本身沒法完成這個操做,那麼就把能完成這個操做的服務器的信息給到客戶端,讓客戶端去請求另外的一個服務器。

這時候你就會發現每個 redis 服務器都須要保持一份完整的分佈式服務器信息的一份資料,否則它怎麼知道讓客戶端去找其餘的哪一個服務器來執行客戶端想要的操做呢。

上面這一大段解釋了這麼多,不知有沒有發現無論是第一個路子仍是第二個路子,都有一個共同的東西存在那就是分佈式服務中全部服務器以及其能提供的服務的信息

這些信息不管如何也是要存在的,區別在於

第一個路子是把這部分信息單獨來管理,用這些信息來協調後端的多個獨立的 redis 服務器

第二個路子則是讓每個 redis 服務器都持有這份信息,彼此知道對方的存在,來達成和第一個路子同樣的目的,優勢是再也不須要一個額外的組件來處理這部分事情

Redis Cluster 的具體實現細節則是採用了 Hash 槽的概念,即預先分配出來 16384 個槽:

在客戶端經過對 Key 進行 CRC16(key)% 16384 運算獲得對應的槽是哪個;

在 redis 服務端則是每一個服務器負責一部分槽,當有新的服務器加入或者移除的時候,再來遷移這些槽以及其對應的數據。

同時每一個服務器都持有完整的槽和其對應的服務器的信息,這就使得服務器端能夠進行對客戶端的請求進行重定向處理。

4 客戶端的 Redis

上面的第三小節主要介紹的是 redis 服務端的演進步驟,解釋了 redis 如何從一個單機的服務,進化爲一個高可用的、去中心化的、分佈式的存儲系統。

這一小節則是關注下客戶端能夠消費的 redis 服務。

4.1 數據類型

Redis 支持豐富的數據類型,從最基礎的 string 到複雜的經常使用到的數據結構都有支持:

  • string:最基本的數據類型,二進制安全的字符串,最大 512M。

  • list:按照添加順序保持順序的字符串列表。

  • set:無序的字符串集合,不存在重複的元素。

  • sorted set:已排序的字符串集合。

  • hash:key-value 對的一種集合。

  • bitmap:更細化的一種操做,以 bit 爲單位。

  • hyperloglog:基於機率的數據結構。

這些衆多的數據類型,主要是爲了支持各類場景的須要,固然每種類型都有不一樣的時間複雜度。

其實這些複雜的數據結構至關於以前我在《解讀REST》這個系列博客基於網絡應用的架構風格中介紹到的遠程數據訪問(Remote Data Access = RDA)的具體實現。

即經過在服務器上執行一組標準的操做命令,在服務端之間獲得想要的縮小後的結果集,從而簡化客戶端的使用,也能夠提升網絡性能。

好比若是沒有 list 這種數據結構,你就只能把 list 存成一個 string,客戶端拿到完整的 list,操做後再完整的提交給 redis,會產生很大的浪費。

4.2 事務

上述數據類型中,每個數據類型都有獨立的命令來進行操做,不少狀況下咱們須要一次執行不止一個命令,並且須要其同時成功或者失敗。

Redis 對事務的支持也是源自於這部分需求,即支持一次性按順序執行多個命令的能力,並保證其原子性。

4.3 Lua腳本

在事務的基礎上,若是咱們須要在服務端一次性的執行更復雜的操做(包含一些邏輯判斷),則 lua 就能夠排上用場了(好比在獲取某一個緩存的時候,同時延長其過時時間)。

Redis 保證 lua 腳本的原子性,必定的場景下,是能夠代替 redis 提供的事務相關的命令的。至關於基於網絡應用的架構風格中介紹到的遠程求值(Remote Evluation = REV)的具體實現。

4.4 管道

由於 redis 的客戶端和服務器的鏈接時基於 TCP 的, 默認每次鏈接都時只能執行一個命令。

管道則是容許利用一次鏈接來處理多條命令,從而能夠節省一些 TCP 鏈接的開銷。

管道和事務的差別在於管道是爲了節省通訊的開銷,可是並不會保證原子性。

4.5 分佈式鎖

官方推薦採用 Redlock 算法,即便用 string 類型,加鎖的時候給的一個具體的 key,而後設置一個隨機的值;取消鎖的時候用使用 lua 腳原本先執行獲取比較,而後再刪除 key。

具體的命令以下:

SET resource_name my_random_value NX PX 30000if redis.call("get",KEYS[1]) == ARGV[1] then    return redis.call("del",KEYS[1])else    return 0end複製代碼

總結

本篇着重從抽象層面來解釋下 redis 的各項功能以及其存在的目的,而沒有關心其具體的細節是什麼。

從而能夠聚焦於其解決的問題,依據抽象層面的概念可使得咱們在特定的場景下選擇更合適的方案,而非侷限於其技術細節。

以上均是筆者我的的一些理解,若是不當之處,歡迎指正。

參考

Redis 文檔:https://github.com/antirez/redis-doc

Redis 簡介:https://redis.io/topics/introduction

Redis 持久化(Persistence):https://redis.io/topics/persistence

Redis 發佈/訂閱(Pub/Sub):https://redis.io/topics/pubsub

Redis 哨兵(Sentinel):https://redis.io/topics/sentinel

Redis 複製(Replication):https://redis.io/topics/replication

Redis 集羣(cluster):https://redis.io/topics/cluster-tutorial

RedIs 事務(Transaction):https://redis.io/topics/transactions

Redis 數據類型(data types):https://redis.io/topics/data-types-intro

Redis 分佈式鎖:https://redis.io/topics/distlock

Redis 管道(pipelining ):https://redis.io/topics/pipelining

Redis Lua Script:https://redis.io/commands/eval


·END·

程序員的成長之路

路雖遠,行則必至

本文原發於 同名微信公衆號「程序員的成長之路」,回覆「1024」你懂得,給個讚唄。

回覆 [ 520 ] 領取程序員最佳學習方式

回覆 [ 256 ] 查看 Java 程序員成長規劃


往期精彩回顧


相關文章
相關標籤/搜索