一、redis 基礎

1.1 導言

若是你歷來沒使用過 Redis 數據庫,那你確定會問,爲何咱們要學 Redis數據庫,我只使用 MySQL 或 Oracle 就夠了。其實 Redis 雖叫數據庫,可又不是傳統意義上的關係型數據庫,Redis 是一個高性能的 Key-Value 數據庫。javascript

首先咱們先來說一下 Redis 的歷史。Redis 實際上是做者 Salvatore Sanfilippo 爲了解決實際問題而創造出來的。當時做者 Salvatore 有這麼一個需求,就是多個網站不斷向服務器發送頁面,而服務器須要爲每一個網站保存必定數量的最新頁面記錄,同時經過網頁將數據實時給用戶看到。可是不管 Salvatore 如何優化,都很難在關係數據庫裏讓小虛擬機處理大負荷的負載。最終他打算本身寫一個內存數據庫,能對列表的兩端執行常數時間複雜度的彈出和推入操做,並加上子進程的持久化操做,因而 Redis 就誕生了。php

到了今天,Redis 已經進入了成熟期。數以千計的開發者都在開發和使用這個數據庫,Redis 擁有很是完善的文檔。我記得第一次使用 Redis,是爲了在保存有數十百萬用戶的關係數據庫裏對某個條件進行查詢。你們知道,要想在幾百萬用戶中找到某條數據,是很難經過關係數據庫在十幾秒查詢到的。因而我選擇了 Redis,在不斷優化後每次操做能夠控制在 1 秒鐘甚至更短,帶給我至關大的震撼。css

本教程不但教給你一些基本的使用,同時也會根據我多年總結的技巧解決平常生產環境上優化和排錯的問題。特別是後期的數據庫優化和集羣的講解,但願對各位進行 Redis 開發有必定幫助。前端

1.2 認識 Redis

在 Redis 以前,不少互聯網公司會使用 MySQL + Memcached 架構,這個架構雖然適合於海量數據存儲,但隨着業務的增長,會出現不少問題,例如,MySQL 數據庫常常拆表,致使 Memcached 也不斷擴容;同步問題;命中率低,致使直接穿透 Memcached 進入 DB 查詢,DB 資源池是有限的,進而宕機。這些問題都會致使Memcached其實並很差用。java

Redis 就在這種時代背景中產生,你會發現 Memcached 遇到的問題都被 Redis 給解決了。若是你用過 Memcached,你就會感覺到 Redis 絕對不是簡單的 Key-Value 數據,還有 List、Set、哈希等各類數據類型的存儲,同時支持冷熱備份和主從複製,不但解決了數據庫的容錯,還能輕易地將數據分佈到多個 Redis 實例中。node

1. Redis 特性

那麼 Redis 有哪些具體特性呢?大體可分爲以下八大特性。mysql

  • 速度極快。官方給出的數據是 10 萬次 ops 的讀寫,這主要歸功於這些數據都存在於內存中。因爲 Redis 是開源的,當你打開源代碼,就會發現 Redis 都是用 C 語言寫的,C 語言是最接近計算機語言的代碼,並且只有區區 5 萬行,保證了 Redis 的速度。同時一個 Redis 只是一個單線程,其真正的緣由仍是由於單線程在內存中是效率最高的。ios

  • 持久化。Redis 的持久化能夠保證將內存中的數據每隔一段時間就保存於磁盤中,重啓的時候會再次加載到內存。持久化方式是 RDB 和 AOF。nginx

  • 支持多種數據結構。分別支持哈希、集合、BitMaps,還有位圖(多用於活躍用戶數等統計)、HyperLogLog(超小內存惟一值計數,因爲只有 12K,是有必定偏差範圍的)、GEO(地理信息定位)。git

  • 支持多種編程語言。支持 Java、PHP、Python、Ruby、Lua、Node.js。

  • 功能豐富。如發佈訂閱、Lua 腳本、事務、Pipeline(管道,即當指令到達必定數量後,客戶端纔會執行)。

  • 簡單。不依賴外部庫、單線程、只有 23000 行 Code。

  • 主從複製。主節點的數據作副本,這是作高可用的基石。

  • 高可用和分佈式。Redis-Sentinel(v2.8)支持高可用,Redis-Cluster(v3.0)支持分佈式。

2. Redis 場景

Redis 最大的做用是增長你原來的訪問性能問題,試想若是項目已經搭建好,這個項目通常是不太可能更換的。可是 Redis 獨特的存在是隻須要增長一層,把經常使用的數據存放在 Redis 便可。你在開發環境中使用 Redis 功能,但卻不須要轉到 Redis。

不管是什麼架構,你均可以將 Redis 融入項目中來,這能夠解決不少關係數據庫沒法解決的問題。好比,現有數據庫處理緩慢的任務,或者在原有的基礎上開發新的功能,均可以使用 Redis。接下來,咱們一塊兒看看 Redis 的典型使用場景。

  • 緩存系統

這是 Redis 使用最多的場景。Redis 可以替代 Memcached,讓你的緩存從只能存儲數據變得可以更新數據,所以你再也不須要每次都從新生成數據。

毫無疑問,Redis 緩存使用的方式與 Memcached 相同。網絡中老是可以看到這個技術更新換代,Redis 的原生命令,儘管簡單卻功能強大,把它們加以組合,能完成的功能是沒法想象的。固然,你能夠專門編寫代碼來完成全部這些操做,但 Redis 實現起來顯然更爲輕鬆。

enter image description here

  • 計數器

如轉發數、評論數,有了原子遞增(Atomic Increment),你能夠放心的加上各類計數,用 GETSET 重置,或者是讓它們過時。目前新浪是號稱史上最大的 Redis 集羣。

好比,你想計算出最近用戶在頁面間停頓不超過 30 秒的頁面瀏覽量,當計數達到好比 10 時,就能夠顯示提示。再好比,若是想知道何時封鎖一個 IP 地址,INCRBY 命令讓這些變得很容易,經過原子遞增保持計數;GETSET 用來重置計數器;過時屬性用來確認一個關鍵字何時應該刪除。

  • 消息隊列系統

雖然 Kafka 更強,可是簡單的能夠使用 Redis。運行穩定而且快速,支持模式匹配,可以實時訂閱與取消頻道。

Redis 還有阻塞隊列的命令,可以讓一個程序在執行時被另外一個程序添加到隊列。你也能夠作些更有趣的事情,好比一個旋轉更新的 RSS Feed 隊列。

  • 排行榜及相關問題

排行榜實際就是一種有序集合。對於 Redis 來講,若是你要在幾百萬個用戶中找到排名,其餘數據庫查詢是很是慢的,由於每過幾分鐘,就會有幾百萬個不一樣的數據產生變化,可是 Redis 卻能夠輕鬆解決。

排行榜(Leader Board)按照得分進行排序。ZADD 命令能夠直接實現這個功能,而 ZREVRANGE 命令能夠用來按照得分獲取前 100 名的用戶,ZRANK 能夠用來獲取用戶排名,很是直接並且操做容易。

  • 社交網絡

Redis 能夠很是好地與社交網絡相結合,如新浪微博、Twiter 等,好比 QQ 和用戶交互的時候,用戶和狀態消息將會聚焦不少有用的信息,不少交互如實時聊天就是經過 Redis 來實現的。

  • 按照用戶投票和時間排序

Reddit 的排行榜,得分會隨着時間變化。LPUSH 和 LTRIM 命令結合運用,把文章添加到一個列表中。一項後臺任務用來獲取列表,並從新計算列表的排序,ZADD 命令用來按照新的順序填充生成列表。列表能夠實現很是快速的檢索,即便是負載很重的站點。

  • 過時項目處理

經過 Unix 時間做爲關鍵字,用來保持列表可以按時間排序。對 current_time 和 time_to_live 進行檢索,完成查找過時項目的艱鉅任務。另外一項後臺任務使用 ZRANGE...WITHSCORES 進行查詢,刪除過時的條目。

  • 實時系統

使用位圖來作布隆過濾器,例如實現垃圾郵件過濾系統的開發變得很是容易。

綜上所述, Redis 的應用是很是普遍的,並且在實際使用中是很是有價值的。你可讓網站向 100 萬用戶推薦新聞、能夠實時顯示最新的項目列表、在遊戲中實時得到排名、得到全球排名,等等。Redis 的出現,解決了傳統關係數據庫的短板,讓開發變得更加簡單和高效,大大提升了開發效率,也在用戶體驗上得到更加實時的體驗。隨着 Redis 的使用愈來愈普遍,將會有更多的開發者加入 Redis 的使用和開發上來。

1.3 小結

最後咱們回顧下本文所講述的內容。

首先,介紹了 Redis 主要是用於緩存系統的,不一樣於通常關係數據庫。

其次,咱們介紹了 Redis 的八大特性。經過這八大特性,咱們能夠把常常變化的數據放在 Redis 數據庫中,並設置過時時間,到達時間 Redis 就會自動刪除;還能夠緩解服務器壓力,如咱們平常發微博,先會保存在 Redis 數據庫中,而後等數據庫壓力比較小的時候保存進關係數據庫中。

2.1 內部實現

接下來,咱們簡單瞭解下 Redis 的內部實現。Redis 內部會封裝一個 redisObject 實例。由這個 redisObject 來表示全部的 key 和 value 。redisObject 所包含的字段中,最主要的是 type 和 encoding。

  • type 表明一個 value 對象具體是何種數據類型,包括 String 、Hash 、List 、Set 和 Sorted set 等數據類型。

  • encoding 則表示不一樣數據類型在 Redis 內部的存儲方式,包括 Raw 、Int 、Ziplist 、LinkedList 、HashMap 和 Intset 存儲方式。

上面說得比較抽象,爲了幫助你們理解,我舉個例子,好比 type 爲 String 表明的是 value 存儲了一個字符串,那麼對應的 encoding 能夠是 Int 或者 Raw 。若是是 Int 則表明實際 Redis 內部是按數值類型存儲的,好比你要存儲 「1234」 、「2018」 等字符串。

還有一個特別的內部字段,vm 字段。這個字段默認是關閉的,只有打開了 Redis 的虛擬內存功能,才能真正分配內存。有同窗要問了,這個字段有什麼用呢?由於 Redis 是 Key/Value 存儲,是很是浪費內存的,這些內存成本主要是爲了給 Redis 上面的 7 種數據類型提供統一管理接口。

2.2 單線程

咱們再來看下爲何 Redis 中單線程快。不少程序員應該深有體會,其實其餘不少語言單線程是很是慢的,可是爲何 Redis 的單線程快呢?

我以爲最大的緣由是純內存存儲。正由於這個是主要緣由,因此後面兩個緣由顯得有點不過重要,即非阻塞 IO 和避免線程切換和競態消耗。

你要清楚,首先 Redis 一次只運行一條命令。其次咱們應該減小長命令,哪些是長命令呢?如 KEYS 、 FLUSHALL 、FLUSHDB 、Slow Lua Script 、MULTI/EXEC 、Operate Big Value( Collection )。最後說明一點,其實 Redis 不僅是單線程,若是你去讀源碼,你就會發現有些指令毫不是單線程可以作的。如 Fysnc File Descriptor 、Close File Descriptor 等。

2.3 7 種數據類型的使用

1. 字符串 String

Redis 的 key 沒什麼好說,值得一提的就是 value 的五種數據類型。分別是字符串類型、數字、二進制、和 JSON 類型的數據。

那咱們在實際生產環境哪些場景中使用它們呢?如緩存、計數器( 每次加 1 的計數 )、分佈式鎖等場景都能看到。

接着我將列出與該數據類型相關的命令及使用說明。

  • GET 、SET 和 DEL 。這是 Redis 最簡單的命令,若是你要獲得一個 value 的值,只須要 GET key ,就 ok 了。若是你要設置某個 key 的值,那就 SET key value ,搞定。

  • INCR 、DECR 、INCRBY 和 DECRBY 。

  • INCR key :就是 key 自增 1,不存在,自增後 get(key)=1 ;

  • DECR key :就是 key 自減 1,不存在,自減後返回 -1 ;

  • INCRBY key k :自增 k ,不存在,則返回 k ;

  • DECRBY key k :自減 k 。

    實際使用:若是你想訪問某個主頁的訪問量,那能夠用 INCR id:pageview 。

    咱們實際開發中,經常會使用下面的僞代碼。

