先看一下Redis是一個什么東西
官方簡介解釋到:Redis 是一個基于 BSD 開源的專案,是一個把結構化的資料放在記憶體中的一個存盤系統,你可以把它作為資料庫,快取和訊息中間件來使用。
同時支持strings,lists,hashes,sets,sorted sets,bitmaps,hyperloglogs 和 geospatial indexes 等資料型別。
它還內建了復制,lua 腳本,LRU,事務等功能,通過 redis sentinel實作高可用,通過 redis cluster 實作了自動分片。
以及事務,發布/訂閱,自動故障轉移等等。
綜上所述,redis 提供了豐富的功能,初次見到可能會感覺眼花繚亂,
這些功能都是干嘛用的?
都解決了什么問題?
什么情況下才會用到相應的功能?
那么下面從零開始,一步一步的演進來粗略的解釋下。
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 30000
if redis.call("get",KEYS[1]) == ARGV[1] then
return redis.call("del",KEYS[1])
else
return 0
end
總結
本篇著重從抽象層面來解釋下 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
轉載請註明出處,本文鏈接:https://www.uj5u.com/caozuo/123599.html
標籤:實用資料發布區
