《Redis開發與運維》

第1章 初識Redis

1. Redis介紹:

  Redis是一種基於鍵值對(key-value)的NoSQL數據庫java

  與不少鍵值對數據庫不一樣的是,Redis中的值能夠是由string(字符串)、hash(哈希)、list(列表)、set(集合)、zset(有序集合等多種數據結構和算法組成,所以Redis能夠知足不少的應用場景。linux

  並且由於Redis會將全部數據都存放在內存中,因此它的讀寫性能很是驚人redis

  不只如此,Redis還能夠將內存的數據利用快照(RDB)和日誌(AOF)的形式保存到硬盤上,這樣在發生相似斷電或者機器故障的時候,內存中的數據不會「丟失」。算法

2. Redis特性:

(1)速度快。速度快的緣由:數據庫

  • Redis的全部數據都是存放在內存中的,這是Redis速度快的最主要緣由。
  • Redis是用C語言實現的,通常來講C語言實現的程序「距離」操做系統更近,執行速度相對會更快。
  • Redis使用了單線程架構,預防了多線程可能產生的競爭問題。

(2)基於鍵值對的數據結構服務器。編程

  與不少鍵值對數據庫不一樣的是,Redis中的值不只能夠是字符串,並且還能夠是具體的數據結構,它主要提供了5種數據結構:字符串、哈希、列表、集合、有序集合。這樣不只能便於在許多應用場景的開發,同時也可以提升開發效率。後端

(3)簡單穩定數組

  • 首先,Redis的源碼不多。
  • 其次,Redis使用單線程模型,這樣不只使得Redis服務端處理模型變得簡單,並且也使得客戶端開發變得簡單
  • 最後,Redis不須要依賴於操做系統中的類庫(例如Memcache須要依賴libevent這樣的系統類庫),Redis本身實現了事件處理的相關功能

(4)持久化瀏覽器

  一般看,將數據放在內存中是不安全的,一旦發生斷電或者機器故障,重要的數據可能就會丟失,所以Redis提供了兩種持久化方式:RDB和AOF,便可以用兩種策略將內存的數據保存到硬盤中(如圖1-1所示),這樣就保證了數據的可持久性。緩存

(5)主從複製

   Redis提供了複製功能,實現了多個相同數據的Redis副本。

(6)高可用和分佈式

  Redis從2.8版本正式提供了高可用實現Redis Sentinel(哨兵模式),它可以保證Redis節點的故障發現和故障自動轉移。

  Redis從3.0版本正式提供了分佈式實現Redis Cluster(集羣模式),它是Redis真正的分佈式實現,提供了高可用、讀寫和容量的擴展性。

3. Redis使用場景:

  (1)緩存。合理地使用緩存不只能夠加快數據的訪問速度,並且可以有效地下降後端數據源的壓力。

  (2)排行榜系統。Redis提供了列表和有序集合數據結構,合理地使用這些數據結構能夠很方便地構建各類排行榜系統。

  (3)計數器應用。Redis自然支持計數功能並且計數的性能也很是好。

  (4)社交網絡。贊/踩、粉絲、共同好友/喜愛、推送、下拉刷新等是社交網站的必備功能,因爲社交網站訪問量一般比較大,並且傳統的關係型數據不太適合保存這種類型的數據,Redis提供的數據結構能夠相對比較容易地實現這些功能。

  (5)消息隊列系統。消息隊列系統能夠說是一個大型網站的必備基礎組件,由於其具備業務解耦、非實時業務削峯等特性。Redis提供了發佈訂閱功能和阻塞隊列的功能,雖然和專業的消息隊列比還不夠足夠強大,可是對於通常的消息隊列功能基本能夠知足。

4. 在Linux系統上安裝Redis

  第1步:將redis的源碼包上傳到linux系統。

      Alt+p打開sftp窗口:輸入put "F:/java/ziyuan/redis-3.0.0.tar.gz"

  第2步:解壓:tar -zxvf redis-3.0.0.tar.gz

  第3步:進行編譯。 cd到解壓後的目錄 輸入命令:make  

  第4步:進行安裝。 輸入命令:make install PREFIX=/usr/local/redis 

啓動:redis-server (加上配置文件)      [root@localhost bin]# ./redis-server redis.conf

鏈接Redis服務:redis-cli       [root@localhost bin]# ./redis-cli

中止Redes服務:redis-cli shutdown       [root@localhost bin]# ./redis-cli shutdown

 第2章 API的理解和使用

2.1 預備

2.1.1 全局命令:

keys *       :將全部的鍵都輸出

dbsize      :輸出鍵總數

exits key  :檢查某個鍵是否存在,若是存在返回1,不存在返回0

del key     :刪除某個鍵

expire key 時間 :爲某個鍵設置過時時間

ttl key       :觀察某鍵的剩餘過時時間

type key   :返回某鍵的數據結構類型,若是鍵不存在返回none

2.1.2 數據結構與內部編碼:

  type命令實際返回的就是當前鍵的數據結構類型,它們分別是:string(字符串)、hash(哈希)、list(列表)、set(集合)、zset(有序集合),但這些只是Redis對外的數據結構。

  實際上每種數據結構都有本身底層的內部編碼實現,並且是多種實現,這樣Redis會在合適的場景選擇合適的內部編碼。

  多種內部編碼實現能夠在不一樣場景下發揮各自的優點,例如ziplist比較節省內存,可是在列表元素比較多的狀況下,性能會有所降低,這時候Redis會根據配置選項將列表類型的內部實現轉換爲linkedlist。

2.1.3 單線程架構:

(1)單線程模型: 

  Redis使用了單線程架構I/O多路複用模型來實現高性能的內存數據庫服務

  由於Redis是單線程來處理命令的,因此一條命令從客戶端達到服務端不會馬上被執行,全部命令都會進入一個隊列中,而後逐個被執行。因此假若有多個客戶端命令,則這些命令的執行順序是不肯定的,可是能夠肯定不會有兩條命令被同時執行。

  可是像發送命令、返回結果、命令排隊確定不像描述的這麼簡單,Redis使用了I/O多路複用技術來解決I/O的問題。

(2)爲何單線程號還能這麼快?

爲何Redis使用單線程模型會達到每秒萬級別的處理能力呢?能夠將其歸結爲三點:

  第一,純內存訪問Redis將全部數據放在內存中,內存的響應時長大約爲100納秒,這是Redis達到每秒萬級別訪問的重要基礎

  第二,非阻塞I/ORedis使用epoll做爲I/O多路複用技術的實現,再加上Redis自身的事件處理模型將epoll中的鏈接、讀寫、關閉都轉換爲事件,不在網絡I/O上浪費過多的時間

  第三,單線程避免了線程切換和競態產生的消耗

2.2. 五種數據類型

2.2.1 字符串String

  字符串類型的值實際能夠是字符串(簡單的字符串、複雜的字符串(例如JSON、XML))、數字(整數、浮點數),甚至是二進制(圖片、音頻、視頻),可是值最大不能超過512MB。

一、命令

  • 設置值:set key value
  • 獲取值:get key
  • 批量設置值:mset key value key value ...  例如:mset  a 1 b 2 c 3
  • 批量獲取值:mset key key ...  例如:mset  a b c
  • 計數:incr key(自增)、decr key(自減)、incrby key number(自增指定數字)、decrby key number(自減指定數字)

二、字符串類型的內部編碼有3種

  • int:8個字節的長整型。
  • embstr:小於等於39個字節的字符串。
  • raw:大於39個字節的字符串。

  Redis會根據當前值的類型和長度決定使用哪一種內部編碼實現

三、典型使用場景

(1)緩存功能

  下圖是比較典型的緩存使用場景,其中Redis做爲緩存層,MySQL做爲存儲層,絕大部分請求的數據都是從Redis中獲取。因爲Redis具備支撐高併發的特性,因此緩存一般能起到加速讀寫和下降後端壓力的做用。

  首先從Redis中獲取用戶信息(僞代碼):

  

  若是沒有從Redis獲取到用戶信息,須要從MySQL中進行獲取,並將結果回寫到Redis,添加1小時(3600秒)過時時間:(僞代碼)

  

(2)計數

   例如使用Redis做爲視頻播放數計數的基礎組件,用戶每播放一次視頻,相應的視頻播放數就會自增1:

  

(3)共享sessio

  一個分佈式Web服務將用戶的Session信息(例如用戶登陸信息)保存在各自服務器中,這樣會形成一個問題,出於負載均衡的考慮,分佈式服務會將用戶的訪問均衡到不一樣服務器上,用戶刷新一次訪問可能會發現須要從新登陸,這個問題是用戶沒法容忍的。

  爲了解決這個問題,可使用Redis將用戶的Session進行集中管理,以下圖所示,在這種模式下只要保證Redis是高可用和擴展性的,每次用戶更新或者查詢登陸信息都直接從Redis中集中獲取。

(4)限速

   不少應用出於安全的考慮,會在每次進行登陸時,讓用戶輸入手機驗證碼,從而肯定是不是用戶本人。可是爲了短信接口不被頻繁訪問,會限制用戶每分鐘獲取驗證碼的頻率,例如一分鐘不能超過5次。此功能可使用Redis來實現,下面的僞代碼給出了基本實現思路:

  

2.2.2  哈希Hash

  

 一、命令:

  • 設置值:hset key field value  例:爲user:1 添加一對field-value:hset user:1 name tom
  • 獲取值:hget key field  例hget user:1 name
  • 刪除field:hdel key field [field ...] (能夠同時刪除多個)
  • 計算field的個數:hlen key
  • 批量設置或獲取field-value:hmset key field value [field value ...]  hmget key field [field ...]
  • 判斷field是否存在:hexits key field
  • 獲取全部field:hkeys key
  • 獲取全部value:hvals key
  • 獲取全部field-value:hgetall key
  • field自增:hincrby
  • 計算value的字符串長度:hstrlen key field

 二、哈希類型的內部編碼:

  哈希類型的內部編碼有兩種:

  ziplist(壓縮列表):當哈希類型元素個數小於hash-max-ziplist-entries配置(默認512個)、同時全部值都小於hash-max-ziplist-value配置(默認64字節)時,Redis會使用ziplist做爲哈希的內部實現,ziplist使用更加緊湊的結構實現多個元素的連續存儲,因此在節省內存方面比hashtable更加優秀。

  hashtable(哈希表):當哈希類型沒法知足ziplist的條件時,Redis會使用hashtable做爲哈希的內部實現,由於此時ziplist的讀寫效率會降低,而hashtable的讀寫時間複雜度爲O(1)。

2.2.3  列表List

   列表(list)類型是用來存儲多個有序的字符串.

一、命令:

  • 從右邊插入元素:rpush key value [value ...]
  • 從左邊插入元素:lpush key value [value ...]
  • 向某個元素(pivot)前或者後插入元素:linsert key before | after pivot value
  • 查找指定範圍內的元素列表:lrange key start end  例lrange listket 0 -1 查找所有元素
  • 獲取列表指定索引下標的元素:lindex key index
  • 獲取列表長度:llen key
  • 從列表左側彈出元素:lpop key
  • 從列表左側彈出元素:rpop key刪除指定元素:lrem key count value (lrem命令會從列表中找到等於value的元素進行刪除,根據count的不一樣分爲三種狀況:count>0,從左到右,刪除最多count個元素;count<0,從右到左,刪除最多count絕對值個元素;count=0,刪除全部)
  • 修改:lset key index newvalue
  • 阻塞彈出:blpop key [key ...] timeout

二、內部編碼
  列表類型的內部編碼有兩種:

  • ziplist(壓縮列表):當列表的元素個數小於list-max-ziplist-entries配置(默認512個),同時列表中每一個元素的值都小於list-max-ziplist-value配置時默認64字節),Redis會選用ziplist來做爲列表的內部實現來減小內存的使用。
  • inkedlist(鏈表):當列表類型沒法知足ziplist的條件時,Redis會使用linkedlist做爲列表的內部實現。

 三、使用場景

 (1)消息隊列

  Redis的lpush+brpop命令組合便可實現阻塞隊列,生產者客戶端使用lrpush從列表左側插入元素,多個消費者客戶端使用brpop命令阻塞式的「搶」列表尾部的元素,多個客戶端保證了消費的負載均衡和高可用性。

 (2)文章列表

   每一個用戶有屬於本身的文章列表,現須要分頁展現文章列表。此時能夠考慮使用列表,由於列表不可是有序的,同時支持按照索引範圍獲取元素。

 實際上列表的使用場景不少,在選擇時能夠參考如下口訣:

  • lpush+lpop=Stack(棧)
  • lpush+rpop=Queue(隊列)
  • lpsh+ltrim=Capped Collection(有限集合)
  • lpush+brpop=Message Queue(消息隊列)