public VideoInfo get(long id){ String redisKey = redisPrefix + id; VideoInfo videoInfo = redis.get(redisKey); if(videoInfo == null){ videoInfo = mysql.get(id); if(videoInfo != null){ // 序列化 redis.set(redisKey, serialize(videoInfo)); } } return videoInfo; } 
  • SET 、SETNX 和 SET xx 。

  • SET key value :無論 key 是否存在,都設置;

  • SETNX key value :key 不存在,才設置( 至關於 add );

  • SET key value xx :key 存在,才設置( 至關於 update )。

    實際操做,見以下代碼。

exists php --> 0 set php good -->OK setnx php bad -->0 set php best xx -->ok exists lua --> 0 set lua hehe xx -->(nil) 
  • MGET 、MSET 。

  • MGET key1 key2 key3 …:批量獲取 key,原子操做;

  • MSET key1 val2 key2 val2 key3 val3:批量設置 key-value。

    實際開發的過程當中,咱們一般使用 MGET,由於 MGET 只有 1 次網絡時間和 n 次命令時間。可是若是你使用 GET 的話,就是 n 次網絡時間和 n 次命令時間。

    使用 MGET 效率更高。

  • GETSET 、APPEND 和 STRLEN。

  • GETSET key newvalue:set key newvalue 並返回舊的 value,舊的 value 會被刪除;

  • APPEND key value :將 value 追加到舊的 value 上;

  • STRLEN key :返回字符串的長度( 注意中文)。

  • INCRBYFLOAT 、GETRANGE 和 SETRANGE。

  • INCRBYFLOAT key 3.5:在 key 上追加對應的值 3.5;

  • GETRANGE key start end :獲取字符串指定下標全部的值;

  • SETRANGE key index value :設置指定下標全部對應的值。

2. 哈希 Hash

說到 Hash,就要說到爲何咱們要使用 Hash。咱們在使用字符串的數據類型的時候,若是咱們存儲的是個對象,好比某個圖書館的會員,裏面存儲着會員的姓名、年齡、身份證信息、地址、借閱書籍、借閱時間……一系列的屬性。

若是咱們用 String 來存儲的話,那咱們就須要每次都序列化這個字符串,每次只要一修改某個屬性,咱們就要把一串屬性都覆蓋一遍。這樣是否是很是麻煩?

這個時候,哈希就應運而生了。Hash 至關於 value 是一個 Map ,裏面的全部屬性咱們均可以單獨新增、修改或者刪除,而不須要像字符串那樣所有覆蓋操做。

經常使用的命令有 HGET 、HSET 和 HGETALL。

  • HGET key field:獲取存儲在哈希表中指定字段的值;
  • HSET key field value:將哈希表 key 中的字段 field 的值設爲 value ;
  • HGETALL key:獲取在哈希表中指定 key 的全部字段和值,生產環境不經常使用

3. 列表 List

List 是一種簡單的字符串的集合,是有順序的。在實際生產環境中,咱們時常會使用它。好比當咱們須要獲取某個數據的列表(例如粉絲列表)。

因爲 Redis 的 List 是鏈表結構,咱們能夠很是輕鬆地實現消息排行等功能,還能用於消息隊列等功。

經常使用的命令有 LPUSH 、RPUSH 、LPOP 、RPOP 和 LRANGE。

  • LPUSH key value1 [value2] :將一個或多個值插入到列表頭部;
  • RPUSH key value1 [value2] :在列表中添加一個或多個值;
  • LPOP key :移出並獲取列表的第一個元素;
  • RPOP key :移除並獲取列表最後一個元素;
  • LRANGE key start stop :獲取列表指定範圍內的元素。

4. 集合 Set

Set 和 List 最大的不一樣是無序,Set 是沒有順序的。集合成員是惟一的,這就意味着集合中不能出現重複的數據。

經常使用命令有 SADD 、SCARD 、SNENVERS 和 SPOP。

  • SADD key member1:向集合添加一個或多個成員;
  • SCARD key:獲取集合的成員數;
  • SMEMBERS key:返回集合中的全部成員;
  • SPOP key:移除並返回集合中的一個隨機元素。

5. Sorted Set 有序集合

Sorted Set 和 Set 最大的不一樣是前者是自動排序的,然後者是無序的。若是你須要一個有序的,可是不重複的數據結構,就能夠使用 Sorted Set 。

經常使用的命令有 ZADD 、ZRANGE 、ZREM 和 ZCARD。

  • ZADD key score1 member1:向有序集合添加一個或多個成員,或者更新已存在成員的分數;
  • ZRANGE key start stop:經過索引區間返回有序集合成指定區間內的成員;
  • ZREM key member:移除有序集合中的一個或多個成員;
  • ZCARD key:獲取有序集合的成員數。

6. Pub/Sub 發佈訂閱

即發佈(Publish)與訂閱(Subscribe)。在 Redis 中,你能夠設定對某一個 key 值進行消息發佈及消息訂閱,當 key 的值進行了消息發佈後,全部訂閱它的客戶端都會收到相應的消息,這相似於 QQ 和微信。

經常使用的命令有 PSUBSCRIBE 、PUBSUB 、PUBLISH 和 SUBSCRIBE。

  • PSUBSCRIBE pattern:訂閱一個或多個符合給定模式的頻道;
  • PUBSUB subcommand :查看訂閱與發佈系統狀態;
  • PUBLISH channel message:將信息發送到指定的頻道;
  • SUBSCRIBE channel:訂閱給定的一個或多個頻道的信息;
  • UNSUBSCRIBE [channel [channel …]]:指退訂給定的頻道。

7. Transactions 事務

咱們通常認爲 NoSQL 數據庫都沒有事務,恐怕要讓你失望了。Redis 就支持事務,但並非咱們通常意義上的事務,若是你執行 exec 命令,途中斷電或者服務器掛掉了,咱們仍是會發現 Redis 裏一部分插入了,一部分未插入。

不過 Redis 提供了 WATCH 命令,咱們能夠對某個 key 來 watch 一下,而後再執行 Transactions 。若是這個被 Watch 的值進行了修改,那麼這個 Transactions 會發現並拒絕執行。

redis 127.0.0.1:6381> MULTI OK redis 127.0.0.1:6381> SET book-name "JAVA Programming Mastering Series" QUEUED redis 127.0.0.1:6381> GET book-name QUEUED redis 127.0.0.1:6381> SADD tag "java" "Programming" "Mastering Series" QUEUED redis 127.0.0.1:6381> SMEMBERS tag QUEUED redis 127.0.0.1:6381> EXEC 1) OK 2) "JAVA Programming Mastering Series" 3) (integer) 3 4) 1) "java" 2) "Programming" 3) "Mastering Series" 

經常使用命令有 MULTI 、EXEC 和 DISCARD。

  • MULTI:標記一個事務塊的開始;
  • EXEC:執行全部事務塊內的命令;
  • DISCARD:取消事務,放棄執行事務塊內的全部命令;
  • UNWATCH:取消 WATCH 命令對全部 key 的監視;
  • WATCH key:監視 key,若是在事務執行以前 key 被其餘命令所改動,那麼事務將被打斷。

Redis 做爲一個數據庫,不少開發者還能夠單獨使用它。事實上,更多時候 Redis 是在數據庫和代碼中間做爲一箇中間件使用,若是你發現目前的數據庫出現瓶頸,那麼就能夠經過 Redis 來優化。

 

4.1 慢查詢

1. 生命週期

慢查詢的生命週期,可參見下圖。首先客戶端發送命令給 Redis,Redis 須要對慢查詢排隊處理,這裏須要說明的是慢查詢發生在第三階段,也就是下圖的「3,執行命令」這一階段。同時客戶端超時不必定是慢查詢,但慢查詢倒是客戶端超時的一個緣由。最後,執行完後返回。

enter image description here

請你們認識好這個模型,在實際開發中,咱們須要對異常進行判斷,這時,該流程就顯得很是重要。

2. 如何配置慢查詢

所謂慢查詢指的是內部執行時間超過某個指定時限的查詢,而控制該指定時限的就是 Redis 配置文件中的配置項 slowlog-log-slower-than。除 slowlog-log-slower-than 外,在配置文件中還有另一個參數與慢查詢日誌有關,那就是 slowlog-max-len,該配置項控制了 Redis 系統最多可以維護多少條慢查詢。下面讓咱們一個一個地講解。

  • slowlog-max-len:服務器使用先進先出的方式保存多條慢查詢日誌。

當服務器儲存的慢查詢日誌數量等於 slowlog-max-len 選項的值時, 服務器在添加一條新的慢查詢日誌以前,會先將最舊的一條慢查詢日誌刪除。

什麼意思呢?首先 Redis 會配置一個 slowlog-log-slower-than = 10000,slowlog-max-len = 100,就是說 10000 微秒(1 秒等於 1,000,000 微秒)後是慢查詢,而後就把它放到隊列(內存)中,而且從 100 開始一直到 1。以下圖所示:

enter image description here

  • slowlog-log-slower-than:該選項指定執行時間超過多少微秒。

例如,若是這個選項的值爲 1000,那麼執行時間超過 1000 微秒的命令就會被記錄到慢查詢日誌。若是這個選項的值爲 5000,那麼執行時間超過 5000 微秒的命令就會被記錄到慢查詢日誌。以此類推。

redis> CONFIG SET slowlog-log-slower-than 0 OK redis> CONFIG SET slowlog-max-len 10 OK 

上面的代碼表示,CONFIG_SET 命令將 slowlog-log-slower-than 選項的值設爲 0 微秒,這樣 Redis 執行的所有命令都會被記錄進去,而後將 slowlog-max-len 選項的值設爲 10,讓服務器最多隻保存 10 條慢查詢日誌。

而後咱們發送幾個請求:

redis> SET msg "welcome my city" OK redis> SET number 12345 OK redis> SET database "redis" OK 

SLOWLOG GET 命令就能夠查看慢日誌,以下。

redis> SLOWLOG GET
1) 1) (integer) 4 # 日誌的惟一標識符(uid) 2) (integer) 1338784447 # 命令執行時的 UNIX 時間戳 3) (integer) 12 # 命令執行的時長,以微秒計算 4) 1) "SET" # 命令以及命令參數 2) "database" 3) "redis" 2) 1) (integer) 3 2) (integer) 1372181139 3) (integer) 10 4) 1) "SET" 2) "number" 3) "12345" ... 

3. 慢查詢的默認值

咱們在使用 Redis 時,能夠設置慢查詢的默認值。有如下兩種方式。

方式一,請見下面代碼。

// 不推薦 config get slowlog-max-len = 128 // 設置 slowlog-max-len 爲 128,保存數據最多 128 條 config get slowlog-log-slower-than = 10000// 表示超時到 10000 微秒會被記錄到日誌上 

上面的配置表示修改配置文件後須要重啓,這裏不推薦使用此方式。

方式二,動態配置。配置方式,請見下面兩條命令。推薦使用該方式。

// 推薦使用動態配置
config set slowlog-max-len 1000 // 設置 slowlog-max-len 爲 1000,保存數據最多 1000 條,不能過小,也不能太大 config set slowlog-log-slower-than 10000 // 表示超時到 10000 微秒會被記錄到日誌上 

4. 慢查詢的命令

  • slowlog get[n]:獲取慢查詢隊列;
  • slowlog len:獲取慢查詢隊列的長度;
  • slowlog reset:清空慢查詢隊列。

對於慢查詢,咱們在實際使用的時候須要注意如下幾點。

  • slowlog-log-slower-than 不要太大,默認是 10ms,實際使用中也只是 1ms 或者 2ms,必須根據 QPS 的大小來設定;
  • slowlog-max-len 不要過小,一般是 1000。默認是 128。由於存在內存中,若是設置太小,會致使以前的慢查詢丟失,故建議改爲 1000;
  • 按期對慢查詢進行持久化。

