咱們將先從Redis、Nginx+Lua等技術點出發,瞭解緩存應用的場景。經過使用緩存相關技術,解決高併發的業務場景案例,來深刻理解一套成熟的企業級緩存架構如何設計的。本文Redis部分總結於蔣德鈞老師的《Redis核心技術與實戰》。mysql
Redis是一個開源的使用ANSI C語言編寫、遵照BSD協議、支持網絡、可基於內存亦可持久化的日誌型、Key-Value數據庫,並提供多種語言的API。redis
它一般被稱爲數據結構服務器,由於值(value)能夠是 字符串(String), 哈希(Hash), 列表(list), 集合(sets) 和 有序集合(sorted sets)等類型。算法
Redis 與其餘 key - value 緩存產品有如下三個特色:sql
優點mongodb
string 是 redis 最基本的類型,你能夠理解成與 Memcached 如出一轍的類型,一個 key 對應一個 value。shell
string 類型是二進制安全的。意思是 redis 的 string 能夠包含任何數據。好比jpg圖片或者序列化的對象。數據庫
string 類型是 Redis 最基本的數據類型,string 類型的值最大能存儲 512MB。數組
redis 127.0.0.1:6379> SET runoob "laowang" OK redis 127.0.0.1:6379> GET runoob "laowang"
Redis hash 是一個鍵值(key=>value)對集合。緩存
Redis hash 是一個 string 類型的 field 和 value 的映射表,hash 特別適合用於存儲對象。安全
每一個 hash 能夠存儲 2^32 -1 鍵值對(40多億)。
redis 127.0.0.1:6379> HMSET runoob field1 "Hello" field2 "World" "OK" redis 127.0.0.1:6379> HGET runoob field1 "Hello" redis 127.0.0.1:6379> HGET runoob field2 "World"
Redis 列表是簡單的字符串列表,按照插入順序排序。你能夠添加一個元素到列表的頭部(左邊)或者尾部(右邊)。
列表最多可存儲 2^32 - 1 元素 (4294967295, 每一個列表可存儲40多億)。
redis 127.0.0.1:6379> lpush runoob redis (integer) 1 redis 127.0.0.1:6379> lpush runoob mongodb (integer) 2 redis 127.0.0.1:6379> lpush runoob rabitmq (integer) 3 redis 127.0.0.1:6379> lrange runoob 0 10 1) "rabitmq" 2) "mongodb" 3) "redis"
Redis 的 Set 是 string 類型的無序集合。
集合是經過哈希表實現的,因此添加,刪除,查找的複雜度都是 O(1)。
sadd 命令 :添加一個 string 元素到 key 對應的 set 集合中,成功返回 1,若是元素已經在集合中返回 0。
集合中最大的成員數爲 2^32 - 1(4294967295, 每一個集合可存儲40多億個成員)。
redis 127.0.0.1:6379> DEL runoob redis 127.0.0.1:6379> sadd runoob redis (integer) 1 redis 127.0.0.1:6379> sadd runoob mongodb (integer) 1 redis 127.0.0.1:6379> sadd runoob rabitmq (integer) 1 redis 127.0.0.1:6379> sadd runoob rabitmq (integer) 0 redis 127.0.0.1:6379> smembers runoob 1) "redis" 2) "rabitmq" 3) "mongodb"
Redis zset 和 set 同樣也是string類型元素的集合,且不容許重複的成員。
不一樣的是每一個元素都會關聯一個double類型的分數。redis正是經過分數來爲集合中的成員進行從小到大的排序。
zset的成員是惟一的,但分數(score)卻能夠重複。
zadd 命令 :添加元素到集合,元素在集合中存在則更新對應score
redis 127.0.0.1:6379> zadd runoob 0 redis (integer) 1 redis 127.0.0.1:6379> zadd runoob 0 mongodb (integer) 1 redis 127.0.0.1:6379> zadd runoob 0 rabitmq (integer) 1 redis 127.0.0.1:6379> zadd runoob 0 rabitmq (integer) 0 redis 127.0.0.1:6379> > ZRANGEBYSCORE runoob 0 1000 1) "mongodb" 2) "rabitmq" 3) "redis"
對這個問題的思考,將有助於咱們從總體架構上去學習Redis。
假設如今咱們已經設計好了一個KV數據庫,首先若是咱們要使用,是否是得有入口,咱們是經過動態連接庫仍是經過網絡socket對外提供訪問入口,這就涉及到了訪問模塊。Redis就是經過
經過訪問模塊訪問KV數據庫以後,咱們的數據存儲在哪裏?爲了保證訪問的高性能,咱們選在存儲在內存中,這又須要有存儲模塊。存在內存中的數據,雖然訪問速度快,但存在的的問題就是斷電後,沒法恢復數據,因此咱們還須要支持持久化操做。
有了存儲模塊,咱們還須要考慮,數據是以什麼樣的形式存儲?怎樣設計才能讓數據操做更優,這就設計到了,數據類型的支持,索引模塊。 索引的做用是讓鍵值數據庫根據 key 找到相應 value 的存儲位置,進而執行操做。
有了以上模塊的只是,咱們是否是要對數據進行操做了?好比往KV數據庫中插入或更新一條數據,刪除和查詢,這就是須要有操做模塊了。
至此咱們已經構造除了一個KV數據庫的基本框架了,帶着這些架構,咱們再深刻到每一個點中去探究,這樣就會輕鬆不少,不會迷失在末枝細節中了。
咱們都知道Redis訪問快,這是由於redis的操做都是在內存上的,內存的訪問自己就很快,另外Redis底層的數據結構也對「快」起到了相當重要的做用。
咱們日常因此所說Redis的5種數據結構:String、Hash、Set、ZSet和List指的只是鍵值對中值的數據結構,而我這裏所說的數據結構,指的是它們底層實現。
Redis的底層數據結構有:簡單動態字符串、整數數組、壓縮列表、跳錶、hash表、雙向列表6種。
簡單動態數組:就是String的底層實現
其中整數數組、hash表、雙向列表都是咱們常見的數據結構
壓縮列表和跳錶屬於特殊的數據結構
壓縮列表是Redis實現的特殊的數組:它本質就是一個數組,只不過,咱們常見的數組的每一個元素分配的空間大小是一致的,這樣就會致使有多餘的內存空間被浪費了。壓縮列表就是爲了解決這樣的問題,他的每一個元素大小是按實際大小分配的,避免了內存的浪費,同時在壓縮列表的表頭還存了關於改列表的相關屬性:用於記錄列表個數zllen,表尾偏移量zltail和列表長度zlbytes。表尾還有一個zlend標記列表的結束。
跳錶:有序鏈表查詢元素只能逐一查詢,跳錶本質上就是鏈表的基礎上加了多級索引,經過多級索引的幾個跳轉,快遞定位到元素所在位置。
不一樣數據結構的查詢時間複雜度
上面從存儲方面解釋了,redis爲何快.
逆向思惟能夠說爲何不用多線程,這個咱們得先看下多線程存在哪些問題?在正常應用操做中,使用多線程能夠大大提升處理的時間。那是否是能夠無限的加大線程數量,以獲取更快的處理速度?實際試驗後,發如今機器資源有限的狀況下,不斷增長線程處理時間,並無像咱們想象的那樣成線性增加,而是到達必定階段就趨於平衡,甚至有降低的趨勢,這是爲何呢?
其實主要有兩個方面,咱們知道線程是CPU調度的最小單元,當線程多的時候,CPU須要不停的切換線程,線程切換是須要消耗時間的,當大量線程須要來回切換,那麼CPU在這切換的損耗了不少時間。
另外當多個線程,須要對共享資源進行操做的時候,爲了保證併發安全性,須要有額外的機制保證,好比加鎖。這樣就使得當多個線程再操做共享數據時,變成了串行。
因此爲了不這些問題,Redis採用了單線程操做數據。
咱們知道Redis單線程操做的,可是隻是指的Redis對外提供鍵值對存儲服務是單線程的。Redis的其餘功能並非,好比持久化,異步刪除,集羣同步等,都是由額外的線程去執行的。
除了上面說的,Redis的大部分操做都是在內存上完成的,加上高效的數據結構,是他實現高性能的一方面。另一方面Redis採用的多路複用機制,使其在網絡IO操做中能併發處理大量的客戶端請求。
在網絡 IO 操做中,有潛在的阻塞點,分別是 accept() 和 recv()。當 Redis 監聽到一個客戶端有鏈接請求,但一直未能成功創建起鏈接時,會阻塞在 accept() 函數這裏,致使其餘客戶端沒法和 Redis 創建鏈接。相似的,當 Redis 經過 recv() 從一個客戶端讀取數據時,若是數據一直沒有到達,Redis 也會一直阻塞在 recv()。 這就致使 Redis 整個線程阻塞,沒法處理其餘客戶端請求,效率很低。不過,幸運的是,socket 網絡模型自己支持非阻塞模式。
Socket 網絡模型的非阻塞模式設置,主要體如今三個關鍵的函數調用上,若是想要使用 socket 非阻塞模式,就必需要了解這三個函數的調用返回類型和設置模式。接下來,咱們就重點學習下它們。在 socket 模型中,不一樣操做調用後會返回不一樣的套接字類型。socket() 方法會返回主動套接字,而後調用 listen() 方法,將主動套接字轉化爲監聽套接字,此時,能夠監聽來自客戶端的鏈接請求。最後,調用 accept() 方法接收到達的客戶端鏈接,並返回已鏈接套接字。
針對監聽套接字,咱們能夠設置非阻塞模式:當 Redis 調用 accept() 但一直未有鏈接請求到達時,Redis 線程能夠返回處理其餘操做,而不用一直等待。可是,你要注意的是,調用 accept() 時,已經存在監聽套接字了。
相似的,咱們也能夠針對已鏈接套接字設置非阻塞模式:Redis 調用 recv() 後,若是已鏈接套接字上一直沒有數據到達,Redis 線程一樣能夠返回處理其餘操做。咱們也須要有機制繼續監聽該已鏈接套接字,並在有數據達到時通知 Redis。這樣才能保證 Redis 線程,既不會像基本 IO 模型中一直在阻塞點等待,也不會致使 Redis 沒法處理實際到達的鏈接請求或數據。
Linux 中的 IO 多路複用機制是指一個線程處理多個 IO 流,就是咱們常常聽到的 select/epoll 機制。簡單來講,在 Redis 只運行單線程的狀況下,該機制容許內核中,同時存在多個監聽套接字和已鏈接套接字。內核會一直監聽這些套接字上的鏈接請求或數據請求。一旦有請求到達,就會交給 Redis 線程處理,這就實現了一個 Redis 線程處理多個 IO 流的效果。爲了在請求到達時能通知到 Redis 線程,select/epoll 提供了基於事件的回調機制,即針對不一樣事件的發生,調用相應的處理函數。
由於Redis是操做是基於內存的,全部一點系統宕機存在內存中的數據就會丟失,爲了實現數據的持久化,Redis中存在兩個持久化機制AOF和RBD。
AOF的原理就是,經過記錄下Redis的全部命令操做,在須要數據恢復的時候,再按照順序把全部命令執行一次,從而恢復數據。
但跟數據庫的寫前日誌不一樣的,AOF採用的寫後日志,也就是在Redis執行過操做以後,再寫入AOF日誌。之因此爲何採用寫後日志,能夠避免由於寫日誌的佔用redis調用的時間,另外爲了保證Redis的高性能,在寫aof日誌的時候,不會作校驗,若採用寫前日誌,若是命令是錯誤非法的,在恢復數據的時候就會出現異常。採用寫後日志,只有命令執行成功的纔會被保存。
AOF的執行策略有三種
all:每次寫入/刪除命令都會被寫入日誌文件中,保證了數據可靠性,可是寫入日誌,涉及到了磁盤的IO,必然會影響性能
everysec:每秒鐘執行一第二天志寫入,在一秒以內的命令操做會記錄在aof內存緩衝區,每一秒會寫回到日誌文件中,相對於每次寫入性能得以提高,可是在aof緩衝區沒有來得及回寫到日誌文件中時,系統發生宕機就會丟失這部分數據。
no:內存緩衝區的命令記錄不會不主動寫回到日誌文件中,而交給操做系統決定。這種策略性能最高,可是丟失數據的風險也最大。
可是AOF文件過大,會帶來性能問題,全部AOF重寫機制就登場了。
AOF重寫的原理是,將多個命令對同一個key的操做合併成一個,由於數據恢復時,咱們只要關心數據最後的狀態就能夠了。
須要注意的是,與AOF日誌由主線程寫回不一樣,重寫過程是由後臺子線程bgwriteaof來完成的,這個避免阻塞主線程,致使數據庫性能降低。
每次 AOF 重寫時,Redis 會先執行一個內存拷貝,用於重寫;而後,使用兩個日誌保證在重寫過程當中,新寫入的數據不會丟失。並且,由於 Redis 採用額外的線程進行數據重寫,因此,這個過程並不會阻塞主線程。
所謂內存快照,就是指內存中的數據在某一個時刻的狀態記錄。對 Redis 來講,就是把某一時刻的狀態以文件的形式寫到磁盤上。
Redis執行RDB的策略是什麼?
Redis進行快照的時候,是進行全量的快照,而且爲了避免阻塞主線程,會默認使用bgsave命令建立一個子線程,專門用於寫入RDB文件。
快照期間數據還能修改嗎?
若是不能修改,那麼在快照期間,這塊數據就會只能讀取不能修改,那麼必然影響使用。若是能夠修改,那麼Redis是如何實現的?其實Redis是藉助操做系統的寫時複製,在執行快照期間,讓修改的數據,會在內存中拷貝出一份副本,副本的數據能夠被寫入rdb文件中,而主線程仍然能夠修改原數據。
多久執行一次呢?
跟aof一樣的問題,若是快照頻率低,那麼在兩次快照期間出現宕機,就會出現數據不完整的狀況,若是快照頻率過快,那麼又會出現兩個問題,一個是不停的對磁盤寫出,增大磁盤壓力,可能上一次寫入還沒完成,新的快照又來了,形成惡性循環.另外雖然執行快照是主線程fork出來的,可是不停的fork的過程是阻塞主線程的。
那麼如何配置才合適呢?
其實咱們只須要第一次全量快照,後續只快照有數據變更的地方就能夠大大下降快照的資源損耗了,那麼如何記錄這變更的數據呢,這裏咱們能夠想到aof具備這樣的功能。Redis4.0就提使用RDB+AOF混合模式來完成Redis的持久化。簡單來講,內存快照以必定的頻率執行,在兩次快照之間,使用 AOF 日誌記錄這期間的全部命令操做。
前面咱們經過Redis的持久化機制,來保證服務器宕機以後,經過回放日誌和從新讀取RDB文件恢復數據,減小數據丟失的風險。
可是在單臺及其的狀況下,機器發生宕機,就沒法對外提供服務了。咱們所說的Redis具備高可靠性,指的一是,數據儘可能少丟失,以前持久化機制就解決了這一問題,另外一個是服務儘可能少中斷,Redis的作法是增長副本冗餘量。Redis提供的主從模式,主從庫之間採用了讀寫分離的方式。
從庫只讀取,主庫執行讀與寫,寫的數據主庫會同步給從庫。之因此只讓主庫寫,是由於,若是從庫也寫,那麼當客戶端對一個數據修改了3次,爲了保證數據的正確性,就要設法讓主從庫對於寫操做協同,這會帶來鉅額的開銷。
主從庫間如何進行第一次同步的?
當咱們啓動多個 Redis 實例的時候,它們相互之間就能夠經過 replicaof(Redis 5.0 以前使用 slaveof)命令造成主庫和從庫的關係,以後會按照三個階段完成數據的第一次同步。
主庫收到 psync 命令後,會用 FULLRESYNC 響應命令帶上兩個參數:主庫 runID 和主庫目前的複製進度 offset,返回給從庫。從庫收到響應後,會記錄下這兩個參數。
這裏有個地方須要注意,FULLRESYNC 響應表示第一次複製採用的全量複製,也就是說,主庫會把當前全部的數據都複製給從庫。
在第二階段,主庫將全部數據同步給從庫。從庫收到數據後,在本地完成數據加載。這個過程依賴於內存快照生成的 RDB 文件。
具體來講,主庫執行 bgsave 命令,生成 RDB 文件,接着將文件發給從庫。從庫接收到 RDB 文件後,會先清空當前數據庫,而後加載 RDB 文件。這是由於從庫在經過 replicaof 命令開始和主庫同步前,可能保存了其餘數據。爲了不以前數據的影響,從庫須要先把當前數據庫清空。
在主庫將數據同步給從庫的過程當中,主庫不會被阻塞,仍然能夠正常接收請求。不然,Redis 的服務就被中斷了。可是,這些請求中的寫操做並無記錄到剛剛生成的 RDB 文件中。爲了保證主從庫的數據一致性,主庫會在內存中用專門的 replication buffer,記錄 RDB 文件生成後收到的全部寫操做。
最後,也就是第三個階段,主庫會把第二階段執行過程當中新收到的寫命令,再發送給從庫。具體的操做是,當主庫完成 RDB 文件發送後,就會把此時 replication buffer 中的修改操做發給從庫,從庫再從新執行這些操做。這樣一來,主從庫就實現同步了。
Redis在有了主從集羣后,若是從庫掛了,Redis對外提供服務不受影響,主庫和其餘從庫,依然能夠提供讀寫服務,可是當主庫掛了以後,由於是讀寫分離的,若是此時有寫的請求,那麼就沒法處理了。Redis是若是解決這樣的問題的呢,這就要引入哨兵機制了。
當主庫掛了,咱們須要從從庫中選出一個當作主庫,這樣就能夠正常對外提供服務了。哨兵的本質就是一個Redis示例,只不過它是運行在特殊模式下的Redis進程。它主要有三個做用:監控、選舉、通知。
哨兵在監控到主庫下線的時候,會從從庫中經過必定的規則,選舉出適合的從庫當主庫,並通知其餘從庫變動主庫的信息,讓他們執行replicaof命令,和新主庫創建鏈接,並進行數據複製。那麼具體每一步都是怎麼作的呢?
監控:哨兵會週期性向主從庫發送PING命令,檢測主庫是否正常運行,若是主從庫沒有在規定的時間內迴應哨兵的PING命令,則會被斷定爲「下線狀態」,若是是主庫下線,則開始自動切換主庫的流程。可是通常若是隻有一個哨兵,那麼它的判斷可能不具備可靠性,因此通常哨兵都是採用集羣模式部署,稱爲哨兵集羣。單多個哨兵均判斷該主庫下線了,那麼可能他就真的下線了,這是一個少數服從多數的規則。
選舉: 哨兵選擇新主庫的過程稱爲「篩選 + 打分」。簡單來講,咱們在多個從庫中,先按照必定的篩選條件,把不符合條件的從庫去掉。而後,咱們再按照必定的規則,給剩下的從庫逐個打分,將得分最高的從庫選爲新主庫,以下圖所示:
一、排除那些已經下線的從庫,以及鏈接不穩定的從庫。鏈接不穩定是經過配置項down-after-milliseconds,當主從鏈接超時達到必定閾值,就會被記錄下來,好比設置的10次,那麼就會標記該從庫網絡很差,不適合作爲主庫。
二、篩選出從庫後,第二部就要開始打分了,主要從三方面打分,
1.從庫優先級,這是能夠經過slave-property設置的,設置的高,打分的就高,就會被選爲主庫,好比你能夠給從庫中內存帶寬資源充足設置高優先級,當主庫掛了以後被優先選舉爲主庫。
2.從庫與舊主庫之間的複製進度,以前咱們知道主從之間增量複製,有個參數slave-repl-offset記錄當前的複製進度。這個數值越大,說明與主庫複製進度約靠近,打分也會越高。
3.每一個從庫建立實例的時候,會隨機生成一個id,id越小的得分越高。
通知:哨兵提高一個從庫爲新主庫後,哨兵會把新主庫的地址寫入本身實例的pubsub(switch-master)中。客戶端須要訂閱這個pubsub,當這個pubsub有數據時,客戶端就能感知到主庫發生變動,同時能夠拿到最新的主庫地址,而後把寫請求寫到這個新主庫便可,這種機制屬於哨兵主動通知客戶端。
若是客戶端由於某些緣由錯過了哨兵的通知,或者哨兵通知後客戶端處理失敗了,安全起見,客戶端也須要支持主動去獲取最新主從的地址進行訪問。
因此,客戶端須要訪問主從庫時,不能直接寫死主從庫的地址了,而是須要從哨兵集羣中獲取最新的地址(sentinel get-master-addr-by-name命令),這樣當實例異常時,哨兵切換後或者客戶端斷開重連,均可以從哨兵集羣中拿到最新的實例地址。
部署哨兵集羣的時候,咱們知道只須要配置:sentinel monitor
Redis有提供了pub/sub機制,哨兵跟主庫創建了鏈接以後,將本身的信息發佈到 「sentinel:hello」頻道上,其餘哨兵發佈並訂閱了該頻道,就能夠獲取其餘哨兵的信息,那麼哨兵之間就能夠相互通訊了。
那麼哨兵如何知道從庫的鏈接信息呢,那是由於INFO命令,哨兵向主庫發送該命令後,得到了全部從庫的鏈接信息,就能分從庫創建鏈接,並進行監控了。
從本質上說,哨兵就是一個運行在特定模式下的 Redis 實例,只不過它並不服務請求操做,只是完成監控、選主和通知的任務。因此,每一個哨兵實例也提供 pub/sub 機制,客戶端能夠從哨兵訂閱消息。哨兵提供的消息訂閱頻道有不少,不一樣頻道包含了主從庫切換過程當中的不一樣關鍵事件。
與mysql同樣,當一張表的數據很大時,查詢耗時可能就會愈來愈大,咱們採起的措施是分表分庫。一樣的Redis也樣,當數據量很大時,好比高達25G,在單分片下,咱們須要機器有32G的內存。可是咱們會發現,有時候redis響應會變的很慢,經過INFO查詢Redis的latest_fork_usec指標,最近fork耗時,發現耗時很大,快到秒級別了,fork這個動做會阻塞主線程,因而就致使了Redis變慢了。
因而就有redis分片集羣, 啓動多個 Redis 實例組成一個集羣,而後按照必定的規則,把收到的數據劃分紅多份,每一份用一個實例來保存。回到咱們剛剛的場景中,若是把 25GB 的數據平均分紅 5 份(固然,也能夠不作均分),使用 5 個實例來保存,每一個實例只須要保存 5GB 數據。
那麼,在切片集羣中,實例在爲 5GB 數據生成 RDB 時,數據量就小了不少,fork 子進程通常不會給主線程帶來較長時間的阻塞。採用多個實例保存數據切片後,咱們既能保存 25GB 數據,又避免了 fork 子進程阻塞主線程而致使的響應忽然變慢。
那麼數據是如何決定存在在哪一個分片上的呢?
Redis Cluster 方案採用哈希槽(Hash Slot,接下來我會直接稱之爲 Slot),來處理數據和實例之間的映射關係。在 Redis Cluster 方案中,一個切片集羣共有 16384 個哈希槽,這些哈希槽相似於數據分區,每一個鍵值對都會根據它的 key,被映射到一個哈希槽中。具體的映射過程分爲兩大步:首先根據鍵值對的 key,按照CRC16 算法計算一個 16 bit 的值;而後,再用這個 16bit 值對 16384 取模,獲得 0~16383 範圍內的模數,每一個模數表明一個相應編號的哈希槽。
咱們在部署 Redis Cluster 方案時,可使用 cluster create 命令建立集羣,此時,Redis 會自動把這些槽平均分佈在集羣實例上。例如,若是集羣中有 N 個實例,那麼,每一個實例上的槽個數爲 16384/N 個。 也可使用 cluster meet 命令手動創建實例間的鏈接,造成集羣,再使用 cluster addslots 命令,指定每一個實例上的哈希槽個數。