2.2.4  集合Set

   集合(set)類型也是用來保存多個的字符串元素,但和列表類型不同的是,集合中不容許有重複元素,而且集合中的元素是無序的,不能經過索引下標獲取元素。

  一個集合最多能夠存儲2^32-1個元素。Redis除了支持集合內的增刪改查,同時還支持多個集合取交集、並集、差集,合理地使用好集合類型,能在實際開發中解決不少實際問題。

一、命令:

  • 添加元素:sadd key element [element ...] (返回結果爲添加成功的元素個數)
  • 刪除元素:srem key element [element ...] (返回結果爲成功刪除的元素個數)
  • 計算元素個數:scard key
  • 判斷元素是否在集合中:sismember key element
  • 隨機從集合返回指定個數元素:srandmember key [count] (count若是不寫默認爲1)
  • 從集合隨機彈出元素:spop key
  • 獲取全部元素:smembers key

二、內部編碼:

  • intset(整數集合):當集合中的元素都是整數且元素個數小於set-maxintset-entries配置(默認512個)時,Redis會選用intset來做爲集合的內部實現,從而減小內存的使用。
  • hashtable(哈希表):當集合類型沒法知足intset的條件時,Redis會使用hashtable做爲集合的內部實現。

三、使用場景:

  集合類型比較典型的使用場景是標籤(tag)。例如一個用戶可能對娛樂、體育比較感興趣,另外一個用戶可能對歷史、新聞比較感興趣,這些興趣點就是標籤。

  給用戶添加標籤:

  