4.2 PipeLine 流水線

1. 概念

若是想同時使用 HSET 或者 MSET,那有沒有 HMSET 呢?其實是沒有的,可是咱們能夠使用 PipeLine 流水線功能。

若是使用 n 次網絡請求加 n 次命令就很麻煩,可是若是使用 PipeLine 的 1 次網絡請求加 n 次命令,就能夠節約網絡帶寬和訪問時間。可是 PipeLine 每次條數要控制。

2. 如何使用 PipeLine

這裏咱們介紹在 Jedis 中使用 PipeLine,咱們經過使用 PipeLine 和不使用 PipeLine 做對比,你就能清晰地感覺 PipeLine 的方便了。

第一種方式用 HSET,就是咱們不用 PipeLine 的時候,代碼以下。

Jedis jedis = new Jedis("127.0.0.1",6379); // new 一個 Redis,它提供 IP 和端口號的參數 for(int i=0;i<10000;i++){ jedis.hset("keyvalue:"+i,"keyfield"+i); } // 測試後發現,1w 的 HSET 須要 50s 

第二種使用 PipeLine 方式,代碼以下。

Jedis jedis = new Jedis("127.0.0.1",6379);// new 一個 Redis,它提供 IP 和端口號的參數 for(int i=0;i<100;i++){ Pipeline pipeline = jedis.pipelined();// 此句就是使用 pipelined 方法,激活 pipeline for(int j=i*100;j<(i+1)*100;j++){ pipeline.hset("key"+j,"keyfield"+j,"keyvalue"+j); } pipeline.syncAndReturnAll();//結束的時候必需要加的 } // 測試發現,1w 的 HSET 只須要 0.7s 

從上面的代碼你就能看到,若是你不使用 PipeLine,這個速度是很是慢的,須要 50 秒;可是若是使用 PipeLine,那麼只須要 0.7 秒就能循環出來了。

這裏須要注意如下兩點:

  • 每次 PipeLine 攜帶的數據量;
  • PipeLine 一次只能在一個 Redis 節點上。在後面的集羣部分,我也會進行說明。

4.3 BitMap 位圖

接下來咱們來說位圖功能。咱們知道,若是你要 Set 一個值,好比 one,那麼 one 這個值存放在內存中實際上是以二進制的方式,那麼咱們經過 getbit one 0,就能查詢到這個 one 在內存中的第一個二進制是多少。

enter image description here

如上圖所示,上面的英文字母在內存裏都是 0 和 1,那麼當咱們 getbit one 0 的時候,咱們取的就是這個內存中二進制的第一個值,獲得的就是 0。

使用 BitMap,有以下五個命令。

  • SETBIT
setbit key offset value //給位圖指定索引設置值 

例如,setbit book1:books:2017-01-10 1 1,即將 book1 的 2017 年 1 月 10 日的第一個位圖值改成 1,返回結果是以前的值。

  • GETBIT
getbit key offset //獲取位圖對應的值 

例如,getbit book1:books:2017-01-10 1 8,和 SET 相似。

  • BITCOUNT
bitcount key [start end] // 獲取位圖指定範圍值爲 1 的個數 

例如,bitcount book1:books:2017-01-10 1 3,返回 3。

  • BITOP
bitop op destkey key [key...] 

上面代碼表示,進行多個 Bitmap 的 and、or、not、xor 操做並保存到 destkey 中。

  • BITOPS
bitops key targetBit [start] [end] // 計算指定位圖範圍第一個偏移量對應的值等於 targetBit 的位置 

例如,bitops book1:books:2017-01-10 1 2 5,後面三個數表示 2 到 5 的位置裏爲 1 的位置。

比較 SET 和 BitMap,不少是使用在統計上的,可見下面兩個示例。

示例一,若是你有 1 個億的用戶,天天 5 千萬的獨立訪問。

數據類型 空間佔用 存儲用戶量 所有內存佔用
set 32位 50,000,000 50,000,000*32 =200M
bitmap 1位 100,000,000 100,000,000*1=12.5M

若是你用 SET,一天是 200 M,那麼一年就是 72G,但若是你用 BitMap,一年只有 4.5G,你以爲哪一個好呢?

示例二,可是若是你只有 10萬獨立用戶呢?

數據類型 空間佔用 存儲用戶量 所有內存佔用
set 32位 1,000,000 1,000,000*32 =4M
bitmap 1位 100,000,000 100,000,000*1=12.5M

經過上述表格可知,這個時候就要用 SET 了。

因此,BitMap 不必定好,BitMap 是針對大數據量設計的。咱們須要根據需求來區分使用,若是數據量很是大,能夠考慮,只有在用戶量很是大的時候,纔會使用。

4.4 HyperLogLog

這種算法又叫:極小空間完成獨立數量統計。咱們平常是不會使用 HyperLogLog 算法的,只有當統計數據量很是大的時候,纔會使用它。不少小夥伴在研究 HyperLogLog 的時候,就會發現 HyperLogLog 本質上仍是字符串 String。你不信能夠使用 type 命令,就知道 HyperLogLog 返回的是字符串。

經常使用的 HyperLogLog 命令有如下三個。

  • pfadd key element ...:向HyperLogLog 添加元素;
  • pfcount key ...:計算HyperLogLog 的獨立總數;
  • pfmerge key1 key2 ...:合併多個 HyperLogLog。

4.5 發佈訂閱

有的小夥伴會問什麼是發佈訂閱,工業生產設計以前是根據非定時的監聽設計,定時器會定時在內存中進行監聽,若是有改變,再發送,其實不算是實時的。

同時,這種工業設計實際上是很是複雜的。不少實時的發佈依賴於 Redis 作事件消息推送。發佈訂閱大大簡化了設計流程,並且性能也比較可觀。

Redis 在 2.0 以後實現了事件推送的 Pub/Sub 命令。Pub 就是發佈者 Publisher,Sub 就是訂閱者 Subcriber。

訂閱者只要訂閱這個頻道,就能夠實時得到信息,見下圖。

enter image description here

說明:當發佈者發送一條消息到 Redis Server 後,只要訂閱者訂閱了該頻道就能夠接收到這樣的信息。同時,訂閱者能夠訂閱不一樣的頻道。

使用發佈訂閱,有以下四個命令。

  • PUBLISH,發佈。
publish channel message //發佈命令 publish youku:tv "welcome back!" // 返回有幾個訂閱者。 
  • SUBSCRIBE,訂閱。
subcribe [channel] // 訂閱一個或者多個 subcribe youku:tv // 訂閱優酷並返回信息。 
  • UNSUBSCRIBE,取消訂閱。
unsubcribe [channel] // 取消訂閱一個或者多個 unsubcribe youku:tv // 取消訂閱優酷並返回信息。 
  • 其餘命令

    如 PSUBSCRIBE(訂閱模式)、PUNSUBSCRIBE(退訂指定模式)、PUBSUB CHANNELS(列出至少一個訂閱者頻道)

補充:消息隊列。

Redis 消息隊列是搶的模式,就是隻有一個用戶能收到,誰網速快,誰人品好,誰就能得到那條消息,做爲開發者咱們須要瞭解需求,若是隻須要一個消息訂閱者收到,那麼就能夠使用消息隊列,以下圖所示。

enter image description here

這種場景好比搶紅包、誰中獎這種模式,就能夠使用消息隊列了。

4.6 GEO 地理位置存儲

Redis 3.2 之後纔出現 GEO 的功能。GEO 是使用 ZSET 來實現的。最經常使用的功能就是微信裏的附近的人和搖一搖的功能,還有美團經過本地的座標自動識別周圍的餐館等功能。

使用 GEO,有以下四個命令。

  • GEOADD,增長。
geo add 經度 維度 標識 ... 

例如,geo add cities:location 117.20 40.11 beijing。

  • GEOPOS,獲取地理信息。
geopos key 標識 ... 

例如,geopos cities:locations beijing,返回經度和緯度。

  • GEODIST,計算距離。
geodist key 標識1 標識2 ... [unit] // unit 表示單位 

例如,geodist cities:locations beijing tianjing km,計算北京到天津的距離

  • GEORADIUS,它是很是複雜的,若是你有興趣能夠訪問以下網址找到詳細內容。

5.1 持久化的概念及其做用

首先咱們來看下什麼是 Redis 的持久化。Redis 的全部數據都保持在內存中,對數據的更新將異步保存到磁盤上。若是 Redis 須要恢復時,就是從硬盤再到內存的過程。

enter image description here

由上圖可知,持久化就是把內存中的數據保存到硬盤中的過程。,由於 Redis 自己就在內存中運行的;當忽然斷電或者死機後,咱們能夠從硬盤中拷貝數據到內存,這是整個過程。

這就是 Redis 持久化的用處。持久化是爲了讓硬盤成爲備份,以保證數據庫數據的完整。

持久化有哪些方式呢?你會看到網上和書上有不少分類,可是其實大體只分紅兩種。

  • 快照 RDB
  • 日誌 AOF

5.2 快照 RDB

顧名思義,快照就是拍個照片,作個備份,而這種備份是 Redis 自動完成的。

這個功能是 Redis 內置的一個持久化方式,即便你不設置,它也會經過配置文件自動作快照持久化的操做。你能夠經過配置信息,設置每過多長時間超過多少條數據作一次快照。具體的操做以下:

save 500 100 // 每過 500 秒超過 100 個 key 被修改就執行快照 

咱們再來看看它們的運行情況。

  • Redis 調用 fork() 進程,同時擁有父進程和子進程;

  • 子進程將數據都寫到一個臨時 RDB 文件之中;

  • 當子進程完成對新 RDB 文件的寫入時,Redis 用新 RDB 文件替換舊的 RDB 文件。

這個過程使 Redis 能夠從寫時複製中獲得備份。

接下來咱們講解三種 RDB 的觸發機制:save( 同步 )、bgsave( 異步 )和自動觸發 RDB 文件。

1. save(同步)

即其餘的命令都要排隊。

enter image description here

若是有 1000 萬條數據,執行 save 命令,Redis 就會對 1000 萬條數據打包,而這個時候是同步的,Redis 就會進入阻塞狀態。這也是 save 的缺點,接下來咱們講異步命令 bgsave。

2. bgsave(異步命令)

即返回 OK,後臺會新開一個線程去執行。

enter image description here

上圖可知,客戶端會在 Redis 發出 bgsave 命令,另外開一個進程調用 fork() 方法,這個進程同時會建立 RDB 文件,也就是咱們上面提到的自動觸發 RDB 文件機制。這樣,你在父進程裏操道別的命令,就不會受影響了。

3. 自動觸發 RDB 文件

快照方式雖然是 Redis 自動的,可是若是 Redis 服務器掛掉後,那麼最近寫入的,是不會被拷入快照中的。因此 RDB 存在兩方面的缺點。

  • 耗時耗性能:Redis 寫 RDB 文件是 dump 操做,因此須要的時間是 O(n),須要全部命令都執行一遍,很是耗時耗性能。

  • 容易丟失數據和不可控制:若是咱們在某個時間 T1 內寫多個命令,這個時候 T2 時間執行 RDB 文件操做,T3 時間又執行多個命令,T4 時間就會出現宕機了。那麼 T3 到 T4 的數據就會丟失。

enter image description here

雖然 RDB 有缺陷,可是依然在生產環境中會使用,RDB 適合冷備,就是當用戶數據不高的時候,好比在午夜時分就可以使用 RDB 備份。其餘時候咱們一般使用 AOF 日誌備份。

5.3 日誌 AOF

AOF(Append-only File)是用日誌方式,通俗點講就是當寫一條命令的時候,如 Set 某個值,Redis 就會去日誌裏寫一條 Set 某個值的語句。以下圖:

enter image description here

當服務器宕機後,Redis 就會調用 AOF 日誌文件,而且這個過程通常是實時的,不須要時間消耗。

enter image description here

上圖表示客戶端向 AOF 文件寫入的時候,是會經過緩衝的,緩衝是系統機制,是爲了提升文件的寫入效率。

AOF 三種策略分別是 always 、everysec 和 no。

  • always

客戶端是不會直接把命令寫入 AOF 文件的,Liunx 系統會有一個緩衝機制,把一部分命令打包再同步到 AOF 文件,從而提升效率。

可是若是你使用的是 always 命令,就表示每條命令都寫入 AOF 文件中,這樣是爲了保證每條命令都不丟失。

  • everysec

即每秒策略,簡而言之,就是說每一秒的緩衝區的數據都會刷新到硬盤當中。可是它不像 always 那樣,每條數據都會寫入硬盤中,若是硬盤發生故障有可能丟失 1 秒的數據。

  • no

這個 no 的配置至關於把控制權給了操做系統,操做系統來決定何時刷新數據到硬盤,以及不須要咱們考慮哪一種狀況。

命令 always everysec no
優勢 不丟失數據 每秒一次同步,丟一秒數據 不須要管
缺點 IO 開銷大,通常 SATA 盤只有 TPS 丟一秒數據 不可控制

關於 AOF,咱們補充一點。對於 AOF 操做,Redis 在寫入的時候,會壓縮命令。它既能夠減小硬盤的佔用量,同時能夠提升恢復硬盤的速度。例如以下表格。

原生 AOF AOF 複寫
set hello a1 set hello a3
set hello a2  
set hello a3  
incr counter set counter 2
incr counter  
rpush hello a rpush hello a b c
rpush hello b  
rpush hello c  

從上表能夠看到,set hello 有三個值,可是 a1 和 a2 是無效的,最終 AOF 會自動 set 最後一個 a3 的值;incr counter 兩次,AOF 自動識別 2 次;rpush 三個值,rpush 會自動簡寫爲一條數據。

針對上面的 Redis 的 AOF 複寫。Redis 提供了兩種命令。這兩種命令是 bgrewriteaof 和 AOF 重寫配置。bgrewriteaof 相似 RDB 中的 bgsave 命令,它仍是 fork() 子進程,而後完成 AOF 的過程。

AOF 重寫配置包含兩個配置命令,見以下表格。

配置名 含義
auto-aof-rewrite-min-size AOF 文件重寫最小的尺寸
auto-aof-rewrite-percentage AOF 文件增加率

auto-aof-rewrite-min-size 表示配置最小尺寸,超過這個尺寸就進行重寫。

auto-aof-rewrite-percentage ,這裏說的是 AOF 文件增加比例,指當前 AOF 文件比上次重寫的增加比例大小。AOF 重寫即 AOF 文件在必定大小以後,從新將整個內存寫到 AOF 文件當中,以反映最新的狀態(至關於 bgsave)。這樣就避免了 AOF 文件過大而實際內存數據小的問題(頻繁修改數據問題)。

接下來看下統計配置,以下表所示,有了它,就能夠對上面的配置命令進行控制。

配置名 含義
aof-current-size AOF 當前尺寸(字節)
aof-base-size AOF 上一次啓動和重寫的尺寸(字節)

enter image description here

由上圖可知,bgrewriteaof 命令發出後,Redis 會在父進程中 fork 一個子進程,同時父進程會分別對舊的 AOF 文件和新的 AOF 文件發出 aof_buf 和 aof_rewrite_buf 命令,同時子進程寫入新的 AOF 文件,並通知父進程,最後 Redis 使用 aof_rewrite_buf 命令寫入新的 AOF 文件。

實際配置過程以下。

appendonly yes // appendonly 默認是 no appendfilename "append only - ${port}.aof" //設置 AOF 名字 appendfsync everysec // 每秒同步 dir /diskpath // 新建一個目錄 no-appendfsync-on-rewrite yes // 爲了減小磁盤壓力,AOF 性能上須要權衡。默認是 no,不會丟失數據,可是延遲會比較高。爲了減低延遲,通常咱們設置成 yes,這樣可能丟數據 

5.4 Redis 持久化開發運維時遇到的問題

問題可總結爲四種,即 fork 操做、進程外的開銷和優化、AOF 追加阻塞和單機多實例部署。

1. fork 操做

fork 操做包括如下三種:

  • 同步操做,即 bgsave 時是否進行同步;
  • 與內存量息息相關:內存越大,耗時越長;
  • info:lastest_fork_usec,持久化操做。

改善 fork 的方式有如下四種:

  • 使用物理機或支持高效 fork 的虛擬技術;
  • 控制 Redis 實例最大可用內存 maxmemory;
  • 合理配置 Liunx 系統內存分配策略:vm.overcommit_memory=1;
  • 下降頻率,如延長 AOF 重寫 RDB,沒必要要的全量複製。

2. 子進程的開銷以及優化

這裏主要指 CPU、內存、硬盤三者的開銷與優化。

  • CPU

    開銷:AOF 和 RDB 生成,屬於 CPU 密集型,對 CPU 是巨大開銷;

    優化:不作 CPU 綁定,不與 CPU 密集型部署。

  • 內存

    開銷:須要經過 fork 來消耗內存的,如 copy-on-write。

    優化:echo never > /sys/kernel/mm/transparent_hugepage/enabled,有時啓動的時候會出現警告的狀況,這個時候須要配置這個命令。

enter image description here

  • 硬盤

    開銷:因爲大量的 AOF 和 RDB 文件寫入,致使硬盤開銷大,建議使用 iostat、iotop 分析硬盤狀態。

    優化:

  • 不要和高硬盤負載部署到一塊兒,好比存儲服務、消息隊列等等;

  • 配置文件中的 no-appendfsync-on-rewrite 設置成 yes;

  • 當寫入量很大的時候,建議更換 SSD 硬盤;

  • 單機多實例持久化文件考慮硬盤分配分盤。

3. AOF 追加阻塞

咱們若是使用 AOF 策略,一般就會使用每秒刷盤的策略(everysec),主線程會阻塞,直到同步完成。首先咱們知道主線程是很是寶貴的資源,其次咱們每秒刷盤實際上未必是 1 秒,多是 2 秒的數據。

enter image description here

咱們如何定位 AOF 阻塞?

  • 經過 Redis 日誌

enter image description here

上圖能夠看到, Redis 日誌會出現上述的語句,告訴你異步 IO 同步時間太長,你的硬盤是否有問題,同時會拖慢 Redis。

  • 固然除了上述的問題,你還能夠用 Redis 的 info 方式來肯定問題。
info rersistence // 直接在命令中打這個命令便可。 ... aof_delayed_fsync : 100 // 會記錄你發生阻塞的次數,每一次加 1 ... ... 

可是這個命令沒法看到當前的問題,由於它是歷史的累計值。

固然你還能夠使用 Liunx 命令 top。

enter image description here

上圖能看到 wa 值,wa 值是表示 IO 瓶頸,若是超過 50%,就表示 IO 出現阻塞了。

本文,咱們首先講了持久化的概念,持久化的方式有 RDB 和 AOF 兩種。RDB 主要是 save(同步)、bgsave(異步)、自動觸發 RDB 文件三種觸發方式;AOF 則是 always、everysec、no 三種方式,同時咱們補充了 AOF 重寫的命令和如何配置。

最後咱們討論了 Redis 持久化開發運維時遇到的問題,主要有 fork 操做、進程外的開銷和優化、AOF 追加阻塞、單機多實例部署四種問題。

這要求咱們在作 RDB 和 AOF 備份時,要注意到這些問題。特別是大數據,須要監控 Redis 是否阻塞、開銷是否過大等。

要實現分佈式數據庫的更大的存儲容量和高併發訪問量,咱們會將原來集中式數據庫的數據分別存儲到其餘多個網絡節點上。Redis 爲了解決這個單一節點的問題,也會把數據複製多個副本部署到其餘節點上進行復制,實現 Redis的高可用,實現對數據的冗餘備份,從而保證數據和服務的高可用。

應該說,Redis 複製是高可用的基石。沒有 Redis 複製,也就不可能實現高可用。然而,Redis 複製這塊常常出現開發和運維故障,在咱們排錯以前,須要對 Redis 有一個清晰的認識。

下面咱們將從如下五個方面全面認識 Redis 複製。

  • 什麼是主從複製
  • 複製的配置
  • 全量複製和部分複製
  • 故障如何處理
  • 開發和運維遇到的常見問題

在此以前,咱們須要明確將 Redis 應用到工程項目中時,只用一臺 Redis 顯然是萬萬不能的,爲何不能呢?主要歸納爲如下三個緣由。

第一,機器故障。咱們部署到一臺 Redis 服務器,當發生機器故障時,須要遷移到另一臺服務器而且要保證數據是同步的。而數據是最重要的,若是你不在意,基本上也就不會使用 Redis 了。

第二,容量瓶頸。當咱們有需求須要擴容 Redis 內存時,從 16G 的內存升到 64G,單機確定是知足不了。固然,你能夠從新買個 128G 的新機器。可是咱們非要這麼作嗎?我將會在後續講到。

第三,QPS 瓶頸。Redis 號稱支持10 萬 QPS,當業務須要 100 萬 QPS 時,咱們該怎麼作呢?這時就用到了 Redis 複製。本文講解的主要內容即是 Redis 複製(Replication)。

6.1 什麼是主從複製

enter image description here

如上圖所示,咱們將 Redis 服務器做爲 Master 主庫,另一臺做爲 Slave 從庫,主庫只是負責寫數據,每次有數據更新的時候,Redis 會將數據同步到其餘的從庫中,而從庫只是負責讀數據。

固然你還能夠根據業務需求,增長更多的從庫,以下圖所示,紅色的 Redis 爲主庫,藍色的是三臺從庫。

enter image description here

不少小夥伴都以爲這沒什麼好說的,但在這裏我仍是想說這麼作的兩大好處。

  • 實現了讀寫分離,讀寫分離不只能夠提升服務器的負載能力,同時可根據需求的變化,改變從庫的數量,第一張圖中只有一個從庫,你還能夠像第二張圖那樣增長至兩個、三個……你以爲這個優勢怎麼樣?

  • 數據備份了多份,若是一臺機器宕機,你能夠從其餘機器上快速恢復。但須要注意的是一臺主庫能夠擁有多個從庫,但一個從庫卻只能隸屬於一個主庫。

6.2 複製的配置

接下來,咱們講一下主從複製的做用。首先請看下面這張圖。

enter image description here

如該圖所示,Master 屬於主節點,Slave 屬於從節點。咱們在主機上執行 set 和 incr 命令,在從庫中經過 get 獲得這些數據,這正好說明 Redis 複製是自動完成的。當 Master 節點宕機之後,Slave節點能夠支援主節點。

除了一主一從,Redis 還支持一主多從,這樣咱們就能夠得到更多高可用的可能性。好比,當一個主機和一個從機都宕機了,那麼還有幾臺從機能夠作備份;再好比,當 Master 節點的流量超過最大值的時候,能夠經過從機對流量進行分流。

總而言之,主從複製的做用主要是提供數據副本及擴展 Redis 讀的性能。

接下來,咱們再來配置一下主從架構,這是爲一些剛開始學習的朋友準備的,經過實際的配置,以便於你們更好的理解。

  • 安裝兩臺服務器的實例,關於如何安裝 Redis,我在第一篇文章已經講到。我將主庫端口設置爲6379,從庫設置爲 6380。bind 都設置爲 127.0.0.1。

enter image description here

  • 在 Slave 實例中咱們增長了 slaveof 127.0.0.1 6379 的配置,從庫配置相同。以下圖所示。

enter image description here

配置後,須要啓動這兩個實例,如輸出下面內容,說明主從複製的架構已經配置成功。

enter image description here

這裏惟一須要說明的是,主庫和從庫的端口號不能相同,不然不可能同時啓動。

6.3 全量複製和部分複製

在講解全量複製和部分複製以前,咱們先來說一下,runid 和偏移量的概念。