2.2.5  有序集合zset

   它保留了集合不能有重複成員的特性,但不一樣的是,有序集合中的元素能夠排序。可是它和列表使用索引下標做爲排序依據不一樣的是,它給每一個元素設置一個分數(score)做爲排序的依據。

一、命令:

  • 添加成員:zadd key score member [score member ...]
  • 計算成員個數:zcard key
  • 計算某個成員的分數:zscore key member
  • 計算成員的排名:zrank key member
  • 刪除成員:zrem key member [member ...]
  • 增長成員的分數:zincrby key increment member
  • 返回指定排名範圍的成員:zrange key start end [withscores] (若是加上withscores選項,同時會返回成員的分數)

 二、內部編碼:

  • ziplist(壓縮列表):當有序集合的元素個數小於zset-max-ziplist-entries配置(默認128個),同時每一個元素的值都小於zset-max-ziplist-value配置(默認64字節)時,Redis會用ziplist來做爲有序集合的內部實現,ziplist能夠有效減小內存的使用。
  • skiplist(跳躍表):當ziplist條件不知足時,有序集合會使用skiplist做爲內部實現,由於此時ziplist的讀寫效率會降低。

三、使用場景:

  有序集合比較典型的使用場景就是排行榜系統。例如視頻網站須要對用戶上傳的視頻作排行榜,榜單的維度多是多個方面的:按照時間、按照播放數量、按照得到的贊數。本節使用贊數這個維度,記錄天天用戶上傳視頻的排行榜。主要須要實現如下4個功能:

添加用戶贊數:zadd和zincrby

取消用戶贊數:zrem

展現獲取贊數最多的十個用戶:zrevrange

展現用戶信息以及用戶分數:zscore和zrank

 2.3 數據庫管理

   Redis提供了幾個面向Redis數據庫的操做,它們分別是dbsize、select、flushdb/flushall命令。

(1) 切換數據庫:select dbIndex

   許多關係型數據庫,例如MySQL支持在一個實例下有多個數據庫存在的,可是與關係型數據庫用字符來區分不一樣數據庫名不一樣,Redis只是用數字做爲多個數據庫的實現。Redis默認配置中是有16個數據庫

  例:selet 15  切換到15號數據庫

能不能像使用測試數據庫和正式數據庫同樣,把正式的數據放在0號數據庫,測試的數據庫放在1號數據庫,那麼二者在數據上就不會彼此受影響了。事實真有那麼好嗎?

  Redis3.0中已經逐漸弱化這個功能,緣由:

  1. Redis是單線程的。若是使用多個數據庫,那麼這些數據庫仍然是使用一個CPU,彼此之間仍是會受到影響的。
  2. 多數據庫的使用方式,會讓調試和運維不一樣業務的數據庫變的困難,假若有一個慢查詢存在,依然會影響其餘數據庫,這樣會使得別的業務方定位問題很是的困難。
  3. 部分Redis的客戶端根本就不支持這種方式。即便支持,在開發的時候來回切換數字形式的數據庫,很容易弄亂。

  若是要使用多個數據庫功能,徹底能夠在一臺機器上部署多個Redis實例,彼此用端口來作區分,由於現代計算機或者服務器一般是有多個CPU的。這樣既保證了業務之間不會受到影響,又合理地使用了CPU資源。

 (2)flushdb/flushall

   flushdb/flushall命令用於清除數據庫,二者的區別的是flushdb只清除當前數據庫,flushall會清除全部數據庫

注意若是當前數據庫鍵值數量比較多,flushdb/flushall存在阻塞Redis的可能性。

第3章 小功能大用處

3.1 慢查詢分析

  許多存儲系統(例如MySQL)提供慢查詢日誌幫助開發和運維人員定位系統存在的慢操做。所謂慢查詢日誌就是系統在命令執行先後計算每條命令的執行時間,當超過預設閥值,就將這條命令的相關信息(例如:發生時間,耗時,命令的詳細信息)記錄下來,Redis也提供了相似的功能。如圖3-1所示,Redis客戶端執行一條命令分爲以下4個部分:

1)發送命令  2)命令排隊  3)命令執行  4)返回結果

  慢查詢的兩個配置參數:slowlog-log-slower-thanslowlog-max-len

  • slowlog-log-slower-than是預設閥值,它的單位是微秒,默認值是10000,假如執行了一條「很慢」的命令(例如keys*),若是它的執行時間超過了10000微秒,那麼它將被記錄在慢查詢日誌中。 
  • Redis使用了一個列表來存儲慢查詢日誌,slowlog-max-len就是列表的最大長度。一個新的命令知足慢查詢條件時被插入到這個列表中,當慢查詢日誌列表已處於其最大長度時,最先插入的一個命令將從列表中移出

   獲取慢查詢日誌:slow get

  獲取慢查詢日誌列表當前的長度:slowlog len

  慢查詢日誌重置:slowlog reset

 3.2 Redis Shell

   Redis提供了redis-cli、redis-server、redis-benchmark等Shell工具。

啓動:redis-server (加上配置文件)      [root@localhost bin]# ./redis-server redis.conf

鏈接Redis服務:redis-cli       [root@localhost bin]# ./redis-cli

中止Redes服務:redis-cli shutdown       [root@localhost bin]# ./redis-cli shutdown

redis-benchmark能夠爲Redis作基準性能測試:

  -c(clients)選項表明客戶端的併發數量(默認是50)

  -n(num)選項表明客戶端請求總量(默認是100000)

3.3 Pipeline

  Redis客戶端執行一條命令分爲以下四個過程:1)發送命令   2)命令排隊   3)命令執行   4)返回結果。     其中1)+4)稱爲RTT(往返時間)

  Redis提供了批量操做命令(例如mget、mset等),有效地節約RTT。但大部分命令是不支持批量操做的,例如要執行n次hgetall命令,並無mhgetall命令存在,須要消耗n次RTT。

  Pipeline(流水線)機制能將一組Redis命令進行組裝,經過一次RTT傳輸給Redis,再將這組Redis命令的執行結果按順序返回給客戶端

3.4 事務與Lua