什麼是 runid,每次 Redis 啓動的時候,Redis 就會有一個運行的 ID,這個 ID 只在 Redis 運行的時候纔有,若是關閉 runid 就不存在了。runid 的做用是一個標識,若是主庫去複製從庫的數據,就須要根據這個 runid 去複製。

enter image description here

上圖所示,經過 redis-cli -p 6379 info server | grep run 顯示出 6379 端口的 runid 和6380端口的 runid。

不少 Redis 第一次啓動的時候,壓根就不知道其餘機器上面的數據,這個時候咱們就須要全量複製。

什麼是偏移量呢?偏移量就是記錄到底寫了多少數據,好比在 6379 端口的 Redis 中,執行 set k1 v1 這個命令就是寫入一個字節。這時它同步給 6380 端口,6380 也會記錄偏移量。

主 Redis 每次向從 Redis 傳播 N 個字節的數據時,都會在本身的複製偏移量上加 N; 同理,從 Redis 每次接收到 N 個字節時,也會在本身的複製偏移量上加 N。

enter image description here

上圖所示,咱們首先運行 redis-cli -p 6379 info replication,就能在裏面找到 master_repl_offset:1865,而後執行一次 set 操做,再運行 redis-cli -p 6379 info replication,此時就能看到偏移值變成了 1950。若是在其餘從庫中運行,也會變成 1950,偏移量是記錄部分複製的依據。你們只要瞭解便可,生產環境中不怎麼關心這個值。

1. 全量複製

咱們來看下 Redis 全量複製的流程圖。

enter image description here

如圖所示:

第一步,Redis 內部會發出一個同步命令,剛開始是 Psync 命令,Psync ? -1 表示要求 Master 主機同步數據;

第二步,Master 會向從機發送 runid 和 offset,由於 Slave 並無對應的 offset,因此是全量複製;

第三步,經過指令 save masterInfo,從機 Slave 會保存 Master 的基本信息;

第四步,Master 執行 bgsave 命令(持久化命令),對於一個快照來講,怎麼快怎麼來。實際上 Master 主機裏有 repl_back_buffer(複製緩衝區);

第五步,經過指令 send RDB 發送 RDB 文件;

第六步,發送緩衝區數據;

第七步,刷新舊的數據;

第八步,加載 RDB 文件和緩衝區數據的加載。

這就是創建全量數據副本的過程。但願你們結合着圖示可以瞭解 Redis 進行全量複製的流程,在大腦裏有一個印象。

那麼全量複製須要哪些開銷呢?主要有如下幾項。

  • bgsave 時間;
  • RDB 文件網絡傳輸時間;
  • 從節點清空數據的時間;
  • 從節點加載 RDB 的時間;
  • AOF 重寫的時間(這裏須要說明一下,RDB 全量複製完加載 RDB,若是 AOF 開啓的話,就會出現 AOF 重寫來保證是最新的)。

2. 部分複製

部分複製是 Redis 2.8 之後出現的,之因此要加入部分複製,是由於全量複製會產生不少問題,好比像上面的時間開銷大、沒法隔離等問題, Redis 但願可以在 Master 出現抖動(至關於斷開鏈接)的時候,能夠有一些機制將複製的損失下降到最低。

enter image description here

如圖所示:

第一步,若是打算抖動(鏈接斷開 connection lost);

第二步,Master 仍是會寫 repl_back_buffer(複製緩衝區);

第三步,Slave 會繼續嘗試鏈接主機;

第四步,Slave 會把本身當前 runid 和偏移量傳輸給主機 Master,而且執行 pysnc 命令同步;

第五步,若是 Master 發現你的偏移量在緩衝區的範圍內,就會返回 continue 命令;

第六步,同步了 offset 的部分數據,因此部分複製的基礎就是偏移量 offset。

經過部分複製,能夠有效的減小全量複製的開銷。

6.4 故障如何處理

講解完主從複製和部分複製,咱們來說一下 Redis 複製的故障如何處理。在開發運維的時候,故障是不可避免的。若是採用單機 Redis ,一旦經過短信或者電話告知你錯誤時,你多會一臉茫然,這種故障一般發生在半夜。

但若是咱們使用了自動故障轉移,那效果就不一樣了。即便半夜出現故障,你仍是能夠好好休息,等早上起來再去完成故障排除。

主要考慮主機 Master 宕機、從機 Slave 宕機兩種狀況。

enter image description here

上圖所示,一主二從的結構,若是某一臺從機宕機了。這個時候咱們須要把客戶端傳到宕機的請求改爲第一個 Slave 從機就能夠了。基本不會有什麼問題。

但若是是 Master 宕機,Master 就會和兩個 Slave 從機斷掉,這個時候該怎麼辦呢?以下圖所示。

enter image description here

當 Master 宕機,原來從 Master 讀的客戶端會經過 slaveof no one 讀寫 Slave 從機,發生該命令的 Slave 從機會變成 Master 主機,而另外一臺從機會執行 slaveof new master 命令,讓另一臺 Slave 知道哪臺變成 Master 而且與它發生同步。

上述兩種狀況其實並非自動的,那咱們如何讓這兩種狀況變成自動化呢,Redis Sentinel 相關知識會在下一篇講解。Redis Sentinel能夠真正實現高可用,它會自動設置好哪臺是 Master,當 Master 宕機,就會自動把某一臺 Slave 從機變成 Master,而且告訴全部 Slave 從機哪臺變成了 Master 並自動鏈接。

6.5 開發和運維中的問題

我將從下面四點來講明:讀寫分離、主從配置不一致、規避全量複製、規避複製風暴。

1. 讀寫分離

讀流量分攤到從節點。這是個很是好的特性,若是一個業務只須要讀數據,那麼咱們只須要連一臺 Slave 從機讀數據。

enter image description here

雖然讀寫有優點,可以讓讀這部分分配給各個 Slave 從機,若是不夠,直接加 Slave 機器就行了。可是也會出現如下問題。

  • 複製數據延遲。可能會出現 Slave 延遲致使讀寫不一致等問題,固然你也能夠使用監控偏移量 offset,若是 offset 超出範圍就切換到 Master 上。
  • 讀到過時數據。Redis 採用懶惰性策略和採樣式策略,懶惰性策略指的是 Redis 操做 key,它纔去看 key 有沒有過時數據。採樣式策略是指按期會去採樣,若是是過時的,就自動刪除。當過時數量很是多的時候,個人採樣速度比不上邏輯數據變化的速度,Slave 沒有刪除權限,只有 Master 有,這個時候就會出現過時數據。但若是你用 Redis 3.2 以上版本時,就沒有這個問題了。
  • 從節點故障。怎麼對發生故障的從節點進行遷移。

2. 配置不一致

主機和從機不一樣,常常致使主機和從機的配置不一樣,並帶來下列兩種問題。

  • 數據丟失:主機和從機有時候會發生配置不一致的狀況,例如 maxmemory 不一致,若是主機配置 maxmemory 爲 8G,從機 Slave 設置爲 4G,這個時候是能夠用的,並且還不會報錯。可是若是要作高可用,讓從節點變成主節點的時候,就會發現數據已經丟失了,並且沒法挽回。

  • 數據結構優化參數致使不一致:hash-max-ziplist-enties 參數,若是主機對這些優化參數設置了,從機 Slave 卻沒有優化,就會發生數據不一致的狀況。

3. 規避全量複製

全量複製指的是當 Slave 從機斷掉並重啓後,runid 產生變化而致使須要在 Master 主機裏拷貝所有數據。這種拷貝所有數據的過程很是耗資源。

全量複製是不可避免的,例如第一次的全量複製是不可避免的,這時咱們須要選擇小主節點,且maxmemory 值不要過大,這樣就會比較快。同時選擇在低峯值的時候作全量複製。

形成全量複製的緣由之一是主從機的運行 runid 不匹配。解釋一下,主節點若是重啓,runid 將會發生變化。若是從節點監控到 runid 不是同一個,它就會認爲你的節點不安全。當發生故障轉移的時候,若是主節點發生故障,那麼從機就會變成主節點。咱們會在後面講解哨兵和集羣再詳細解釋。

形成全量複製的第二個緣由是複製緩衝區空間不足,好比默認值 1M,能夠部分複製。但若是緩存區不夠大的話,首先須要網絡中斷,部分複製就沒法知足。其次須要增大複製緩衝區配置(rel_backlog_size),對網絡的緩衝加強。默認是 1M,咱們實際會設置成 10M 左右。

4. 規避複製風暴

當一個主機下面掛了不少個 Slave 從機的時候,主機 Master 掛了。這時 Master 主機重啓後,由於 runid 發生了變化,全部的 Slave 從機都要作一次全量複製。這將引發單節點和單機器的複製風暴,開銷會很是大。

enter image description here

  • 單節點複製風暴。當主節點重啓,多從節點會複製。這個時候須要更換複製拓撲。上圖就是改變拓撲結構的問題,經過在 Slave 下再分從機,能夠有效的減小主機 Master 的壓力。

enter image description here

  • 單機器的複製風暴。如上圖,若是每一個 Master 主機只有一臺 Slave 從機,那麼當機器宕機之後,會產生大量全量複製。這是很是危險的狀況,帶寬立刻會被佔用,會致使不可用。這個問題在實際運維中必須注意。在這種狀況下,建議將單機器改爲 Redis Sentinel。這樣能夠自動將從機變成主機 Master。

Redis Sentinel 正是爲了解決這樣的問題而被開發的。Redis Sentinel 是一個分佈式的架構,每個 Sentinel 節點會對數據節點和其他 Sentinel 節點進行監控,當發現某個節點沒法到達的時候,會自動標識該節點。若是這個節點是主節點,那麼它會和其餘 Sentinel 節點「協商」,大部分節點都認爲主節點沒法到達的時候,它們會選舉一個 Sentinel 節點來完成自動故障轉移,同時會告知 Redis 的應用方。

因爲這個過程是自動化的,不須要人工參與,大大提升了 Redis 的高可用性。

接下來,咱們將從實現流程、安裝配置、客戶端鏈接、實現原理、常見開發運維問題這五個方面來探討。

7.1 實現流程

以下圖所示,Sentinel 集羣會監控每個 Slave 和 Master。客戶端再也不直接從 Redis 獲取信息,而是經過 Sentinel 集羣來獲取信息。

再看下面這張圖,當 Master 宕機了,Sentinel 監控到 Master 有問題,就會和其餘 Sentinel 進行選舉,選舉出一個 Sentinel 做爲領導,而後選出一個 Slave 做爲 Master,並通知其餘 Slave。上圖 Slave1 變成了 Master,若是原來的 Master 又連上了,也會變成 Slave 從機。

enter image description here

7.2 安裝與配置

咱們將從如下兩個方面講解如何安裝和配置主從節點和 Sentinel 節點。

  • 如何配置開啓主從節點;
  • 如何開啓 Sentinel 監控主節點。

1. 開啓主從節點

Sentinel 對主節點和從節點的配置是不一樣的,須要分別配置,咱們分開來說解。

  • 主節點配置

咱們在命令行使用下面的命令進行主節點的啓動。

redis-server redis-7000.conf 

啓動完成之後,咱們參考下面的配置進行參數的設置。

port 7000 daemonize yes // 守護進程 pidfile /var/run/redis-7000.pid // 給出 pid logfile 「7000.log」 // 日誌查詢 dir "/opt/redis/data" // 工做目錄 
  • 從節點配置

咱們在命令行使用下面的命令進行從節點的啓動。

redis-server redis-7001.conf redis-server redis-7002.conf 

啓動完成之後,和主節點配置同樣,配置下面的參數。這個時候要注意,咱們須要分別對 Slave 節點的每臺機器進行配置。

Slave1 的配置以下。

port 7001 daemonize yes // 守護進程 pidfile /var/run/redis-7001.pid // 給出 pid logfile 「7001.log」 // 日誌查詢 dir "/opt/redis/data" // 工做目錄 slaveof 127.0.0.1 7000 

Slave2 的配置以下。

port 7002 daemonize yes // 守護進程 pidfile /var/run/redis-7002.pid // 給出 pid logfile 「7002.log」 // 日誌查詢 dir "/opt/redis/data" // 工做目錄 slaveof 127.0.0.1 7000 

2. Sentinel 監控主要配置

開啓了主從節點之後,咱們須要對 Sentinel 進行監控上的配置,見下面的配置參數。

port 端口號 dir "/opt/redis/data/" logfile "端口號.log" sentinel monitor mymaster 127.0.0.1 7000 2 sentinel down-after-millseconds mymaster 30000 sentinel parallel-syncs mymaster 1 sentinel failover-timeout mymaster 180000 

因爲須要配置多臺 Sentinel,從上面配置信息能夠看到,除了修改端口號,其餘配置都是相同的。重點看最後四個配置,這四個配置是 Sentinel 的核心配置。咱們分別來解釋一下這四個配置,斜槓後面的文字解釋了該參數的意義。

sentinel monitor mymaster 127.0.0.1 7000 2 // 監控的主節點的名字、IP 和端口,最後一個 2 表示有 2 臺 Sentinel 發現有問題時,就會發生故障轉移; sentinel down-after-millseconds mymaster 30000 // 這個是超時的時間。打個比方,當你去 ping 一個機器的時候,多長時間後仍 ping 不通,那麼就認爲它是有問題; sentinel parallel-syncs mymaster 1 // 指出 Sentinel 屬於併發仍是串行。1 表明每次只能複製一個,能夠減輕 Master 的壓力; sentinel failover-timeout mymaster 180000 // 表示故障轉移的時間。 

7.3 Sentinel 客戶端原理

咱們配置高可用的時候,若是隻是配置服務端的高可用是不夠的。若是客戶端感知不到服務端的高可用,是不會起做用的。因此,咱們不但要讓服務端高可用,還要讓客戶端也是高可用的。

咱們先來看下客戶端基本原理。

第一步,客戶端 Client 須要遍歷 Sentinel 節點集合,找到一個可用的 Sentinel 節點,同時須要獲取 Master 主機的 masterName。以下圖所示。

enter image description here

第二步,當客戶端找到 Sentinel-2 節點的時候,Client 會經過 get-master-addr-by-name 命令獲取 masterName,這個時候,Sentinel-2 會獲取真正的名稱和地址。以下圖所示。

enter image description here

第三步,Client 獲取到 Master 節點的時候,還會發出 role 或 role replication 命令,驗證是否是 Master 節點,Sentinel-2 會返回這個節點信息加以驗證。以下圖所示。

enter image description here

第四步,若是 Sentinel 感知到 Master 宕機了,這時 Sentinel 集羣應該是最早知道的。客戶端和 Sentinel 集羣之間實際上是發佈訂閱,客戶端 Client 去訂閱某個 Sentinel 的頻道,若是哪一個 Sentinel 發現 Master 發生了變化,Client 是會被通知到這個信息的,以下圖所示。可是要注意這個不是代理模式

enter image description here

總結一下,以上四步就是客戶端和 Sentinel 集羣的基本原理,任何客戶端原理都是按照這個流程作的,只是內部的封裝不一樣而已。

1. Jedis

咱們先經過使用率最高的 Java 的客戶端 Jedis 講起。

如何經過代碼實現 Sentinel 的訪問,讓咱們來看看代碼如何鏈接 Sentinel 的資源池。

JedisSentinelPool sentinelPool = new JedisSentinelPool(masterName,sentinelSet,poolConfig,timeout); //內部的本質仍是去鏈接 Master 主機,參數 masterName 表示 Master 名稱,sentinelSet 表示 Sentinel 集合,後面依次是 poolConfig 配置和超時時間 Jedis jedis = null; try{ //得到 redisSentinelPool 資源 jedis = redisSentinelPool.getResource(); //Jedis 相關的命令 }catch (Exception e){ logger.error(e.getMessage(),e); }finally{ if(jedis!=null){ jedis.close(); // Jedis 歸還 } } 

2. redis-py

接下來咱們再來看下如何使用 Python 連 Redis 客戶端的 Sentinel,和 Jedis 同樣,咱們將直接給出鏈接 Sentinel 的代碼。

from redis.sentinel import Sentinel sentinel = Sentinel([('localhost',26379),('localhost',26380),('localhost',26381)],socket_time=0.1) // 獲取可用的 Sentinel,並設置超時時間。 sentinel.discover_master('mymaster') // 獲取 Master 地址 >>> ('127.0.0.1',7000) sentinel.discover_slaves('mymaster') //獲取 Slave 地址 >>> [('127.0.0.1',7001),('127.0.0.1',7002)] 

7.4 Sentinel 實現原理

講完了 Sentinel 的代碼實現,不少人還不懂 Sentinel 的原理。接下來咱們就講解下它的實現原理,主要分爲如下三個步驟。

  • 檢測問題,主要講的是三個定時任務,這三個內部的執行任務能夠保證出現問題立刻讓 Sentinel 知道。

  • 發現問題,主要講的是主觀下線和客觀下線。當有一臺 Sentinel 機器發現問題時,將對它主觀下線,可是當多個 Sentinel 都發現有問題的時候,纔會出現客觀下線。

  • 找到解決問題的人,主要講的是領導者選舉,如何在 Sentinel 內部多臺節點中進行領導者選舉,選出一個領導者。

  • 解決問題,主要講得是如何進行故障轉移。

咱們分開進行闡述。

1. 三個定時任務

首先要講的是內部 Sentinel 會執行如下三個定時任務。

  • 每 10 秒每一個 Sentinel 對 Master 和 Slave 執行一次 Info Replication。
  • 每 2 秒每一個 Sentinel 經過 Master 節點的 Channel 交換信息(Pub/Sub)。
  • 每 1 秒每一個 Sentinel 對其餘 Sentinel 和 Redis 執行 Ping。

在這裏一一解釋下。

第一個定時任務,指的是 Redis Sentinel 能夠對 Redis 節點作失敗判斷和故障轉移,在 Redis 內部有三個定時任務做爲基礎,來 Info Replication 發現 Slave 節點,這個命令能夠肯定主從關係。

第二個定時任務,相似於發佈訂閱,Sentinel 會對主從關係進行斷定,經過 _sentinel_:hello 頻道交互。瞭解主從關係有助於更好地自動化操做 Redis。而後 Sentinel 會告知系統消息給其餘 Sentinel 節點,最終達到共識,同時 Sentinel 節點可以互相感知到對方。

第三個定時任務,指的是對每一個節點和其餘 Sentinel 進行心跳檢測,它是失敗斷定的依據。

2. 主觀下線和客觀下線

咱們先來回顧一下 Sentinel 的配置。

sentinel monitor mymaster 127.0.0.1 6379 3 //如不懂意思,請參見上面對 Sentinel 進行配置的說明;3 這個配置請記住,我將在後面講解。 sentinel down-after-milliseconds mymaster 3000 //Sentinel 會 Ping 每一個節點,若是超過 30 秒,依然沒有恢復的話,作下線的判斷。 

那麼什麼是主觀下線呢?

每一個 Sentinel 節點對 Redis 節點失敗存在「偏見」。之因此是偏見,只是由於某一臺機器 30 秒內沒有獲得回覆。

那麼如何作到客觀下線呢?

這個時候須要全部 Sentinel 節點都發現它 30 秒內無回覆,纔會達到共識。

3. 領導者選舉

Sentinel 集羣會採用領導者選舉的方式,完成 Sentinel 節點的故障轉移。經過 sentinel is-master-down-by-addr 命令都但願成爲領導者。

領導者選舉的步驟請見下:

  • 每一個作主觀下線的 Sentinel 節點向其餘節點發送命令,要求將它設置爲領導者;

  • 收到命令的 Sentinel 節點,若是沒有贊成經過其餘 Sentinel 節點發送的命令,那麼將贊成該要求,不然就會拒絕。

  • 若是 Sentinel 節點發現本身的票數已經超過 Sentinel 半數同時也超過 Sentinel monitor mymaster 127.0.0.1 6379 3 中的 3 個的時候,那麼它將成爲領導者;

  • 若是有多個 Sentinel 節點成爲領導者,那麼將等待一段時間後從新選舉。

這裏須要解釋一下爲何要從新選舉。由於若是有多個領導者,那麼哪一個節點能覆蓋更多的節點,纔會成爲真正的領導者,盲目成爲領導者,只會讓 Sentinel 效率低下,只有不斷確認保證最優的選舉,纔是高效的,固然這個過程是須要消耗時間的。

4. 故障轉移

故障轉移主要包括如下四個步驟:

  • 從 Slave 節點中選出一個「合適的」節點做爲新節點;

  • 對上面的節點執行 slaveof no one 命令讓其成爲 Master節點;

  • 向剩餘的 Salve 節點發送命令,讓它們成爲新的 Master 節點的 Slave節點,複製規則和同步參數;

  • 將原來 Master 節點更新配置爲 Slave 節點,並保持其「關注」。當其恢復後命令它去複製新的 Master 節點。

經過以上四步,就能得到「Master 斷掉 -> 選出新的 Master -> 同步 -> 舊 Master 恢復後成爲 Slave,同時同步新的 Master數據」這樣一整套的流程。

5. 如何選擇「合適的」Slave 節點

Redis 內部實際上是有一個優先級配置的,在配置文件中 slave-priority 這個參數是 Salve 節點的優先級配置,若是存在則返回,若是不存在則繼續。

當上面這個優先級不知足的時候,Redis 還會選擇複製偏移量最大的 Slave 節點,若是存在則返回,若是不存在則繼續。之因此選擇偏移量最大,這是由於偏移量越小,和 Master 的數據越接近,如今 Master掛掉了,說明這個偏移量小的機器數據也可能存在問題,這就是爲何要選偏移量最大的 Slave 的緣由。

若是發現偏移量都同樣,這個時候 Redis 會默認選擇 runid 最小的節點。

7.5 常見的開發運維的問題

對於 Sentinel 來講,平常是不須要作太多運維工做的,主要說的是如下兩點。

  • 節點運維:偏運維,例如對 Master 和 Slave 節點的上下限進行操做。

  • 高可用讀寫分離:偏開發,開發人員會思考是否能有更加好用的方式。例如用高可用的讀寫分離。

咱們分別來說解一下這兩個問題。

1. 節點運維

節點運維包括主節點、從節點和 Sentinel 節點的運維。

首先是機器下線問題,如過保等狀況。

其次是機器性能不足問題,如 CPU、內存、硬盤、網絡等硬件。

最後是節點自身故障,如服務器不穩定,可能由於系統、硬件等未知緣由,這個時候咱們只能對它進行下線,轉移至其餘機器上。

  • 主節點

主節點的節點運維,主要是經過如下命令,對主節點作故障轉移。

sentinel failover <masterName> 
  • 從節點

對於從節點,咱們要區別是永久下線仍是臨時下線。例如是否作一些清理工做(如 RDB、AOF 文件的清理),但要考慮一下讀寫分離的狀況。

咱們再來看一下節點上線,咱們須要把某臺 Slave 晉升爲主節點,就須要 Sentinel failover 進行替換;對於從節點的上線,咱們只須要執行 slaveof 就能夠了,Sentinel 節點會根據命令自動感知;對於 Sentinel 節點,咱們只須要參考其餘 Sentinel 節點啓動就能夠。

2. 高可用讀寫分離

你們知道,從節點是高可用的基礎,它的擴展功能是讀的能力。咱們先來看下面這張高可用讀寫分離使用以前的圖。

enter image description here

如上圖所示,Sentinel 實際上是對 Master 作故障轉移,對 Slave 只有下線的操做。Sentinel 集羣是不會對 Slave 作故障轉移的。那麼咱們應該怎麼作呢?

其實咱們須要使用一個客戶端去監控 Slave,和 Master 相似。主要運用如下命令。

  • switch-master:這個命令用來切換主節點,從節點晉升爲主節點的操做;
  • covert-to-slave:切換從節點,原來主節點須要降爲從節點的時候使用該命令。
  • sdown:這個命令在主觀下線時使用。