3.4.1 事務

  爲了保證多條命令組合的原子性,Redis提供了簡單的事務功能以及集成Lua腳原本解決這個問題。

  事務表示一組動做,要麼所有執行,要麼所有不執行。例如在社交網站上用戶A關注了用戶B,那麼須要在用戶A的關注表中加入用戶B,而且在用戶B的粉絲表中添加用戶A,這兩個行爲要麼所有執行,要麼所有不執行,不然會出現數據不一致的狀況。

  Redis提供了簡單的事務功能,將一組須要一塊兒執行的命令放到multiexec兩個命令之間。multi命令表明事務開始,exec命令表明事務結束,它們之間的命令是原子順序執行的

  Redis提供了簡單的事務,之因此說它簡單,主要是由於它不支持事務中的回滾特性,同時沒法實現命令之間的邏輯關係計算。Lua腳本一樣能夠實現事務的相關功能,可是功能要強大不少。

3.4.2 Lua腳本

  Redis將Lua做爲腳本語言可幫助開發者定製本身的Redis命令。Lua語言提供了以下幾種數據類型:booleans(布爾)、numbers(數值)、strings(字符串)、tables(表格)。

  在Redis中執行Lua腳本有兩種方法:evalevalsha

  • eval 腳本內容 key個數 key列表 參數列表

例:eval 'return "hello" .. KEYS[1] .. ARGV[1]' 1 redis word    (此時KEYS[1]="redis",ARGV[1]="world",因此最終的返回結果是"hello redisworld"。)

若是Lua腳本較長,還可使用redis-cli--eval直接執行文件。

eval命令和--eval參數本質是同樣的,客戶端若是想執行Lua腳本,首先在客戶端編寫好Lua腳本代碼,而後把腳本做爲字符串發送給服務端,服務端會將執行結果返回給客戶端。

  • 除了使用eval,Redis還提供了evalsha命令來執行Lua腳本。以下圖所示,首先要將Lua腳本加載到Redis服務端,獲得該腳本的SHA1校驗和,evalsha命令使用SHA1做爲參數能夠直接執行對應Lua腳本,避免每次發送Lua腳本的開銷。這樣客戶端就不須要每次執行腳本內容,而腳本也會常駐在服務端,腳本功能獲得了複用。

Lua可使用redis.call函數實現對Redis的訪問,例以下面代碼是Lua使用redis.call調用了Redis的get操做:

  除此以外Lua還可使用redis.pcall函數實現對Redis的調用,redis.call和redis.pcall的不一樣在於,若是redis.call執行失敗,那麼腳本執行結束會直接返回錯誤,而redis.pcall會忽略錯誤繼續執行腳本,因此在實際開發中要根據具體的應用場景進行函數的選擇。

Lua腳本功能爲Redis開發和運維人員帶來以下三個好處:

  • Lua腳本在Redis中是原子執行的,執行過程當中間不會插入其餘命令。
  • Lua腳本能夠幫助開發和運維人員創造出本身定製的命令,並能夠將這些命令常駐在Redis內存中,實現複用的效果。
  • Lua腳本能夠將多條命令一次性打包,有效地減小網絡開銷。

 Redis提供了4個命令實現對Lua腳本的管理:

  • script load sript:此命令用於將Lua腳本加載到Redis內存中。
  • script exists sha1 [sha1 ...]:此命令用於判斷sha1是否已經加載到Redis內存中.
  • script flush:此命令用於清除Redis內存已經加載的全部Lua腳本。
  • script kill:此命令用於殺掉正在執行的Lua腳本。

3.5 Bitmaps

 

Redis提供了Bitmaps這個「數據結構」能夠實現對位的操做。把數據結構加上引號主要由於:

  • Bitmaps自己不是一種數據結構,實際上它就是字符串,可是它能夠對字符串的位進行操做
  • Bitmaps單獨提供了一套命令,因此在Redis中使用Bitmaps和使用字符串的方法不太相同。能夠把Bitmaps想象成一個以位爲單位的數組,數組的每一個單元只能存儲0和1,數組的下標在Bitmaps中叫作偏移量

下面說下Bitmaps的命令。假設將每一個獨立用戶是否訪問過網站存放在Bitmaps中,將訪問的用戶記作1,沒有訪問的用戶記作0,用偏移量做爲用戶的id:

(1)設置值:setbit key offset value設置鍵的第offset個位的值(從0算起)

  假設如今有20個用戶,userid=0,5,11,15,19的用戶對網站進行了訪問,那麼當前Bitmaps初始化結果以下圖所示:

  

(2)獲取值:getbit key offset 獲取鍵的第offset位的值(從0開始算))

(3)獲取Bitmaps指定範圍值爲1的個數:bitcount [start] [end]

(4)Bitmaps間的運算:bitop  and | or | not | xor destkey key [key ...]   (作多個Bitmaps的and(交集)、or(並集)、not(非)、xor(異或)操做並將結果保存在destkey中)

 

  假設網站有1億用戶,天天獨立訪問的用戶有5千萬,若是天天用集合類型和Bitmaps分別存儲活躍用戶,這種狀況下使用Bitmaps能節省不少的內存空間,尤爲是隨着時間推移節省的內存仍是很是可觀的。

  

  但假如該網站天天的獨立訪問用戶不多,例如只有10萬(大量的殭屍用戶),那麼二者的對好比下表所示,很顯然,這時候使用Bitmaps就不太合適了,由於基本上大部分位都是0。

  