咱們再來看看使用高可用讀寫分離以後的圖。

enter image description here

如圖所示,和第一張不一樣的是,咱們把 Slave 的機器所有作到同一個資源池中,讓客戶端每次都是訪問這個 Slave 資源池。

可是這種高可用讀寫分離在實際應用場景中不多使用,主要由於這種高可用讀寫分離太過複雜,配置參數比較多。那怎麼辦呢?

當咱們在實際運維中真正須要可擴展的時候,Redis 其實給咱們提供了集羣的模式

咱們在使用 Redis 的時候,常常是會遇到一些問題。好比高可用問題、容量問題、併發性能問題等。因而開發者考慮能不能像服務器同樣,當一臺機器不夠的時候,咱們用多臺機器造成 Redis Cluster 集羣呢?

在 Redis 團隊的努力下,終於作出了一套解決方案。這套解決方案有如下特色:

  • 去中心化。Redis Cluster 增長了1000個節點,性能隨着節點而線性擴展。
  • 管理簡單方便。可根據實際狀況去掉節點或者增長節點,移動分槽等。
  • 官方推薦。
  • 容易上手。

根據以上 Redis 集羣的特色,咱們將從如下七個方面對 Redis Cluster 進行講解:

  • 爲何要有集羣;
  • 如何進行數據分佈;
  • 如何搭建集羣;
  • 如何進行集羣的伸縮;
  • 如何使用客戶端去鏈接 redis-cluter;
  • 理解集羣原理;
  • 常見的開發運維的問題。

8.1 爲何要有集羣

首先是併發量,通常 QPS 到10萬每秒已經很是牛了,隨着公司業務的發展,或者當須要離散計算的時候,須要用到中間件緩存的時候,業務須要100萬每秒。這個時候,咱們就須要使用分佈式了。

其次是數據量,通常一個 Redis 的內存大約是16G~256G,假設咱們在一臺主從機器上配置了200G內存,可是業務需求是須要500G的時候,咱們首先不會經過升級硬件,而是經過分佈式。

咱們對併發量大和數據量劇增的時候,採起的最經常使用的手段就是加機器,對數據進行分區。作一個形象的比喻,咱們的數據量至關於貨物,當貨物只有不多一部分的時候,咱們能夠使用驢來拉貨;當貨物多起來了的時候,已經超出驢能拉的範圍,咱們能夠使用大象來拉貨;當貨物更多的時候,已經沒有更強壯的動物了,這個時候咱們能夠考慮使用多隻大象來拉貨。

分佈式就是一種採用某種規則對多臺機器管理的方式。採用分佈式咱們就是爲了節省費用。

8.2 如何進行數據分佈

什麼是數據分佈?數據分佈有兩種方式,順序分區和哈希分區。

1. 順序分佈

順序分佈就是把一整塊數據分散到不少機器中,以下圖所示。

enter image description here

順序分佈通常都是平均分配的。

2. 哈希分區

以下圖所示,1~100這整塊數字,經過 hash 的函數,取餘產生的數。這樣能夠保證這串數字充分的打散,也保證了均勻的分配到各臺機器上。

enter image description here

二者的區別,請見下表。

分佈 特色 特色
順序分佈 數據分散容易傾斜、鍵值業務相關、可順序訪問、支持批量 Big Table、HBase
哈希分佈 數據分散度高、鍵值分佈業務無關、支持批量、沒法順序訪問 一致性哈希、redis cluster、其餘緩存

由上表可知,哈希分佈和順序分佈只是場景上的適用。哈希分佈不能順序訪問,好比你想訪問1~100,哈希分佈只能遍歷所有數據,同時哈希分佈由於作了 hash 後致使與業務數據無關了。而順序分佈是會致使數據傾斜的,主要是訪問的傾斜。每次點擊會重點訪問某臺機器,這就致使最後數據都到這臺機器上了,這就是順序分佈最大的缺點。其餘的特色見表可知。

但哈希分佈實際上是有個問題的,當咱們要擴容機器的時候,專業上稱之爲「節點伸縮」,這個時候,由於是哈希算法,會致使數據遷移。在節點取餘的時候,遷移數量和添加的節點數是有關的,這個時候建議使用翻倍擴容。

這裏須要說明的是節點取餘究竟是什麼。

好比以前是三個節點,那麼如今加一個節點,就是四個節點。這個時候哈希算法就是從3的取餘變成了4的取餘,這個時候,原來的數字所在的位置確定是須要發生變化的,總體的數據基本上都是作了漂移。

總體的數據漂移實際上是有問題的,對數據庫的性能,硬件上都是考驗,因此爲了減小總體的數據漂移,咱們就須要對哈希算法有個一致性哈希算法

3. 一致性哈希

enter image description here

上圖就是一個一致性哈希的原理解析。假設咱們有 n1~n4 這四臺機器,咱們對每一臺機器分配一個惟一 token,每次有數據(圖中黃色表明數據),一致性哈希算法規定每次都順時針漂移數據,也就是圖中黃色的數據都指向 n3。這個時候咱們須要增長一個節點 n5,在 n2 和 n3 之間,數據仍是會發生漂移,可是這個時候你是否注意到,其實只有 n2~n3 這部分的數據被漂移,其餘的數據都是不會變的,這樣就實現了部分漂移,而沒有對全部數據進行漂移的弊端了。

最後咱們來介紹另一個哈希算法,就是 Redis Cluster 的哈希算法 —— 虛擬槽分區。咱們經過下面這張圖來解釋。

enter image description here

如上圖所示,咱們知道槽的範圍是0~16383,若是此時有五個節點,key 會經過 CRC16 哈希算法,對16383取餘,而後保存到 Redis Cluster 裏,Redis Cluster 會根據數據判斷是不是這個虛擬槽裏的數據,若是不是這個槽範圍的,因爲 Redis Cluster 數據是共享的,因而就會告知數據應該存到那臺機器上。

若是你對虛擬槽理解還有問題,這是正常的。下面咱們將經過講解 Redis Cluster 的基本架構、安裝使用讓你真正認識虛擬槽的概念。

8.3 基本架構

先來看看,什麼是分佈式架構。分佈式架構是一種彼此通信的架構,經過每一個節點之間負責對應的槽,每一個節點都負責讀寫。以下圖

enter image description here

那麼 Redis Cluster 是怎樣的架構呢?實際上咱們能夠經過安裝來了解 Redis Cluster 的架構。瞭解Redis Cluster 架構,咱們從瞭解節點、meet操做、指派槽、複製四個知識點開始。

1. 節點

Redis Cluster 是有不少節點的,每一個節點負責讀和寫。對某個節點進行配置文件設置,以下所示。

cluster-enabled:yes // 集羣模式來啓動 

2. meet 操做

咱們知道 Redis Cluster 是經過節點完成數據交換的,meet 操做則是這個過程的基礎。

enter image description here

上圖所示,A 和 C 之間有 meet 操做通信數據,A 和 B 之間也有 meet 操做通信,那麼 B 和 C 之間也就能夠相互通訊。只要某個節點和另外的節點可以正常讀寫,那麼任何節點之間其實也是能夠相互進行數據交換的。

能夠說,全部節點均可以共享消息。

3. 指派槽

咱們只有對節點進行指派某個槽,每一個 key 算出來的哈希值是否在某個槽內,它才能正常的讀寫。

enter image description here

如上圖所示,當咱們的 Redis Cluster 有三臺機器的時候,把0~16383的槽平均分配給每臺機器(Redis Cluster 制定給每一個槽分配16384個槽,也就是0~16383)。每當 key 訪問過來,Redis Cluster 會計算哈希值是否在這個區間裏。它們彼此都知道對應的槽在哪臺機器上。

客戶端只須要計算一個 key 的哈希值,而後傳給 Redis Cluster 就能夠了。

4. 複製

Redis Cluster 是一個主從複製的過程。因此在這裏就不解釋了。

8.4 安裝 Redis Cluster

安裝 Redis Cluster 有兩種方式,一種是原生安裝,一種是官方工具安裝。

1. 原生安裝

原生安裝首先是經過配置開啓節點;其次是經過 meet,實現節點間相互通信;再次是指配槽,只有經過指配槽才能實現客戶端數據的基本訪問;最後是主從配置,纔可以實現故障轉移。

  • 配置開啓 Redis

代碼以下。

port 端口
daemonize yes // 守護進程方式啓動 dir "/opt/redis/data/" // 目錄 dbfilename "dump-3339.rdb" // 端口來區分 logfile "3339.log" //日誌文件,使用端口區分 cluster-enabled yes //表明開啓cluster cluster-config-file nodes-3339.conf //clstuer開啓各個節點的配置。 

配置完之後能夠使用下面的命令,分別開啓每個 cluster:

redis-server redis-端口.conf 

可是以上命令是相互獨立的。下面咱們進行 meet 操做:

redis-cli -h 127.0.0.1 -p 7000 cluster meet 127.0.0.1 7001 

根據上面的命令自動感知兩臺,而後分別用上面的命令都和7000這個端口進行 meet 操做便可所有實現 meet 配置。

  • Cluster 節點主要配置

配置代碼以下。

cluster-enabled yes // 使用cluster cluster-node-timeout 15000 // 默認超時配置 cluster-config-file "nodes.conf" //集羣節點,端口來區分 cluster-require-full-coverage no //若是有一個節點壞掉了,對外就不提供服務了。默認是yes,必須配置爲no。 
  • 分配槽

咱們須要爲每一個端口配置對於的分配槽,代碼以下。

redis-cli -h 127.0.0.1 -p 7000 cluster addslots {0...5461}
redis-cli -h 127.0.0.1 -p 7001 cluster addslots {5462...10922}
redis-cli -h 127.0.0.1 -p 7001 cluster addslots {10923...16383}

根據有多少個端口,就分配好對應的槽就能夠了。記住這個範圍是0~16383。

  • 設置主從

爲了完成故障轉移,就必須進行主從配置,代碼以下。

redis-cli -h 127.0.0.1 -p 7001 cluster replicate ${nodeid-7000} 

這裏注意 nodeid 和咱們單機講解的 runid 是不一樣的,runid 重啓之後是會改變的,可是 nodeid 卻不會。關於如何獲取 nodeid,咱們將會在後續進行講解。

2. 官方工具安裝

Redis 官方給咱們提供了官方安裝工具,主要是經過 Ruby 安裝。

  • 安裝 Ruby

下載對應的 Ruby,代碼以下。

wget https://cache.ruby-lang.org/ruby/2.3/ruby-2.3.1.tar.gz 

安裝 Ruby,代碼以下。

tar -xvf ruby-2.3.1.tar.gz
./configure -prefix=/usr/local/ruby //編譯
make
make install //安裝 cd /usr/local/ruby cp bin/ruby /usr/local/bin //拷貝到此路徑 cp bin/gem /usr/local/bin //拷貝到此路徑 
  • 安裝 Rubygem Redis

下載 Ruby 的 gem,代碼以下。

wget http://rubygems.org/downloads/redis-3.3.0.gem 

安裝,代碼以下。

gem install -l redis-3.3.0.gem gem list --check redis gem 
  • 安裝 redis-trib.rb

代碼以下。

cp ${redis_home}/src/redis-trib.rb /usr/local/bin 

經過複製就能夠把 redis-trib.rb 文件拷貝到 bin 文件下,就能夠使用了。

8.5 總結

最後,對本文的內容作下總結,主要有如下三點。

  • 使用原生命令安裝,理解 Redis Cluster 的架構。可是生產環境中不使用。
  • 官方工具安裝是很是簡單和高效的。其實在生產環境中會使用腳本,會更加簡單高效準確。通常生產環境都會使用。
  • 其餘部署還能夠經過可視化部署,可是隻存在少數企業中,大部分仍是在使用官方工具安裝的。

論如下七個問題。

  1. 緩存收益與成本的問題
  2. 緩存更新的策略
  3. 緩存顆粒的控制
  4. 緩存穿透的優化
  5. 無底洞問題的優化
  6. 緩存雪崩的優化
  7. 熱點key的重建優化

9.1 緩存收益與成本的問題

關於緩存收益與成本主要分爲三個方面的講解,第一個是什麼是收益;第二個是什麼是成本;第三個是有哪些使用場景。

1. 收益

主要有如下兩大收益。

  • 加速讀寫:經過緩存加速讀寫,如 CPU L1/L2/L3 的緩存、Linux Page Cache 的讀寫、遊覽器緩存、Ehchache 緩存數據庫結果。
  • 下降後端負載:後端服務器經過前端緩存來下降負載,業務端使用 Redis 來下降後端 MySQL 等數據庫的負載。

2. 成本

產生的成本主要有如下三項。

  • 數據不一致:這是由於緩存層和數據層有時間窗口是不一致的,這和更新策略有關的。
  • 代碼維護成本:這裏多了一層緩存邏輯,顧會增長成本。
  • 運維費用的成本:如 Redis Cluster,甚至是如今最流行的各類雲,都是費用的成本了。

3. 使用場景

使用場景主要有如下三種。

  • 下降後端負載:這是對高消耗的 SQL,join 結果集和分組統計結果緩存。
  • 加速請求響應:這是利用 Redis 或者 Memcache 優化 IO 響應時間。
  • 大量寫合併爲批量寫:好比一些計數器先 Redis 累加之後再批量寫入數據庫。

9.2 緩存的更新策略

主要有如下三種策略。

  • LRU、LFU、FIFO 算法策略。例如 maxmemory-policy,這是最大內存的策略,當 maxmemory 最大時,會優先刪除過時數據。咱們在控制最大內存,讓它幫咱們去刪除數據。
  • 過時時間剔除,例如 expire。設置過時時間能夠保證其性能,若是用戶更新了重要信息,應該怎麼辦。因此這個時候就不適用了。
  • 主動更新,例如開發控制生命週期。

這三個策略中,一致性最好的就是主動更新。可以根據代碼實時的更新數據,可是維護成本也是最高的;算法剔除和超時剔除一致性都作的不夠好,可是維護成本卻很是低。

根據緩存的使用場景,咱們會採用不一樣的更新策略。

實際開發中我給你們如下兩個建議。

  • 低一致性:最大內存和淘汰策略,數據庫有些數據是不須要立刻更新的,這個時候就能夠用低一致性來操做。
  • 高一致性:超時剔除和主動更新的結合,最大內存和淘汰策略兜底。你沒辦法確保內存不會增長,從而使服務不可用了。

9.3 緩存粒度問題

咱們知道,用戶第一次訪問客戶端,客戶端訪問 Redis 確定是沒有的,這個時候只能從數據庫 DB 那裏獲取信息,代碼以下。

select * from t_teacher where id= {id} 

在 Redis 設置用戶信息緩存,代碼以下。

set teacher:{id} select * from t_teacher where id= {id} 

這個時候咱們來看看緩存粒度問題。

由於咱們要更新所有屬性。到底咱們是採用 select * 仍是僅僅只是更新你須要更新的那些字段呢?以下兩段代碼。

set key1 = ? from select * from t_teacher 
set key1 = ? from select key1 from t_teacher 

緩存粒度控制能夠從如下三個角度來觀察,經過這三點來決定如何選擇。

  • 通用性:全量屬性更好。上面一個對比 * 和某個字段的查詢,最好是經過全量屬性,這樣的話,select * 具備很好的通用性,由於若是你 select 某個字段的話,將來若是一旦業務改變,代碼也要隨之改變才能夠。
  • 佔用空間:部分屬性會更好。由於這樣佔用的空間是最小的。
  • 代碼維護上:表面上全量屬性會更好。咱們真的須要全量嗎?其實咱們在使用緩存的時候,優先考慮的是內存而不僅僅只是保證代碼的擴展性。

9.4 緩存穿透問題

首先你們看下下面這張圖。

enter image description here

當請求發送給服務器的時候,緩存找不到,而後都堆到數據庫裏。這個時候,緩存至關於穿透了,不起做用了。

緣由有兩點:

  • 業務代碼自身的問題。不少實際開發的時候,若是是一個不熟練的程序員,因爲缺少必要的大數據的意識,不少代碼在第一次寫的時候是 OK 的,可是當須要修改業務代碼的時候,經常會出現問題。
  • 惡意攻擊和爬蟲問題。網絡上充斥着各類攻擊和各類爬蟲模仿着人爲請求來訪問你的數據。若是惡意訪問穿透你的數據庫,將會致使你的服務器瞬間產生大量的請求致使服務停止。

那咱們去如何發現這些問題呢?

  • 業務的相應時間:通常請求的時間都是穩定的,可是若是出現相似穿透現象,必然在短期內有一個體現。
  • 業務自己的問題。產品的功能出現問題。
  • 對緩存層命中數、存儲層的命中數這些值的採集。

1. 解決方案一:緩存空對象

當緩存中不存在,訪問數據庫的時候,又找不到數據,須要設置給 cache 的值爲 null,這樣下次再次訪問該 id 的時候,就會直接訪問緩存中的 null 了。

可是可能存在的兩個問題。首先是須要更多的鍵,可是若是這個量很是大的話,對業務也是有影響的,因此須要設置過時時間;其次是緩存層和存儲層數據「短時間」不一致。當緩存層過時時間到了之後,可能會產生和存儲層數據不一致的狀況。這個時候須要使用一些消息隊列等方式,來確保這個值的一致性。

下面的代碼用 Java 來實現簡單的緩存空對象。

public String getCacheThrough(String key){ String cacheValue = cache.get(key); if(StringUtils.isBlank(cacheValue)){ // 如存儲數據爲空 String storageValue = storage.get(key); cache.set(key,storageValue);//須要設置一個過時時間 if(StringUtils.isBlank(strageValue){ cache.expire(key.60*10); } return storageValue; }else{ return cacheValue; } } 

2. 解決方案二:布隆過濾器攔截

布隆過濾器,其實是一個很長的二進制向量和一系列隨機映射函數。布隆過濾器能夠用於檢索一個元素是否在一個集合中。它的優勢是空間效率和查詢時間都遠遠超過通常的算法,缺點是有必定的誤識別率和刪除困難。

相似於一個字典,你查詞典的時候不須要把全部單詞都翻一遍,而是經過目錄的方式,用戶經過檢索的形式在極小內存中能夠找到對應的內容。

雖然布隆過濾器能夠經過極小的內存來存儲,可是免不了須要一部分代碼來維護這個布隆過濾器,而且常常須要根據規則來調整,在選取是否使用布隆過濾器,還須要經過場景來選取。

9.5 無底洞問題優化

無底洞問題就是即便加機器,性能卻沒有提高,反而下降了。到底這是怎麼回事呢?先看下面的圖。

enter image description here

當客戶端增長一個緩存的時候,只須要 mget 一次,可是若是增長到三臺緩存,這個時候則須要 mget 三次了,每增長一臺,客戶端都須要作一次新的 mget,給服務器形成性能上的壓力。

同時,mget 須要等待最慢的一臺機器操做完成才能算是完成了 mget 操做。這仍是並行的設計,若是是串行的設計就更加慢了。

經過上面這個實例能夠總結出:更多的機器!=更高的性能

可是並非沒辦法,通常在優化 IO 的時候能夠採用如下幾個方法。

  • 命令的優化。例如慢查下 keys、hgetall bigkey。
  • 咱們須要減小網絡通信的次數。這個優化在實際應用中使用次數是最多的,咱們儘可能減小通信次數。
  • 下降接入成本。好比使用客戶端長鏈接或者鏈接池、NIO 等等。

1. 四種批量優化的方法

四種方法主要是:串行 mget、串行 IO、並行 IO、hash_tag。

  • 串行 mget

以下圖所示,串行 mget 就是根據 Redis 增長的臺數,來 mget 屢次網絡時間。

enter image description here

  • 串行 IO

以下圖所示,根據 key 的增長,先在客戶端組裝成各類 subkeys,而後一次性根據 pipeline 方式進行傳輸,這樣能有效的減小網絡時間。

enter image description here

  • 並行 IO

以下圖所示,在串行 IO 的基礎上,再根據並行打包,把請求一次性的傳給 Redis 集羣。

enter image description here

  • hash_tag

以下圖所示,用最極端的方式進行哈希傳送給 Redis 集羣。

enter image description here

總之,實際使用過程當中,咱們根據特定的業務場景,選定對應的批量優化方式,能夠有效的優化。

9.7 熱點 Key 重建優化

咱們知道,使用緩存,若是獲取不到,纔會去數據庫裏獲取。可是若是是熱點 key,訪問量很是的大,數據庫在重建緩存的時候,會出現不少線程同時重建的狀況。

enter image description here

如上圖,就是由於高併發致使的大量熱點的 key 在重建還沒完成的時候,不斷被重建緩存的過程,因爲大量線程都去作重建緩存工做,致使服務器拖慢的狀況。只有最後一個是重建完成,命中緩存。

爲了解決以上的問題,咱們着重研究了三個目標和兩個解決方案。

三個目標爲:

  • 減小重建緩存的次數;
  • 數據儘量保持一致;
  • 減小潛在的風險。

兩個解決方案爲

  • 互斥鎖
  • 永不過時

咱們根據三個目標,解釋一下兩個解決方案。

1. 互斥鎖(mutex key)

由下圖所示,第一次獲取緩存的時候,加一個鎖,而後查詢數據庫,接着是重建緩存。這個時候,另一個請求又過來獲取緩存,發現有個鎖,這個時候就去等待,以後都是一次等待的過程,直到重建完成之後,鎖解除後再次獲取緩存命中。

enter image description here

那麼這個過程是怎麼作到的呢?請見下面代碼演示。

public String getKey(String key){ String value = redis.get(key); if(value == null){ String mutexKey = "mutex:key:"+key; //設置互斥鎖的key if(redis.set(mutexKey,"1","ex 180","nx")){ //給這個key上一把鎖,ex表示只有一個線程能執行,過時時間爲180秒 value = db.get(key); redis.set(key,value); redis.delete(mutexKety); }else{ // 其餘的線程休息100毫秒後重試 Thread.sleep(100); getKey(key); } } return value; } 

可是互斥鎖也有必定的問題,就是大量線程在等待的問題。下面咱們就來說一下永遠不過時

2. 永遠不過時

首先在緩存層面,並無設置過時時間(過時時間使用 expire 命令)。可是功能層面,咱們爲每一個 value 添加邏輯過時時間,當發現超過邏輯過時時間後,會使用單獨的線程去構建緩存。這樣的好處就是不須要線程的等待過程。見下圖。

enter image description here

如上圖所示,T1 時間無需等待,直接輸出,到 T2 的時候,發現 value 已經到了過時時間,因而就開始構建緩存,仍是輸出舊值。到了 T3 已是舊值,直到 T4 時間,構建緩存已經完成,直接輸出新值。

這樣就避免了上面互斥鎖大量線程等待的問題。具體實現僞代碼以下:

public String getKey(final String key){ V v = redis.get(key); String value = v.getValue(); long logicTimeout = v.getLogicTimeout(); if(logicTimeout >= System.currentTimeMillis()){ String mutexKey = "mutex:key:"+key; //設置互斥鎖的key if(redis.set(mutexKey,"1","ex 180","nx")){ //給這個key上一把鎖,ex表示只有一個線程能執行,過時時間爲180秒 threadPool.execute(new Runable(){ public void run(){ String dbValue = db.getKey(key); redis.set(key,(dbValue,newLogicTimeout));//緩存重建,須要一個新的過時時間 redis.delete(keyMutex); //刪除互斥鎖 } }; } } } 

互斥鎖的優勢是思路很是簡單,具備一致性,其缺點是代碼複雜度高,存在死鎖的可能性。

永不過時的優勢是基本杜絕 key 的重建問題,但缺點是不保證一致性,邏輯過時時間增長了維護成本和內存成本。

相關文章
相關標籤/搜索