3.6 發佈訂閱

   Redis提供了基於「發佈/訂閱」模式的消息機制,此種模式下,消息發佈者和訂閱者不進行直接通訊,發佈者客戶端向指定的頻道(channel)發佈消息,訂閱該頻道的每一個客戶端均可以收到該消息

 

命令:

  • 發佈消息:publish channel message ,返回結果爲訂閱者個數。
  • 訂閱消息:subscribe channel [channel ...]  ,訂閱者能夠訂閱一個或多個頻道。
    • 注意:1)客戶端在執行訂閱命令以後進入了訂閱狀態,只能接收subscribe、psubscribe、unsubscribe、punsubscribe的四個命令。2)·新開啓的訂閱客戶端,沒法收到該頻道以前的消息,由於Redis不會對發佈的消息進行持久化。
  •  取消訂閱:unsubscribe channel [channel ...]  
  • 查詢訂閱: 查看活躍的頻道:pubsub channels [pattern] 、 查看頻道訂閱數:pubsub numsub [channel ...] 、查看模式訂閱數:pubsub numpat

使用場景:

  聊天室、公告牌、服務之間利用消息解耦均可以使用發佈訂閱模式,下面以簡單的服務解耦進行說明。以下圖示,圖中有兩套業務,上面爲視頻管理系統,負責管理視頻信息;下面爲視頻服務面向客戶,用戶能夠經過各類客戶端(手機、瀏覽器、接口)獲取到視頻信息。

第4章 客戶端

   Redis是用單線程來處理多個客戶端的訪問,所以做爲Redis的開發和運維人員須要瞭解Redis服務端和客戶端的通訊協議,以及主流編程語言的Redis客戶端使用方法,同時還須要瞭解客戶端管理的相應API以及開發運維中可能遇到的問題。本章將對這些內容進行詳細分析,本章內容以下:

  • 客戶端通訊協議
  • Java客戶端Jedis
  • 客戶端管理
  • 客戶端常見異常
  • 客戶端案例分析

 4.1 客戶端通訊協議

  • 客戶端與服務端之間的通訊協議是在TCP協議之上構建的。
  • Redis制定了RESP(REdis Serialization Protocol,Redis序列化協議)實現客戶端與服務端的正常交互,這種協議簡單高效,既可以被機器解析,又容易被人類識別。

  例如客戶端發送一條set hello world命令給服務端,按照RESP的標準,客戶端須要將其封裝爲以下格式(每行用\r\n分隔):

  

  這樣Redis服務端可以按照RESP將其解析爲set hello world命令,執行後回覆的格式以下:+OK
  Redis的返回結果類型分爲如下五種:

  • 狀態回覆:在RESP中第一個字節爲"+"。
  • 錯誤回覆:在RESP中第一個字節爲"-"。
  • 整數回覆:在RESP中第一個字節爲""。
  • 字符串回覆:在RESP中第一個字節爲"$"。
  • 多條字符串回覆:在RESP中第一個字節爲"*"。

4.2 Java客戶端Jedis

Jedis屬於Java的第三方開發包,在Java中獲取第三方開發包一般有兩種方式:

  • 直接下載目標版本的Jedis-${version}.jar包加入到項目中。
  • 使用集成構建工具,例如maven、gradle等將Jedis目標版本的配置加入到項目中。

一般在實際項目中使用第二種方式,但若是隻是想測試一下Jedis,第一種方法也是能夠的。以Maven爲例子,在項目中加入下面的依賴便可:

<dependency>
    <groupId>redis.clients</groupId>
    <artifactId>jedis</artifactId>
    <version>2.8.2</version>
</dependency>

4.2.1  Jedis使用方法

//1. 生成一個Jedis對象,這個對象負責和指定Redis實例進行通訊。 初始化Jedis須要兩個參數:Redis實例的IP和端口
Jedis jedis = new Jedis("127.0.0.1", 6379);
//2. jedis執行set操做
jedis.set("hello", "world");
//3. jedis執行get操做, value="world"
String value = jedis.get("hello");

Jedis對於Redis五種數據結構的操做:

//-----------1.string------------
// 輸出結果:OK
jedis.set("hello", "world");
// 輸出結果:world
jedis.get("hello");
// 輸出結果:1
jedis.incr("counter");
//-----------2.hash---------------
jedis.hset("myhash", "f1", "v1");
jedis.hset("myhash", "f2", "v2");
// 輸出結果:{f1=v1, f2=v2}
jedis.hgetAll("myhash");
//-----------3.list---------------
jedis.rpush("mylist", "1");
jedis.rpush("mylist", "2");
jedis.rpush("mylist", "3");
// 輸出結果:[1, 2, 3]
jedis.lrange("mylist", 0, -1);
//-----------4.set----------------
jedis.sadd("myset", "a");
jedis.sadd("myset", "b");
jedis.sadd("myset", "a");
// 輸出結果:[b, a]
jedis.smembers("myset");
//------------5.zset----------------
jedis.zadd("myzset", 99, "tom");
jedis.zadd("myzset", 66, "peter");
jedis.zadd("myzset", 33, "james");
// 輸出結果:[[["james"],33.0], [["peter"],66.0], [["tom"],99.0]]
jedis.zrangeWithScores("myzset", 0, -1);

4.2.2  Jedis鏈接池的使用方法

  • 前面介紹的是Jedis的直連方式,所謂直連是指Jedis每次都會新建TCP鏈接,使用後再斷開鏈接,對於頻繁訪問Redis的場景顯然不是高效的使用方式。
  • 所以生產環境中通常使用鏈接池的方式對Jedis鏈接進行管理。全部Jedis對象預先放在池子中(JedisPool),每次要鏈接Redis,只須要在池子中借,用完了在歸還給池子。

  客戶端鏈接Redis使用的是TCP協議,直連的方式每次須要創建TCP鏈接,而鏈接池的方式是能夠預先初始化好Jedis鏈接,因此每次只須要從Jedis鏈接池借用便可,而借用和歸還操做是在本地進行的,只有少許的併發同步開銷,遠遠小於新建TCP鏈接的開銷。另外直連的方式沒法限制Jedis對象的個數,在極端狀況下可能會形成鏈接泄露,而鏈接池的形式能夠有效的保護和控制資源的使用。下表給出兩種方式各自的優劣勢。

 

  Jedis提供了JedisPool這個類做爲對Jedis的鏈接池。使用JedisPool操做Redis的代碼示例:

 (1)Jedis鏈接池(一般JedisPool是單例的):

// common-pool鏈接池配置,這裏使用默認配置
GenericObjectPoolConfig poolConfig = new GenericObjectPoolConfig();
// 初始化Jedis鏈接池
JedisPool jedisPool = new JedisPool(poolConfig, "127.0.0.1", 6379);

(2)獲取Jedis對象再也不是直接生成一個Jedis對象進行直連,而是從鏈接池直接獲取,代碼以下:

Jedis jedis = null;
try {
    // 1. 從鏈接池獲取jedis對象
    jedis = jedisPool.getResource(); // 2. 執行操做
    jedis.get("hello");
} catch (Exception e) {
    logger.error(e.getMessage(),e);
} finally {
    if (jedis != null) {
        // 若是使用JedisPool,close操做不是關閉鏈接,表明歸還鏈接池
        jedis.close();
    }
}

4.2.3 Redis中Pipeline的使用方法

回顧:Pipeline(流水線)機制能將一組Redis命令進行組裝,經過一次RTT傳輸給Redis,再將這組Redis命令的執行結果按順序返回給客戶端

  Jedis支持Pipeline特性,咱們知道Redis提供了mget、mset方法,可是並無提供mdel方法,若是想實現這個功能,能夠藉助Pipeline來模擬批量刪除:

public void mdel(List<String> keys) {
    Jedis jedis = new Jedis("127.0.0.1");
    // 1)生成pipeline對象
    Pipeline pipeline = jedis.pipelined(); // 2)pipeline執行命令,注意此時命令並未真正執行
    for (String key : keys) {
        pipeline.del(key);
    }
    // 3)執行命令
    pipeline.sync();
}

4.2.4 Jedis的Lua腳本

  Jedis中執行Lua腳本和redis-cli十分相似,Jedis提供了三個重要的函數實現Lua腳本的執行:

Object eval(String script, int keyCount, String... params)
Object evalsha(String sha1, int keyCount, String... params)
String scriptLoad(String script)

以一個最簡單的Lua腳本爲例子進行說明: return redis.call('get',KEYS[1])

在redis-cli中執行上面的Lua腳本,方法以下:

  eval "return redis.call('get',KEYS[1])" 1 hello

 在Jedis中執行,方法以下:

String key = "hello";
String script = "return redis.call('get',KEYS[1])";
Object result = jedis.eval(script, 1, key);
System.out.println(result);

 

scriptLoad和evalsha函數要一塊兒使用,首先使用scriptLoad將腳本加載到Redis中,代碼以下:

String scriptSha = jedis.scriptLoad(script);

而後執行結果以下:

Stirng key = "hello";
Object result = jedis.evalsha(scriptSha, 1, key);
System.out.println(result);

4.3 客戶端管理

  client list命令能列出與Redis服務端相連的全部客戶端鏈接信息。

  Redis爲每一個客戶端分配了輸入緩衝區,它的做用是將客戶端發送的命令臨時保存,同時Redis從會輸入緩衝區拉取命令並執行,輸入緩衝區爲客戶端發送命令到Redis執行命令提供了緩衝功能,以下圖所示。

  輸入緩衝使用不當會產生兩個問題:

  • 一旦某個客戶端的輸入緩衝區超過1G,客戶端將會被關閉。
  • 輸入緩衝區不受maxmemory控制,假設一個Redis實例設置了maxmemory爲4G,已經存儲了2G數據,可是若是此時輸入緩衝區使用了3G,已經超過maxmemory限制,可能會產生數據丟失、鍵值淘汰、OOM等狀況。

  Redis爲每一個客戶端分配了輸出緩衝區,它的做用是保存命令執行的結果返回給客戶端,爲Redis和客戶端交互返回結果提供緩衝。與輸入緩衝區不一樣的是,輸出緩衝區的容量能夠經過參數client-output-buffer-limit來進行設置,而且輸出緩衝區作得更加細緻,按照客戶端的不一樣分爲三種:普通客戶端、發佈訂閱客戶端、slave客戶端,以下圖所示。

 

  和輸入緩衝區相同的是,輸出緩衝區也不會受到maxmemory的限制,若是使用不當一樣會形成maxmemory用滿產生的數據丟失、鍵值淘汰、OOM等狀況。

 第5章 持久化

   Redis支持RDBAOF兩種持久化機制,持久化功能有效地避免因進程退出形成的數據丟失問題,當下次重啓時利用以前持久化的文件便可實現數據恢復

 5.1 RDB(快照方式)

   RDB持久化是把當前進程數據生成快照保存到硬盤的過程。觸發RDB持久化過程分爲手動觸發和自動觸發:

(1)手動觸發分別對應save和bgsave命令:

  • save命令:阻塞當前Redis服務器,直到RDB過程完成爲止,對於內存比較大的實例會形成長時間阻塞,線上環境不建議使用。
  • bgsave命令:Redis進程執行fork操做建立子進程,RDB持久化過程由子進程負責,完成後自動結束。阻塞只發生在fork階段,通常時間很短。

  顯然bgsave命令是針對save阻塞問題作的優化。所以Redis內部全部的涉及RDB的操做都採用bgsave的方式,而save命令已經廢棄。

  bgsave命令的運做過程:

  1. 執行bgsave命令,Redis父進程判斷當前是否存在正在執行的子進程,若是存在bgsave命令直接返回。
  2. 父進程fork完成後,bgsave命令返回「Background saving started」信息並再也不阻塞父進程,能夠繼續響應其餘命令。
  3. 子進程建立RDB文件,根據父進程內存生成臨時快照文件,完成後對原有文件進行原子替換。
  4. 進程發送信號給父進程表示完成,父進程更新統計信息。

(2)自動觸發:

  • 使用save相關配置,如「save m n」。表示m秒內數據集存在n次修改時,自動觸發bgsave。
  • 若是從節點執行全量複製操做,主節點自動執行bgsave生成RDB文件併發送給從節點。
  • 執行debug reload命令從新加載Redis時,也會自動觸發save操做。
  • 默認狀況下執行shutdown命令時,若是沒有開啓AOF持久化功能則自動執行bgsave。

 

RDB的優勢:

  • RDB是一個緊湊壓縮的二進制文件,表明Redis在某個時間點上的數據快照。很是適用於備份,全量複製等場景。好比每6小時執行bgsave備份,並把RDB文件拷貝到遠程機器或者文件系統中(如hdfs),用於災難恢復。
  • Redis加載RDB恢復數據遠遠快於AOF的方式

RDB的缺點:

  • RDB方式數據沒辦法作到實時持久化/秒級持久化。由於bgsave每次運行都要執行fork操做建立子進程,屬於重量級操做,頻繁執行成本太高。
  • RDB文件使用特定二進制格式保存,Redis版本演進過程當中有多個格式的RDB版本,存在老版本Redis服務沒法兼容新版RDB格式的問題。

 5.2 AOF(日誌方式)

   AOF(append only file)持久化:以獨立日誌的方式記錄每次寫命令,重啓時再從新執行AOF文件中的命令達到恢復數據的目的AOF的主要做用是解決了數據持久化的實時性,目前已是Redis持久化的主流方式。

   AOF默認是默認不開啓的,開啓AOF功能須要設置配置:appendonly yes。

   AOF工做流程:

    

  1. 全部的寫入命令會追加到aof_buf(緩衝區)中。
  2. AOF緩衝區根據對應的策略向硬盤作同步操做。
  3. 隨着AOF文件愈來愈大,須要按期對AOF文件進行重寫,達到壓縮的目的。
  4. 當Redis服務器重啓時,能夠加載AOF文件進行數據恢復。

 注:

1. AOF爲何把命令追加到aof_buf中?

  Redis使用單線程響應命令,若是每次寫AOF文件命令都直接追加到硬盤,那麼性能徹底取決於當前硬盤負載。先寫入緩衝區aof_buf中,還有另外一個好處,Redis能夠提供多種緩衝區同步硬盤的策略,在性能和安全性方面作出平衡。

 2. AOF緩衝區同步文件策略,由參數appendfsync控制:

appendfsync always    #每次有數據修改發生時都會寫入AOF文件,這樣會嚴重下降Redis的速度
appendfsync everysec  #每秒鐘同步一次,顯示地將多個寫命令同步到硬盤
appendfsync no        #讓操做系統決定什麼時候進行同步

3. AOF文件重寫是把Redis進程內的數據轉化爲寫命令同步到新AOF文件的過程。重寫後的AOF文件爲何能夠變小?

1)進程內已經超時的數據再也不寫入文件。
2)舊的AOF文件含有無效命令,重寫使用進程內數據直接生成,這樣新的AOF文件只保留最終數據的寫入命令。
3)多條寫命令能夠合併爲一個,如:lpush list a、lpush list b、lpush list c能夠轉化爲:lpush list a b c。爲了防止單條命令過大形成客戶端緩衝區溢出,對於list、set、hash、zset等類型操做,以64個元素爲界拆分爲多條。

AOF重寫下降了文件佔用空間,除此以外,另外一個目的是:更小的AOF文件能夠更快地被Redis加載。

 

【注】若是同時配了RDB和AOF,優先加載AOF。

第6章 複製

相關文章
相關標籤/搜索