NoSQL數據庫:Redis 入門篇

Redis 學習筆記

初識 Redis

Redis 是一種基於鍵值對的 NoSQL 數據庫,Redis 中的值可以是由 string、hash、list、set、zset 等多種數據結構和算法組成,因此 Redis 可以滿足很多應用場景。Redis 將所有數據都存放在內存中,所以它的讀寫能力也非常高。Redis 還可以將內存的數據利用快照和日誌的形式保存到硬盤上,這樣在發生類似斷電或者機器故障的時候,內存中的數據不會丟失。除了這些功能,Redis 還提供了鍵國企、發佈訂閱、事務、流水線、Lua 等附加功能。

Redis 的特性

速度快

正常情況下,Redis 執行命令的速度非常快,官方給出的數字是讀寫性能可以達到 10 萬/秒。Redis 速度快的原因主要歸納爲幾點:① Redis 的所有數據都放在內存中。② Redis 是使用 C 語言實現的,一般來說 C 語言實現的程序距離底層操作系統更近,因此速度相對會更快。③ Redis 使用了單線程架構,預防了多線程的競爭問題。

基於鍵值對的數據結構服務器

與很多鍵值對數據庫不同的是,Redis 中的值不僅可以是字符串,還可以是具體的數據結構,這樣不僅能應用於多種場景開發,也可以提高開發效率。Redis 的全稱是 REmote Dictionary Server,它主要提供五種數據結構:字符串、哈希、列表、集合、有序集合,同時在字符串的基礎上演變出了位圖和 HyperLogLog 兩種數據結構,隨着 LBS 基於位置服務的發展,Redis 3.2 加入了有關 GEO 地理信息定位的功能。

豐富的功能

① 提供了鍵過期功能,可以實現緩存。② 提供了發佈訂閱功能,可以實現消息系統。③ 支持 Lua 腳本,可以創造新的 Redis 命令。④ 提供了簡單的事務功能,能在一定程度商保證事務特性。⑤ 提供了流水線功能,這樣客戶端能將一批命令一次性傳到 Redis,減少了網絡開銷。

簡單穩定

Redis 的簡單主要體現在三個方面:① 源碼很少,早期只有 2 萬行左右,在 3.0 版本由於添加了集羣特性,增加到了 5 萬行左右,相對於很多 NoSQL 數據庫來說代碼量要少很多。② 採用單線程模型,使得服務端處理模型更簡單,也使客戶端開發更簡單。③ 不依賴底層操作系統的類庫,自己實現了事件處理的相關功能。雖然 Redis 比較簡單,但也很穩定。

客戶端語言多

Redis 提供了簡單的 TCP 通信協議,很多編程語言可以方便地接入 Redis,例如 Java、PHP、Python、C、C++ 等。

持久化

通常來說數據放在內存中是不安全的,一旦發生斷電或故障數據就可能丟失,因此 Redis 提供了兩種持久化方式 RDB 和 AOF 將內存的數據保存到硬盤中。

主從複製

Redis 提供了複製功能,實現了多個相同數據的 Redis 副本,複製功能是分佈式 Redis 的基礎。

高可用和分佈式

Redis 從 2.8 版本正式提供了高可用實現 Redis Sentinel,能夠保證 Redis 節點的故障發現和故障自動轉移。Redis 從 3.0 版本正式提供了分佈式實現 Redis Cluster,提供了高可用、讀寫和容量的擴展性。


Redis 的使用場景

緩存

緩存機制幾乎在所有大型網站都有使用,合理使用緩存不僅可以加快數據的訪問速度,而且能夠有效降低後端數據源的壓力。Redis 提供了鍵值過期時間設置,並且也提供了靈活控制最大內存和內存溢出後的淘汰策略。

排行榜系統

排行榜系統幾乎存在於所有網站,Redis 提供了列表和有序集合數據結構,合理使用這些數據結構可以方便構建各各種排行榜系統。

計數器應用

計數器在網站中的作用很重要,例如視頻網站有播放數、電商網站有瀏覽數,爲了保證數據實時性,每一次播放和瀏覽都要做加 1 的操作,如果併發量很大對於傳統關係型數據庫的性能是很大的挑戰。Redis 天然支持計數功能而且性能也非常好。

社交網絡

粉絲、共同好友/喜好、推送、下拉刷新等是社交網絡的必備功能,由於社交網站的訪問量通常很大,而且關係型數據不太適合保存這種類型的數據,Redis 提供的數據結構可以相對容易地實現這些功能。

消息隊列系統

消息隊列系統是一個大型網站的必備基礎組件,因爲其具有業務解耦、非實時業務削峯等特性。Redis 提供了發佈訂閱和阻塞隊列的功能,對於一般的消息隊列功能基本可以滿足。

Redis 不適合非常大的數據量,成本非常高,也不適合冷數據,會浪費內存。


Redis 安裝與啓動

安裝

在 Linux 上安裝,下載 Redis 指定版本的源碼壓縮包到當前目錄:

wget http://download.redis.io/releases/redis-3.0.7.tar.gz

下載好之後,進行解壓:

tar xzf redis-3.0.7.tar.gz

建立一個 redis 目錄的軟連接:

ln -s redis-3.0.7 redis

進入 redis 目錄:

cd redis

編譯:

make

安裝:

make install

安裝成功後,可以在任何目錄執行 redis-cli -v查看 Redis 的版本。

usr/local/bin 目錄下的可執行文件說明:

可執行文件 作用
redis-server 啓動 Redis
redis-cli Redis 命令行客戶端
redis-benchmark Redis 基準測試工具
redis-check-aof Redis AOF 持久化文件檢測和修復工具
redis-check-dump Redis RDB 持久化文件檢測和修復工具
redis-sentinel 啓動 Redis Sentinel

啓動

有三種方式:默認配置、運行配置、配置文件啓動。

默認配置:會使用 Redis 的默認配置啓動,直接輸入 redis-server 即可,不推薦。

運行配置:可以在運行時自定義配置,沒設置的配置依然使用默認值,例如 redis-server --port 6380 指定以 6380 爲端口號啓動,不推薦。

配置文件:生產環境中推薦的啓動方式,將配置寫到指定文件裏,例如配置寫到了 /opt/redis/redis.conf 中,執行如下命令即可啓動 Redis:redis-server /opt/redis/redis.conf

客戶端連接方式有兩種:

① 交互式:例如 redis-cli -h 127.0.0.1 -p 6379 ,之後所有操作都是通過交互方式實現的,不用再執行連接了。

② 命令方式:例如 redis-cli -h 127.0.0.1 -p 6379 set name kobe 可以直接得到命令的返回結果。如果沒有 -h 參數默認連接 127.0.0.1,如果沒有 -p,默認連接 6379 端口。

停止

可以使用 redis-cli shutdown 停止 127.0.0.1 上 6379 端口的服務。

Redis 服務端會斷開與客戶端的連接、生成 RDB 持久化文件,將其保存在磁盤上然後關閉。

除了 shutdown 命令也可以使用 kill 進程號的方式關閉 Redis,但不要使用 kill -9 強制殺死 Redis 服務,不但不會做持久化操作,還會造成緩衝區等資源不能優雅關閉,極端情況會造成 AOF 和複製丟失數據的情況。


總結

Redis 有 8 個特性:速度快、基於鍵值對的數據結構服務器、功能豐富、簡單穩定、客戶端語言多、持久化、主從複製、支持高可用和分佈式。

Redis 並不是萬金油,有些場景不適合使用 Redis 做開發。

生產環境中使用配置文件啓動 Redis。

生產環境選取穩定版本的 Redis,版本號第二位是偶數代表穩定。


API 的理解和使用

預備

全局命令

查看所有鍵:keys *

查看鍵總數:dbsize 會返回當前數據庫中鍵的總數,不會遍歷所有鍵而是直接獲取 Redis 內置的鍵總數變量,所有時間複雜度是 O(1)。而 keys 會遍歷所有鍵,時間複雜度爲 O(n),當 Redis 保存了大量的鍵時,線上環境禁止使用。

檢查鍵是否存在:exists key,如果存在返回 1,否則返回 0。

刪除鍵:del key [key..] del 是一個通用命令,無論值是什麼數據類型結構都可以刪除,返回結果爲成功刪除的鍵的個數。

鍵過期:expire key seconds ,Redis 支持對鍵添加過期時間,當超過過期時間之後會自動刪除。ttl 會返回鍵的剩餘過期時間,返回非負數代表鍵的過期時間,-1 代表鍵沒有設置過期時間,-2 代表鍵不存在。

查看鍵的數據類型結構:type key,如果鍵不存在則返回 none。

數據結構和內部編碼

type 命令實際上返回的是當前鍵的數據類型結構,它們分別是:string、hash、list、set、zset,但這些只是 Redis 對外的數據結構。實際上每種數據結構都有自己底層的內部編碼實現,這樣 Redis 會在合適的場景選擇合適的內部編碼,string 包括了 raw、int 和 embstr,hash 包括了 hashtable 和 ziplist,list 包括了 linkedlist 和 ziplist,set 包括了 hashtable 和 intset,zset 包括了 skiplist 和 ziplist。可以使用 object encoding 查看內部編碼。

Redis 這樣設計的好處是:① 可以改進內部編碼,而對外的數據結構和命令沒有影響。② 多種內部編碼實現可以在不同場景下發揮各自的優勢,例如 ziplist 比較節省內存,但在列表元素較多的情況下性能有所下降,這時 Redis 會根據配置選項將列表類型的內部實現轉換爲 linkedlist。

單線程架構

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

每次客戶端調用都經歷了發送命令、執行命令、返回結果三個過程,因爲 Redis 是單線程處理命令的,所以一條命令從客戶端到達服務器不會立即執行,所有命令都會進入一個隊列中,然後逐個被執行。客戶端的執行順序可能不確定,但是可以確定不會有兩條命令被同時執行,不存在併發問題。

通常來說單線程處理能力要比多線程差,Redis 快的原因:① 純內存訪問,Redis 將所有數據放在內存中。② 非阻塞 IO,Redis 使用 epoll 作爲 IO 多路複用技術的實現,再加上 Redis 本身的事件處理模型將 epoll 中的連接、讀寫、關閉都轉換爲時間,不在網絡 IO 上浪費過多的時間。③ 單線程避免了線程切換和競爭產生的消耗。單線程的一個問題是對於每個命令的執行時間是有要求的,如果某個命令執行時間過長會造成其他命令的阻塞,對於 Redis 這種高性能服務來說是致命的,因此 Redis 是面向快速執行場景的數據庫。


字符串

字符串類型是 Redis 最基礎的數據結構,鍵都是字符串類型,而且其他幾種數據結構都是在字符串類型的基礎上構建的。字符串類型的值可以實際可以是字符串(簡單的字符串、複雜的字符串如 JSON、XML)、數字(整形、浮點數)、甚至二進制(圖片、音頻、視頻),但是值最大不能超過 512 MB。

常用命令

設置值

set key value [ex seconds] [px millseconds] [nx|xx]

  • ex seconds:爲鍵設置秒級過期時間,跟 setex 效果一樣
  • px millseconds:爲鍵設置毫秒級過期時間
  • nx:鍵必須不存在纔可以設置成功,用於添加,跟 setnx 效果一樣。由於 Redis 的單線程命令處理機制,如果多個客戶端同時執行,則只有一個客戶端能設置成功,可以用作分佈式鎖的一種實現。
  • xx:鍵必須存在纔可以設置成功,用於更新

獲取值

get key,如果不存在返回 nil

批量設置值

mset key value [key value...]

批量獲取值

mget key [key...]

批量操作命令可以有效提高開發效率,假如沒有 mget,執行 n 次 get 命令需要 n 次網絡時間 + n 次命令時間,使用 mget 只需要 1 次網絡時間 + n 次命令時間。

Redis 可以支持每秒數萬的讀寫操作,但這指的是 Redis 服務端的處理能力,對於客戶端來說一次命令處理命令時間還有網絡時間。因爲 Redis 的處理能力已足夠高,對於開發者來說,網絡可能會成爲性能瓶頸。

計數

incr key

incr 命令用於對值做自增操作,返回結果分爲三種:① 值不是整數返回錯誤。② 值是整數,返回自增後的結果。③ 值不存在,按照值爲 0 自增,返回結果 1。除了 incr 命令,還有自減 decr、自增指定數字 incrby、自減指定數組 decrby、自增浮點數 incrbyfloat。


不常用命令

追加值

append key value,可以向字符串尾部追加值

字符串長度

strlen key

設置並返回原值

getset key value

設置指定位置的字符

setrange key offset value

獲取部分字符串

getrange key start end,start 和 end分別是開始和結束的偏移量,偏移量從 0 開始計算。


內部編碼

字符串類型的內部編碼有三種:

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

典型使用場景

緩存功能

Redis 作爲緩存層,MySQL 作爲存儲層,首先從 Redis 獲取數據,如果沒有獲取到就從 MySQL 獲取,並將結果寫回到 Redis,添加過期時間。

計數

Redis 可以實現快速計數功能,例如視頻每播放一次就用 incy 把播放數加 1。

共享 Session

一個分佈式 Web 服務將用戶的 Session 信息保存在各自服務器,但會造成一個問題,出於負載均衡的考慮,分佈式服務會將用戶的訪問負載到不同服務器上,用戶刷新一次可能會發現需要重新登陸。爲解決該問題,可以使用 Redis 將用戶的 Session 進行集中管理,在這種模式下只要保證 Redis 是高可用和擴展性的,每次用戶更新或查詢登錄信息都直接從 Redis 集中獲取。

限速

例如爲了短信接口不被頻繁訪問會限制用戶每分鐘獲取驗證碼的次數或者網站限制一個 IP 地址不能在一秒內訪問超過 n 次。可以使用鍵過期策略和自增計數實現。


哈希

哈希類型是指鍵值本身又是一個鍵值對結構,哈希類型中的映射關係叫做 field-value,這裏的 value 是指 field 對於的值而不是鍵對於的值。

命令

設置值

hset key field value,如果設置成功會返回 1,反之會返回 0,此外還提供了 hsetnx 命令,作用和 setnx 類似,只是作用於由鍵變爲 field。

獲取值

hget key field,如果不存在會返回 nil。

刪除 field

hdel key field [field...],會刪除一個或多個 field,返回結果爲刪除成功 field 的個數。

計算 field 個數

hlen key

批量設置或獲取 field-value

hmget key field [field...]
hmset key field value [field value...]

hmset 需要的參數是 key 和多對 field-value,hmget 需要的參數是 key 和多個 field。

判斷 field 是否存在

hexists key field,存在返回 1,否則返回 0。

獲取所有的 field

hkeys key,返回指定哈希鍵的所有 field。

獲取所有 value

hvals key,獲取指定鍵的所有 value。

獲取所有的 field-value

hgetall key,獲取指定鍵的所有 field-value。

計數

hincrby key fieldhincrbyfloat key field,作用和 incrby 和 incrbyfloat 一樣,作用域是 field。

計算 value 的字符串長度

hstrlen key field


內部編碼

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

  • ziplist 壓縮列表:當哈希類型元素個數和值小於配置值(默認 512 個和 64 字節)時會使用 ziplist 作爲內部實現,使用更緊湊的結構實現多個元素的連續存儲,在節省內存方面比 hashtable 更優秀。
  • hashtable 哈希表:當哈希類型無法滿足 ziplist 的條件時會使用 hashtable 作爲哈希的內部實現,因爲此時 ziplist 的讀寫效率會下降,而 hashtable 的讀寫時間複雜度都爲 O(1)。

使用場景

緩存用戶信息,有三種實現:

  • 原生字符串類型:每個屬性一個鍵。

    set user:1:name tom
    set user:1:age 23
    set user:1:city xi'an
    

    優點:簡單直觀,每個屬性都支持更新操作。

    缺點:佔用過多的鍵,內存佔用量較大,用戶信息內聚性差,一般不會在生產環境使用。

  • 序列化字符串類型:將用戶信息序列化後用一個鍵保存。

    set user:1 serialize(userInfo)
    

    優點:編程簡單,如果合理使用序列化可以提高內存使用率。

    缺點:序列化和反序列化有一定開銷,同時每次更新屬性都需要把全部數據取出進行反序列化,更新後再序列化到 Redis。

  • 哈希類型:每個用戶屬性使用一對 field-value,但只用一個鍵保存。

    hmset user:1 name tom age 23 city xi'an
    

    優點:簡單直觀,如果合理使用可以減少內存空間使用。

    缺點:要控制哈希在 ziplist 和 hashtable 兩種內部編碼的轉換,hashtable 會消耗更多內存。


列表

列表類型是用來存儲多個有序的字符串,列表中的每個字符串稱爲元素,一個列表最多可以存儲 232-1 個元素。可以對列表兩端插入(push)和彈出(pop),還可以獲取指定範圍的元素列表、獲取指定索引下標的元素等。列表是一種比較靈活的數據結構,它可以充當棧和隊列的角色,在實際開發中有很多應用場景。

列表類型有兩個特點:① 列表中的元素是有序的,可以通過索引下標獲取某個元素或者某個範圍內的元素列表。② 列表中的元素可以重複。

命令

添加操作

從右邊插入元素:rpush key value [value...]

從左到右獲取列表的所有元素:lrange 0 -1

從左邊插入元素:lpush key value [value...]

向某個元素前或者後插入元素:linsert key before|after pivot value,會在列表中找到等於 pivot 的元素,在其前或後插入一個新的元素 value。

查找

獲取指定範圍內的元素列表:lrange key start end,索引從左到右的範圍是 0~N-1,從右到左是 -1~-N,lrange 中的 end 包含了自身。

獲取列表指定索引下標的元素:lindex key index,獲取最後一個元素可以使用 lindex key -1

獲取列表長度:llen key

刪除

從列表左側彈出元素:lpop key

從列表右側彈出元素:rpop key

刪除指定元素:lrem key count value,如果 count 大於 0,從左到右刪除最多 count 個元素,如果 count 小於 0,從右到左刪除最多個 count 絕對值個元素,如果 count 等於 0,刪除所有。

按照索引範圍修剪列表:ltrim key start end,只會保留 start ~ end 範圍的元素。

修改

修改指定索引下標的元素:lset key index newValue

阻塞操作

阻塞式彈出:blpop/brpop key [key...] timeout,timeout 表示阻塞時間。

當列表爲空時,如果 timeout = 0,客戶端會一直阻塞,如果在此期間添加了元素,客戶端會立即返回。

如果是多個鍵,那麼brpop會從左至右遍歷鍵,一旦有一個鍵能彈出元素,客戶端立即返回。

如果多個客戶端對同一個鍵執行 brpop,那麼最先執行該命令的客戶端可以獲取彈出的值。


內部編碼

列表的內部編碼有兩種:

  • ziplist 壓縮列表:跟哈希的 zipilist 相同,元素個數和大小小於配置值(默認 512 個和 64 字節)時使用。
  • linkedlist 鏈表:當列表類型無法滿足 ziplist 的條件時會使用linkedlist。

Redis 3.2 提供了 quicklist 內部編碼,它是以一個 ziplist 爲節點的 linkedlist,它結合了兩者的優勢,爲列表類提供了一種更爲優秀的內部編碼實現。


使用場景

消息隊列

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

文章列表

每個用戶有屬於自己的文章列表,現在需要分頁展示文章列表,就可以考慮使用列表。因爲列表不但有序,同時支持按照索引範圍獲取元素。每篇文章使用哈希結構存儲,例如每篇文章有三個屬性,title、timestamp 和 content:

hmset article:k title t timestamp 147651524 content c

向用戶文章列表添加文章,user:{id}:articles 作爲用戶文章列表的鍵:

lpush user:k:articles article:k

分頁獲取用戶文章列表,例如以下僞代碼獲取用戶 id = 1 的前 10 篇文章。

articles = lrange user:1:articles 0 9
for article in {articles}
	hgetall {article}

使用列表類型保存和獲取文章列表存在兩個問題:① 如果每次分頁獲取的文章個數較多,需要執行多次 hgetall 操作,此時可以考慮使用 Pipeline 批量獲取,或者考慮將文章數據序列化爲字符串類型,使用 mget 批量獲取。② 分頁獲取文章列表時,lrange 命令在列表兩端性能較好,但如果列表大,獲取中間範圍的元素性能會變差,可以考慮將列表做二級拆分,或使用 Redis3.2 的 quicklist。


lpush + lpop = 棧

lpush + rpop = 隊列

lpush + ltrim = 優先集合

lpush + brpop = 消息隊列


集合

集合類型也是用來保存多個字符串元素,和列表不同的是集合不允許有重複元素,並且集合中的元素是無序的,不能通過索引下標獲取元素。一個集合最多可以存儲 232-1 個元素。Redis 除了支持集合內的增刪改查,還支持多個集合取交集、並集、差集。

命令

集合內操作

添加元素

sadd key element [element...],返回結果爲添加成功的元素個數。

刪除元素

srem key element [element...],返回結果爲成功刪除的元素個數。

計算元素個數

scard key,時間複雜度爲 O(1),會直接使用 Redis 內部的遍歷。

判斷元素是否在集合中

sismember key element,如果存在返回 1,否則返回 0。

隨機從集合返回指定個數個元素

srandmember key [count],如果不指定 count 默認爲 1。

從集合隨機彈出元素

spop key,可以從集合中隨機彈出一個元素。

獲取所有元素

smembers key


集合間操作

求多個集合的交集

sinter key [key...]

求多個集合的並集

sunion key [key...]

求多個集合的差集

sdiff key [key...]

保存交集、並集、差集的結果

sinterstore destination key [key...]
sunionstore destination destination key [key...]
sdiffstore destination key [key...]

集合間的運算在元素較多的情況下會比較耗時,所以 Redis 提供了這三個指令將集合間交集、並集、差集的結果保存在 destination key 中。


內部編碼

集合類型的內部編碼有兩種:

  • intset 整數集合:當集合中的元素個數小於配置值(默認 512 個時),使用 intset。
  • hashtable 哈希表:當集合類型無法滿足 intset 條件時使用 hashtable。當某個元素不爲整數時,也會使用 hashtable。

使用場景

集合類型比較典型的使用場景是標籤,例如一個用戶可能與娛樂、體育比較感興趣,另一個用戶可能對例時、新聞比較感興趣,這些興趣點就是標籤。這些數據對於用戶體驗以及增強用戶黏度比較重要。

給用戶添加標籤

sadd user:1:tags tag1 tag2 tag5
sadd user:2:tags tag3 tag4 tag5
...
sadd user:k:tags tagx tagy tagz

給標籤添加用戶

sadd tag:1:users user:1 user:3
sadd tag:2:users user:1 user:4 user:5
...
sadd tag:k:users user:x user:y ...

用戶和標籤的關係維護應該在一個事務內執行,防止部分命令失敗造成的數據不一致。

刪除用戶標籤

srem user:1:tags tag1 tag5

刪除標籤下的用戶

srem tag:1:users user:1

刪除也同樣應該放在一個事務中。

求兩個用戶共同感興趣的標籤

sinter user:1:tags user:2:tags

sadd = 標籤

spop/srandmember = 生成隨機數,比如抽獎

sadd + sinter = 社交需求


有序集合

有序集合保留了集合不能有重複成員的特性,不同的是可以排序。但是它和列表使用索引下標作爲排序依據不同的是,他給每個元素設置一個分數(score)作爲排序的依據。有序集合提供了獲取指定分數和元素查詢範圍、計算成員排名等功能。

數據結構 是否允許元素重複 是否有序 有序實現方式 應用場景
列表 下標 時間軸,消息隊列
集合 / 標籤,社交
有序集合 分值 排行榜,社交

命令

集合內

添加成員

zadd key score member [score member...],返回結果是成功添加成員的個數

Redis 3.2 爲 zadd 命令添加了 nx、xx、ch、incr 四個選項:

  • nx:member 必須不存在纔可以設置成功,用於添加
  • xx:member 必須存在才能設置成功,用於更新
  • ch:返回此次操作後,有序集合元素和分數變化的個數
  • incr:對 score 做增加,相當於 zincrby

zadd 的時間複雜度爲 O(logn),sadd 的時間複雜度爲 O(1)。

計算成員個數

zcard key,時間複雜度爲 O(1)。

計算某個成員的分數

zscore key member ,如果不存在則返回 nil。

計算成員排名

zrank key member,從低到高返回排名

zrevrank key member,從高到低返回排名

刪除成員

zrem key member [member...],返回結果是成功刪除的個數。

增加成員的分數

zincrby key increment member

返回指定排名範圍的成員

zrange key start end [withscores]

zrevrange key start end [withscores]

zrange 從低到高返回,zrevrange 從高到底返回,如果加上 withscores 選項同時會返回成員的分數。

返回指定分數範圍的成員

zrangebyscore key min max [withscores] [limit offset count]

zrevrangebyscore key min max [withscores] [limit offset count]

zrangebyscore 從低到高返回,zrevrangebyscore 從高到底返回,如果加上 withscores 選項同時會返回成員的分數。[limit offset count] 可以限制輸出的起始位置和個數。

返回指定分數範圍成員個數

zcount key min max

刪除指定排名內的升序元素

zremrangebyrank key start end

刪除指定分數範圍內的成員

zremrangebyscore key min max


集合間的操作

交集

zinterstore destination numkeys key [key...] [weights weight [weight...]] [aggregate sum|min|max]

  • destination:交集結果保存到這個鍵

  • numkeys:要做交集計算鍵的個數

  • key [key…]:需要做交集計算的鍵

  • weights weight [weight…]:每個鍵的權重,默認 1

  • aggregate sum|min|max:計算交集後,分值可以按和、最小值、最大值彙總,默認 sum

並集

zunionstore destination numkeys key [key...] [weights weight [weight...]] [aggregate sum|min|max]


內部編碼

有序集合的內部編碼有兩種:

  • ziplist 壓縮列表:當有序集合元素個數和值小於配置值(默認128 個和 64 字節)時會使用 ziplist 作爲內部實現。
  • skiplist 跳躍表:當 ziplist 不滿足條件時使用,因爲此時 ziplist 的讀寫效率會下降。

使用場景

有序集合的典型使用場景就是排行榜系統。

例如用戶 mike 上傳了一個視頻並添加了 3 個贊,可以使用有序集合的 zadd 和 zincrby:

zadd user:ranking:2020_06_19 3 mike

如果之後再獲得一個贊,可以使用 zincrby:

zincrby user:ranking:2020_06_19 1 mike

例如需要將用戶 tom 從榜單刪除,可以使用 zrem:

zrem user:ranking:2020_06_19 tom

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

zrevrange user:ranking:2020_06_19 0 9

展示用戶信息及用戶分數,將用戶名作爲鍵後綴,將用戶信息保存在哈希類型中,至於用戶分數和排名可以使用 zscore 和 zrank:

hgetall user:info:tom
zscore user:ranking:2020_06_19 tom
zrank user:ranking:2020_06_19 tom

鍵管理

單個鍵管理

鍵重命名

rename key newkey

如果 rename 前鍵已經存在,那麼它的值也會被覆蓋。

爲了防止強行覆蓋,Redis 提供了 renamenx 命令,確保只有 newkey 不存在時才被覆蓋。由於重命名鍵期間會執行 del 命令刪除舊的鍵,如果鍵對應值比較大會存在阻塞的可能。

隨機返回一個鍵

random key

鍵過期

expire key seconds:鍵在 seconds 秒後過期

expireat key timestamp:鍵在秒級時間戳 timestamp 後過期

如果過期時間爲負值,鍵會被立即刪除,和 del 命令一樣。

persist 命令可以將鍵的過期時間清除。

對於字符串類型鍵,執行 set 命令會去掉過期時間,set 命令對應的函數 setKey 最後執行了 removeExpire 函數去掉了過期時間。

Redis 不支持二級數據結構(例如哈希、列表)內部元素的過期功能,例如不能對列表類型的一個元素設置過期時間。

setex 命令作爲 set + expire 的組合,不單是原子執行並且減少了一次網絡通信的時間。

鍵遷移

  • move

    move key db

    move 命令用於在 Redis 內部進行數據遷移,move key db 就是把指定的鍵從源數據庫移動到目標數據庫中。

  • dump + restore

    dump key

    restore key ttl value

    可以實現在不同的 Redis 勢力之間進行數據遷移,分爲兩步:

    ① 在源 Redis 上,dump 命令會將鍵值序列化,格式採用 RDB 格式。

    ② 在目標 Redis 上,restore 命令將上面序列化的值進行復原,ttl 參數代表過期時間, ttl = 0 則沒有過期時間。

    整個遷移並非原子性的,而是通過客戶端分步完成,並且需要兩個客戶端。

  • migrate

    實際上 migrate 命令就是將 dump、restore、del 三個命令進行組合,從而簡化了操作流程。migrate 具有原子性,且支持多個鍵的遷移,有效提高了遷移效率。實現過程和 dump + restore 類似,有三點不同:

    ① 整個過程是原子執行,不需要在多個 Redis 實例開啓客戶端。

    ② 數據傳輸直接在源 Redis 和目標 Redis 完成。

    ③ 目標 Redis 完成 restore 後會發送 OK 給源 Redis,源 Redis 接收後會根據 migrate 對應的選項來決定是否在源 Redis 上刪除對應的鍵。

命令 作用域 原子性 支持多個鍵
move Redis 實例內部
dump + restore Redis 實例之間
migrate Redis 實例之間

遍歷鍵

全量遍歷鍵

keys pattern

*代表匹配任意字符,? 匹配一個字符,[] 匹配部分字符,例如 [1,3] 匹配 1 和 3, [1-3] 匹配 1 到 3 的任意數字,\用來做轉義。

keys * 遍歷所有的鍵,一般不在生產環境使用,在以下情況可以使用:

① 在一個不對外提供服務的 Redis 從節點上執行,不會阻塞客戶端的請求,但會影響主從複製。

② 如果確定鍵值總數比較少可以執行。


漸進式遍歷

Redis 從 2.8 版本後提供了一個新的命令 scan,能有效解決 keys 存在的問題。和 keys 遍歷所有鍵不同,scan 採用漸進式遍歷的方式解決阻塞問題,每次 scan 的時間複雜度爲 O(1),但是要真正實現 keys 的功能可能需要執行多次 scan。

scan cursor [match pattern] [count number]

cursor 是必須參數,代表一個遊標,第一次遍歷從 0 開始,每次 scan 完會返回當前遊標的值,直到值爲 0 表示遍歷結束。

match pattern 是可選參數,作用是模式匹配。

count number 是可選參數,作用是表明每次要遍歷的鍵個數,默認值爲 10。

除了 scan 外,Redis 提供了面向哈希、集合、有序集合的掃描遍歷命令,解決了 hgetall、smembers、zrange 可能產生的阻塞問題,對應命令分別爲 hscan、sscan、zscan。

漸進式遍歷可以有效解決 keys 命令可能產生的阻塞問題,但是如果在 scan 過程中有鍵的變化,那麼遍歷效果可能會遇到問題:新增的鍵沒有被遍歷到,遍歷了重複的鍵等情況。


數據庫管理

切換數據庫

select dbIndex

Redis 中默認配置有 16 個數據庫,例如 select 0 將切換到第一個數據庫,數據庫之間的數據是隔離的。

flushdb/flushall

用於清除數據庫,flushdb 只清除當前數據庫,flushall 會清除所有數據庫。如果當前數據庫鍵值數量比較多,flushdb/flushall 存在阻塞 Redis 的可能性。


總結

Redis 提供 5 種數據結構,每種數據結構都有多種內部編碼實現。

純內存存儲、IO 多路複用計數、單線程架構是造就 Redis 高性能的三個因素。

由於 Redis 的單線程結構,所以需要每個命令能被快速執行完,否則會存在阻塞的可能。

批量操作(例如 mget、mset、hmset 等)能夠有效提高命令執行的效率,但要注意每次批量操作的個數和字節數。

persist 命令可以刪除任意類型鍵的過期時間,但 set 也會刪除字符串類型鍵的過期時間。

move、dump + restore、migrate 是 Redis 發展過程中三種遷移鍵的方式,其中 move 命令基本廢棄,migrate 命令用原子性的方式實現了 dump + restore,並且支持批量操作,是 Redis Cluster 實現水平擴容的重要工具。

scan 命令可以解決 keys 命令可能帶來的阻塞問題,同時 Redis 還提供了 hscan、sscan、zscan 漸進式遍歷 hash、set、zset。


高級功能

慢查詢分析

許多系統(例如 MySQL)提供了慢查詢日誌幫助開發和運維任意定位系統存在的慢操作,慢查詢日誌是系統在命令執行前後計算每條命令的執行時間,當超過預設閾值,就將這條命令的相關信息(例如發生時間、耗時、命令的詳細信息)記錄下來,Redis 也提供了類似的功能。

Redis 客戶端執行一條命令分爲四步:發送命令、命令排隊、命令執行、返回結果。慢查詢只統計第三步的時間,所有沒有慢查詢不代表客戶端沒有超時問題。

當超過 slowlog-log-slower-than 閾值(默認 10000 微秒)時,該命令會被記錄在慢查詢日誌,如果設置閾值爲 0 會記錄所有命令,如果設置爲負值對所有命令都不會記錄。Redis 使用了一個列表來存儲慢查詢日誌,一個新的命令滿足慢查詢條件時被插入到這個列表中,當慢查詢日誌列表處於最大長度時,最早插入的一個命令將從列表中移出。slowlog-max-len 是慢查詢列表的最大長度。

在 Redis 中有兩種修改配置的方法,一種是修改配置文件,另一種是使用 config set 命令動態修改,如果要將配置持久化到本地配置文件,需要執行 config rewrite 命令。

雖然慢查詢日誌存放在 Redis 的內存列表中,但 Redis 並沒有暴露這個列表的鍵,而是通過一組命令來實現對慢查詢日誌的訪問和管理。

獲取慢查詢日誌:slowlog get [n],n 可以指定條數。

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

慢查詢日誌重置:slowlog reset,實際是隊列做清理操作。

使用時注意:

  • slowlog-max-len 配置建議:線上建議調大慢查詢列表,記錄慢查詢時 Redis 會對長命令階段,並不會佔用大量內存。增大慢查詢列表可以減緩慢查詢被剔除的可能,線上可以設置爲 1000 以上。
  • slowlog-log-slower-than 配置建議:默認 10 毫秒判斷爲慢查詢,需要根據 Redis 併發量調整。由於 Redis 採用單線程響應命令,對於高流量場景,可以設置爲 1 毫秒。
  • 慢查詢只記錄命令執行時間,並不包括命令排隊和網絡傳輸時間。因此客戶端執行命令的時間會大於命令實際時間,因爲命令執行排隊機制,慢查詢會導致其他所有命令級聯阻塞,所以當客戶端出現請求超時,需要檢查該時間點是否有對應的慢查詢,從而分析出是否爲慢查詢導致命令級聯阻塞。
  • 由於慢查詢日誌是一個 FIFO 的隊列,因此慢查詢比較多的情況下會丟失部分慢查詢命令,爲了防止這種情況可以定期執行 slow get 命令將慢查詢日誌持久化到其他存儲中(例如 MySQL)。

Redis Shell

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

redis-cli

redis-cli 除了 -h、-p 還有一些其他的參數:

-r:代表命令將執行多次

-i:代表每隔幾秒執行一次命令

-x:代表從標準輸入讀取數據作爲 redis-cli 的最後一個參數

-c:連接 Redis Cluster 節點時需要使用的,可以防止 moved 和 ask 異常。

-a:如果 Redis 設置了密碼,可以用 -a 選項。

–scan/–pattern:用於掃描指定模式的鍵,相對於使用 scan 命令。

–slave:把當前客戶端模擬成當前 Redis 節點的從節點,可以用來獲取當前 Redis 節點的更新操作。

–rdb:該選項會請求 Redis 實例生成併發送 RBD 持久化文件,保存在本地。可使用它做持久化文件的定期備份。

–pipe:用於將命令封裝成 Redis 通信協議定義的數據格式,批量發送給 Redis 執行。

–bigkeys:使用 scan 命令對 Redis 的鍵進行採樣,從中找到內存佔用比較大的鍵值,這些鍵值可能是系統的瓶頸。

–eval:用於指定 Lua 腳本。

–latency:檢測網絡延遲。

–stat:實時獲取 Redis 的重要統計信息。

–no-raw:要求命令的返回結果必須是原始的格式。

–raw:要求返回結果是格式化後的結果。


redis-server

除了啓動 Redis 外,還有一個 --test-memory 選項,該選項可以用來檢測當前操作系統能否穩定地分配指定容量地內存給 Redis,通過這種檢測可以有效避免因爲內存問題造成 Redis 崩潰。


redis-benchmark

可以爲 Redis 做基準性能測試,它提供了很多選項幫助開發和運維人員測試 Redis 的相關性能。

相關選項:

-c:代表客戶端的併發數量。

-n:代表客戶端請求總量。

-P:每個請求 pipeline 的數據量,默認爲 1。

-k:代表客戶端是否使用 keepalive,1 爲使用,0 爲不使用,默認 1。

-t:對指定命令進行基準測試。

–csv:將結果按照 csv 格式輸出,便於後續處理。


Pipeline

Redis 客戶端執行一條命令需要經過發送命令、命令排隊、執行命令、返回結果。其中第一步和第四步稱爲 RTT 往返時間。Redis 提供了批量操作命令,有效地節約 RTT。但大部分命令不支持批量操作,例如要執行 n 次 hgetall 命令,並沒有 mhgetall 命令,需要消耗 n 次 RTT。

Pipelin 流水線機制能改善上述問題,它能將一組 Redis 命令進行組裝,通過一次 RTT 傳輸給 Redis,再將這組命令地執行結果按順序返回給客戶端。

Pipeline 執行速度一般比逐條執行要快,客戶端和服務端地網絡延時越大效果就越明顯。

和原生批量命令的區別:

  • 原生批量命令是原子的,Pipeline 是非原子的。
  • 原生批量命令是一個命令對應多個 key,Pipeline 支持多個命令。
  • 原生批量命令是 Redis 服務端支持實現的,而 Pipeline 需要服務端和客戶端共同實現。

事務

Redis 提供了簡單的事務功能,將一組需要一起執行的命令放到 multi 和 exec 兩個命令之間。multi 代表事務開始,exec 命令代表事務結束,它們之間的命令是原子順序執行的。如果要停止事務的執行,可以使用 discard 命令代替 exec。

事務中的命令錯誤分爲命令錯誤和運行時錯誤:

  • 例如 set 寫成了 sett,屬於語法錯誤,會造成整個事務無法執行。
  • 例如把 sadd 寫成了 zadd,這種就是運行時錯誤,Redis 不支持回滾,錯誤前的語句會執行成功,開發人員需要自己修復這類問題。

有些應用場景需要在事務之前,確保事務中的 key 沒有被其他客戶端修改過才執行事務,否則不執行(類似樂觀鎖)。Redis 提供了 watch 命令解決這類問題。

例如客戶端1 在執行 multi 之前執行了 watch 命令,客戶端2 在客戶端1 執行 exec 之前修改了 key 的值,此時事務會執行失敗,返回 nil。

Redis 提供了簡單的事務,之所以說簡單是因爲它不支持事務中的回滾特性,同時無法實現命令之間的邏輯關係計算。


Bitmaps

Bitmaps 本身不是一種數據結構,實際上它就是字符串,但是它可以對字符串的位進行操作。

Bitmaps 單獨提供了一套命令,所以在 Redis 使用 Bitmaps 和使用字符串的方法不太相同,可以把 Bitmaps 看作一個以位爲單位的數組,數組的每個單元只能存儲 0 和 1,數組的下標叫做偏移量。

命令

例:將每個獨立用戶是否訪問過網站存放在 Bitmaps 中,將訪問過的用戶記作 1,沒有訪問過的記作 0,偏移量作爲用戶的 id。

設置值

setbit key offset value

設置鍵的第 offset 個位的值,假設有 20 個用戶,id 爲 0、5、11、15、19 的用戶對網站進行了訪問,那麼初始化如下:

setbit unique:users:2020-06-20 0 1
setbit unique:users:2020-06-20 5 1
setbit unique:users:2020-06-20 11 1
setbit unique:users:2020-06-20 15 1
setbit unique:users:2020-06-20 19 1

很多應用的用戶 id 直接以一個指定數字開頭,例如 10000,直接將用戶 id 與 Bitmaps 的偏移量對應勢必會造成一定浪費,通常做法是每次做 setbit 操作時將用戶 id 減去這個指定數字。在第一次初始化 Bitmaps 時,如果偏移量非常大,那麼整個初始化過程會執行比較慢,可能造成阻塞。

獲取值

getbit key offset

獲取鍵的第 offset 個位的值,例如獲取 id 爲 8 的用戶是否在 2020-06-20 這天訪問過:

getbit unique:users:2020-06-20 8

獲取指定範圍值爲 1 的個數

bitcount key [start end]

例如獲取 2020-06-20 這天訪問過的用戶數量

bitcount unique:users:2020-06-20

start 和 end 代表起始和結束字節數。

Bitmaps 間的運算

bitop op destkey key [key...]

bitop 是一個複合操作,它可以做交集、並集、非、異或並將結果保存到 destkey 中。

例如計算 2020-06-20 和 2020-06-21 都訪問過網站的用戶數量:

bitop and unique:users:and:2020-06-20_21 unique:users:2020-06-20 unique:users:2020-06-21
bitcount unique:users:and:2020-06-20_21 

例如計算 2020-06-20 和 2020-06-21 任意一天訪問過網站的用戶數量:

bitop or unique:users:or:2020-06-20_21 unique:users:2020-06-20 unique:users:2020-06-21
bitcount unique:users:or:2020-06-20_21

計算第一個值爲 tartgetBit 的偏移量

bitops key targetBit [start] [end]

例如計算 2020-06-20 當前訪問網站的最小用戶 id:

bitops unique:users:2019-06-20 1

假設網站的活躍用戶量很大,使用 Bitmaps 相比 set 可以節省很多內存,但如果活躍用戶很少就會浪費內存。


HyperLogLog

HyperLogLog 不是一種新的數據結構,實際也是字符串類型,是一種基數算法。提供 HyperLogLog 可以利用極小的內存空間完成獨立總數的統計,數據集可以是 IP、Email、ID 等。

添加

pfadd key element [element...],如果添加成功會返回 1

計算獨立用戶數

pfcount key [key...]

合併

pfmerge destkey sourcekey [sourcekey...]

HyperLogLog 內存佔用量非常小,但是存在錯誤率,開發者在進行數據結構選型時只需要確認如下兩條:

  • 只爲了計算獨立總數,不需要獲取單條數據。
  • 可以容忍一定誤差率,畢竟 HyperLogLog 在內存佔用量上有很大優勢。

發佈訂閱

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

命令

發佈消息

publish channel message,返回結果爲訂閱者的個數。

訂閱消息

subscribe channel [channel..],訂閱者可以訂閱一個或多個頻道。

客戶端在執行訂閱命令後會進入訂閱狀態,只能接收 subscribe、psubscribe、unsubscribe、punsubscribe 的四個命令。新開啓的訂閱客戶端,無法收到該頻道之前的消息,因爲 Redis 不會對法捕的消息進行持久化。

和很多專業的消息隊列系統如 Kafka、RocketMQ 相比,Redis 的發佈訂閱略顯粗糙,例如無法實現消息堆積和回溯,但勝在足夠簡單,如果當前場景可以容忍這些缺點,也是一個不錯的選擇。

取消訂閱

unsubscribe [channel [channel...]]

客戶端可以通過 unsubscribe 命令取消對指定頻道的訂閱,取消成功後不會再收到該頻道的發佈消息。

按照模式訂閱和取消訂閱

psubscribe pattern [pattern...]

punsubscribe pattern [pattern...]

這兩種命令支持 glob 風格,例如訂閱所有以 it 開頭的頻道:psubscribe it*

查詢訂閱

查看活躍的頻道:pubsub channels [pattern],活躍頻道是指當前頻道至少有一個訂閱者。

查看頻道訂閱數:pubsub numsub [channel ...]

查看模式訂閱數:pubsub numpat


使用場景

聊天室、公告牌、服務之間利用消息解耦都可以使用發佈訂閱模式,以服務器解耦爲例:視頻管理系統負責管理視頻信息,用戶通過各種客戶端獲取視頻信息。

假如視頻管理員在視頻管理系統中對視頻信息進行了更新,希望及時通知給視頻服務端,就可以採用發佈訂閱模式,發佈視頻信息變化的消息到指定頻道,視頻服務訂閱這個頻道及時更新視頻信息,通過這種方式實現解耦。

視頻服務訂閱 video:changes 頻道:

subscribe video:changes

視頻管理系統發佈消息到 video:changes 頻道:

publish video:changes "video1,video3,video5"

視頻服務收到消息,對視頻信息進行更新…


GEO

Redis 3.2 版本提供了 GEO 地理信息定位功能,支持存儲地理位置信息用來實現諸如附近位置、搖一搖這一類依賴於地理位置信息的功能。

增加地理位置信息

geoadd key longitude latitude member [longitude latitude member...]

longitude、latitude、member 分別是該地理位置的經度、緯度、成員。

例如添加北京的地理位置信息:

geoadd cities:locations 116.28 39.55 beijing

返回結果表示成功添加的個數,如果需要更新地理位置信息仍然可以使用 geoadd 命令,返回結果爲 0。

獲取地理位置信息

getpos key member [member...]

獲取兩個地理位置的距離

geodist key member1 member2 [unit]

其中 unit 代表返回結果的單位,包含 m 米、km 公里、mi 英里、ft 英尺。

刪除地理位置信息

zrem key member

GEO 沒有提供刪除成員的命令,但由於它底層是 zset,可以使用 zrem 刪除。


總結

慢查詢中有兩個重要參數 slowlog-log-slower-than 和 slowlog-max-len。

慢查詢不包括命令網絡傳輸和排隊時間。

有必要將慢查詢定期存放。

Pipeline 可以有效減少 RTT 次數,但每次 Pipeline 的命令數量不能無節制。

Redis 可以使用 Lua 腳本創造出原子、高效、自定義命令組合。

Bitmaps 可以用來做獨立用戶統計,有效節省內存。

Bitmaps 中 setbit 一個大的偏移量,由於申請大量內存會導致阻塞。

HyperLogLog 雖然在統計獨立總量時存在一定誤差,但是節省的內存量十分驚人。

Redis 的發佈訂閱相比許多專業消息隊列系統功能較弱,不具備息堆積和回溯能力,但勝在足夠簡單。

Redis 3.2 提供了 GEO 功能,用來實現基於地理位置信息的應用,底層實現是 zset。


客戶端

客戶端通信協議

客戶端與服務端的通信協議是在 TCP 協議之上構建的,Redis 制定了 RESP(Redis 序列化協議)實現客戶端與服務端的正常交互,這種協議簡單高效,既能夠被機器解析,又容易被人類識別。例如客戶端發送一條 set hello world 命令給服務端,按照 RESP 的標準客戶端需要將其封裝爲如下格式:

*3//表示有3個參數
$3//表示參數的字節數
SET
$5
hello
$5
world

這樣 Redis 服務端能夠按照 RESP 將其解析爲 set hello world,執行後回覆的格式爲 +OK。

返回結果格式:

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

有 RESP 提供的發送命令和返回結果的協議格式,各種編程語言就可以利用其來實現相應的 Redis 客戶端。


Java 客戶端 Jedis

在 maven 項目中添加相應的依賴即可:

<dependencies>
    <dependency>
        <groupId>redis.clients</groupId>
        <artifactId>jedis</artifactId>
        <version>2.8.1</version>
    </dependency>
</dependencies>

啓動本地 Redis 服務器後,通過以下代碼連接 Redis:

Jedis jedis = new Jedis("127.0.0.1", 6379);

操作基本數據結構

操作五種數據結構的示例:

public static void main(String[] args) {
    Jedis jedis = new Jedis("127.0.0.1", 6379);
    // string
    String set = jedis.set("hello", "world");
    System.out.println(set);//OK

    String hello = jedis.get("hello");
    System.out.println(hello);//world

    Long counter = jedis.incr("counter");
    System.out.println(counter);//1

    // hash
    jedis.hset("hash", "f1", "v1");
    jedis.hset("hash", "f2", "v2");
    Map<String, String> hash = jedis.hgetAll("hash");
    System.out.println(hash);//{f1=v1, f2=v2}

    // list
    jedis.rpush("list", "1");
    jedis.rpush("list", "2");
    jedis.rpush("list", "3");
    List<String> list = jedis.lrange("list", 0, -1);
    System.out.println(list);//[1, 2, 3]

    // set
    jedis.sadd("set", "a");
    jedis.sadd("set", "b");
    jedis.sadd("set", "a");
    Set<String> set1 = jedis.smembers("set");
    System.out.println(set1);//[b, a]

    // zset
    jedis.zadd("zset", 33, "tom");
    jedis.zadd("zset", 66, "peter");
    jedis.zadd("zset", 99, "james");
    Set<Tuple> zset = jedis.zrangeWithScores("zset", 0, -1);
    System.out.println(zset);//[[[116, 111, 109],33.0], [[112, 101, 116, 101, 114],66.0], [[106, 97, 109, 101, 115],99.0]]

}

序列化對象

序列化 Java 對象,導入以下依賴:

<dependency>
    <groupId>com.dyuproject.protostuff</groupId>
    <artifactId>protostuff-runtime</artifactId>
    <version>1.0.11</version>
</dependency>

<dependency>
    <groupId>com.dyuproject.protostuff</groupId>
    <artifactId>protostuff-core</artifactId>
    <version>1.0.11</version>
</dependency>

創建一個俱樂部實體類:

public class Club implements Serializable {

    private int id;
    private String name;
    private String info;
    private Date createDate;
    private int rank;

    public Club(int id, String name, String info, Date createDate, int rank) {
        this.id = id;
        this.name = name;
        this.info = info;
        this.createDate = createDate;
        this.rank = rank;
    }
    
    public int getId() {
        return id;
    }

    public void setId(int id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getInfo() {
        return info;
    }

    public void setInfo(String info) {
        this.info = info;
    }

    public Date getCreateDate() {
        return createDate;
    }

    public void setCreateDate(Date createDate) {
        this.createDate = createDate;
    }

    public int getRank() {
        return rank;
    }

    public void setRank(int rank) {
        this.rank = rank;
    }
    
    @Override
    public String toString() {
        return "Club{" +
                "id=" + id +
                ", name='" + name + '\'' +
                ", info='" + info + '\'' +
                ", createDate=" + createDate +
                ", rank=" + rank +
                '}';
    }
}

序列化工具類:

public class ProtoStuffSerializeUtils {

    private static Schema<Club> schema = RuntimeSchema.createFrom(Club.class);

    public static byte[] serialize(final Club club) {
        final LinkedBuffer buffer = LinkedBuffer.allocate(LinkedBuffer.DEFAULT_BUFFER_SIZE);
        try {
            return serializeInternal(club, schema, buffer);
        } catch (final Exception e) {
            throw new IllegalStateException(e.getMessage(), e);
        } finally {
            buffer.clear();
        }
    }

    public static Club deserialize(final byte[] bytes) {
        try {
            Club club = deserializeInternal(bytes, schema.newMessage(), schema);
            if (club != null) {
                return club;
            }
        } catch (final Exception e) {
            throw new IllegalStateException(e.getMessage(), e);
        }
        return null;
    }

    private static <T> byte[] serializeInternal(final T source, final Schema<T> schema, final LinkedBuffer buffer) {
        return ProtostuffIOUtil.toByteArray(source, schema, buffer);
    }

    private static <T> T deserializeInternal(final byte[] bytes, final T result, final Schema<T> schema) {
        ProtostuffIOUtil.mergeFrom(bytes, result, schema);
        return result;
    }
}

測試:

public static void main(String[] args) {
    Jedis jedis = new Jedis("127.0.0.1", 6379);

    // 序列化
    String key ="club:1";
    Club club = new Club(1, "LA", "湖人", new Date(), 1);
    byte[] clubBytes = ProtoStuffSerializeUtils.serialize(club);
    jedis.set(key.getBytes(), clubBytes);

    // 反序列化
    byte[] resultBytes = jedis.get(key.getBytes());
    Club resultClub = ProtoStuffSerializeUtils.deserialize(resultBytes);
    System.out.println(resultClub);//Club{id=1, name='LA', info='湖人', createDate=Sat Jun 20 12:26:54 CST 2020, rank=1}
}

連接池

之前的連接方式是直連方式,所謂直連是指 Jedis 每次都會新建 TCP 連接,使用後再斷開連接,對於頻繁訪問 Redis 的場景顯然不是高效的使用方式。

生產方式中一般使用連接池的方式對 Jedis 連接進行管理,所有 Jedis 對象預先放在池子中,每次要連接 Redis,只需要在池子中借,用完了再歸還給池子。

客戶端連接 Redis 使用 TCP 連接,直連的方式每次需要建立 TCP 連接,而連接池的方式是可以預先初始化好的 Jedis 連接,所以每次只需要從 Jedis 連接池借用即可,而借用和歸還操作是在本地進行的,只有少量的併發同步開銷,遠遠小於新建 TCP 連接的開銷。另外直連的方式無法限制 Jedis 對象的個數,在極端情況下可能會造成連接泄露,而連接池的形式可以有效的保護和控制資源的使用。

優點 缺點
直連 簡單方便,適用於少量長期連接的場景。 存在每次連接關閉 TCP 連接的開銷,資源無法控制可能出現連接泄露,Jedis 對象線程不安全
連接池 無需每次連接都生成 Jedis 對象降低開銷,使用連接池的形式保護和控制資源的使用 相對於直連比較麻煩,尤其在資源的管理上需要很多參數來保證,一旦規劃不合理也會出現問題

使用 Jedis 連接池操作的示例:

public static void main(String[] args) {
    // 使用默認連接池配置
    GenericObjectPoolConfig poolConfig = new GenericObjectPoolConfig();
    // 初始化連接池
    JedisPool jedisPool = new JedisPool(poolConfig, "127.0.0.1", 6379);
    // 獲取 Jedis 對象
    Jedis jedis = null;
    try{
        jedis = jedisPool.getResource();
        //。。。
    }catch (Exception e){
        e.printStackTrace();
    }finally {
        if(jedis!=null){
            // 不是關閉連接,而是歸還連接池
            jedis.close();
        }
    }
}

客戶端管理

Redis 提供了客戶端相關 API 對其狀態進行監控和管理。

client list

client list 命令能列出與 Redis 服務端相連的所有客戶端連接信息,輸出的每一行代表一個客戶端信息,每行包括了十幾個屬性,重要的屬性解釋:

  • id:客戶端連接的唯一標識,這個 id 隨着 Redis 的連接自增,重啓 Redis 後會重置爲 0。

  • addr:客戶端連接的 ip 和端口。

  • fd:socket 的文件描述符,與 lsof 命令結果中的 fd 是同一個,如果 fd = -1代表當前客戶端不是外部客戶端,而是 Redis 內部的僞裝客戶端。

  • name:客戶端的名字。

  • 輸入緩衝區:qbuf、qbuf-free。

    Redis 爲每個客戶端分配了輸入緩衝區,它的作用是將客戶端發送的命令臨時保存,同時 Redis 會從輸入緩衝區拉取命令並執行,輸入緩衝區爲客戶端發送命令到 Redis 執行命令提供了緩衝功能。qbuf 和 qbuf-free 分別代表這個緩衝區的總容量和剩餘容量,Redis 沒有提供相應的配置來規定每個緩衝區的大小,輸入緩衝區會根據輸入內容的大小的不同動態調整,只是要求每個客戶端緩衝區的大小不能超過 1G,超過後客戶端將被關閉。

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

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

    輸入緩衝區過大主要是因爲 Redis 的處理速度跟不上輸入緩衝區的輸入速度,並且每次進入輸入緩衝區的命令包含了大量 bigkey,從而造成了輸入緩衝區過大,還有一種情況就是 Redis 發生了阻塞,短期不能處理命令,造成客戶端輸入的命令擠壓在了緩衝區。

    監控輸入緩衝區異常有兩種方法:

    • 定期執行 client list 命令,收集 qbuf 和 qbuf-free 找到異常的連接記錄並分析,找出可能出問題的客戶端。該方法可以精確分析每個客戶端定位問題,但執行速度慢。
    • 通過 info 命令的 info clients 模塊,找到最大的輸入緩衝區,設置報警閾值。該方法執行速度快,但是不能精確定位客戶端。
  • 輸出緩衝區:obl、oll、omem

    Redis 爲每個客戶端分配了輸出緩衝區,它的作用是保存命令執行的結果返回給客戶端,爲 Redis 和客戶端交互返回結果提供緩衝。輸出緩衝區按照客戶端的不同分爲普通客戶端、發佈訂閱客戶端、slave 客戶端。obl 代表固定緩衝區的長度,oll 代表動態緩衝區列表的長度,omem 代表使用的字節數。

    監控輸出緩衝區的方法和輸入緩衝區一樣,提供 client list 和 info clients。


client setName 和 client getName

client setName 用於給客戶端設置名字,這樣比較容易標識出客戶端的來源。

如果想直接查看當前客戶端的 name,可以使用 client getName。

client setName 和 client getName 命令可以作爲標識客戶端來源的一種方式,但是通常來說在 Redis 只有一個應用方使用的情況下,IP 和端口作爲表示會更加清晰。當多個應用共同使用一個 Redis,那麼此時 client setName 可以作爲標識客戶端的一個依據。


client kill

client kill ip:port

此命令用於殺掉指定 IP 地址和端口號的客戶端,由於一些原因需要手動殺掉客戶端連接時,可以使用該命令。


client pause

client pause timeout

該命令用於阻塞客戶端,timeout 是阻塞時間,單位爲毫秒,在此期間客戶端連接將被阻塞。

適用場景:

  • client pause 只對普通和發佈訂閱客戶端有效,對於主從複製無效,因此可以讓主從複製保持一致。
  • 可以用一種可控的方式將客戶端連接從一個 Redis 節點切換到另一個 Redis 節點。

monitor

monitor 命令用於監控 Redis 正在執行的命令。但是一旦併發量過大,monitor 客戶端的輸出緩衝會暴漲,可能瞬間會佔用大量內存。


客戶端相關配置

timeout:檢測客戶端空閒連接的超時時間,一旦空閒時間到了 timeout,客戶端將被關閉,如果設置爲 0 就不進行檢測。

maxclients:客戶端最大連接數,這個參數會受到操作系統的限制。

tcp-keepalive:檢測 TCP 連接活性的週期,默認值爲 0,也就是不進行檢測。如果需要設置,建議爲 60,Redis 每隔一分鐘會對它創建的 TCP 連接進行活性檢測,防止大量死連接佔用系統資源。

tcp-backlog:TCP 三次握手後,會將接受的連接放入隊列中,tcp-backlog 就是隊列的大小,默認值是 511,通常來說這個參數不需要調整。


客戶端常見異常

無法從連接池獲取連接

JedisPool 中的 Jedis 對象個數是有限的,默認是 8 個。如果對象全部被佔用並且沒有歸還,調用者借用 Jedis 時就會阻塞等待,如果超過了最大等待時間 maxWaitMills 就會拋出異常。

還有一種情況就是設置了 blockWhenExhausted = false,那麼調用者發現池子中沒有資源時會立即拋出異常而不進行等待。

造成沒有資源的原因:

  • 客戶端:高併發情況下連接池設置過小,供不應求,但正常情況下只需要比默認的 8 個大一點即可。
  • 客戶端:沒有正確使用連接池,例如沒有釋放。
  • 客戶端:存在慢查詢操作,這些慢查詢持有的 Jedis 對象歸還速度會比較慢。
  • 服務端:客戶端正常,服務端由於一些原因造成了客戶端命令執行過程的阻塞。

客戶端讀寫超時

Jedis 在調用 Redis 時,如果出現了讀寫超時,會拋出異常,造成該異常的原因:

  • 讀寫超時時間設置得過短。
  • 命令本身比較慢。
  • 客戶端與服務端網絡不正常。
  • Redis 自身發生了阻塞。

客戶端連接超時

Jedis 在調用 Redis 時,如果出現了連接超時,會拋出異常,造成該異常的原因:

  • 連接超時時間設置得過短。
  • Redis 發生阻塞,造成 tcp-backlog 已滿。
  • 客戶端與服務端網絡不正常。

客戶端緩衝區異常

Jedis 在調用 Redis 時,如果出現了客戶端數據流異常,會拋出異常,造成該異常的原因:

  • 輸出緩衝區滿。
  • 長時間閒置連接被服務端主動斷開。
  • 不正常併發讀寫:Jedis 對象同時被多個線程併發操作,可能會出現該問題。

Lua 腳本正在執行

如果 Redis 當前正在執行 Lua 腳本,並且超過了 lua-time-limit,此時 Jedis 調用 Redis 時就會拋出異常。

加載持久化文件

Jedis 調用 Redis 時,如果 Redis 正在加載持久化文件,那麼會拋出異常。

Redis 使用內存超過 maxmemory

Jedis 執行寫操作時,如果 Redis 的使用內存大於 maxmemor 的設置,會拋出異常。

客戶端連接數過大

如果客戶端連接數超過了 maxclients,新申請的連接會拋出異常。此時新的客戶端連接執行任何命令都會返回錯誤結果。

一般可從兩方面解決:

  • 客戶端:如果 maxclients 參數不是很小的化,應用方的客戶端連接數基本不會超過 maxclients,通常來看是由於應用方對於 Redis 客戶端使用不當造成的。此時如果應用方是分佈式結構的話,可以通過下線部分應用節點使得 Redis 的連接數先降下來。從而讓絕大部分節點可以正常允許,再通過查找程序 bug 或調整 maxclients 進行問題的修復。
  • 服務端:如果客戶端無法處理,而當前 Redis 爲高可用模式,可以考慮做故障轉移。

客戶端案例分析

Redis 內存陡增

現象:

服務端:Redis 主節點內存陡增,幾乎用滿 maxmemory,而從節點內存並沒有變化。

客戶端:客戶端產生了 OOM 異常,也就是 Redis 主節點使用的內存已經超過了 maxmemory 的設置,無法寫入新的數據。

分析原因:

① 確實有大量寫入,但是主從複製出現問題。

② 如果主從複製正常,可以排查十分由客戶端緩衝區造成主節點內存陡增,使用 info clinets 查詢相關信息。如果客戶端緩衝隊列值很大,通過 client list 命令找到 omem 不正常的連接,一般來說爲 0,因此不爲 0 就是不正常的連接。有可能是因爲客戶端執行 monitor 造成的。

處理方法:

使用 client kil 殺掉這個連接,讓其他客戶端恢復正常即可。需要注意的有三點:

  • 從運維層面禁止 monitor 命令,例如 rename-command 命令重置 monitor 爲一個隨機字符串。
  • 禁止開發人員在生產中使用 monitor。
  • 限制輸出緩衝區的大小。

客戶端週期性超時

現象:

客戶端:客戶端出現大量超時,並且是週期性的。

服務端:沒有明顯的異常,只是有一些慢查詢操作。

分析原因:

① 網絡:服務端和客戶端之間的網絡出現週期性問題,網絡正常。

② Redis 本身:觀察 Redis 的日誌統計,無異常。

③ 客戶端:發現只要慢查詢出現,就會連接超時,因此是慢查詢導致了連接超時。

處理方法:

  • 從運維層面,監控慢查詢,超過閾值就發出警報。
  • 從開發層面,避免不正確的使用。

總結

RESP 保證了客戶端與服務端的正常通信,是各種編程語言開發客戶端的基礎。

要選擇社區活躍客戶端,在實際項目中使用穩定版本的客戶端。

區分 Jedis 直連和連接池的區別,在生產環境應該使用連接池。

Jedis.close() 在直連下是關閉連接,在連接池則是歸還連接。

客戶端輸入緩衝區不能配置,強制限制在 1G 以內,但是不會受到 maxmemory 限制。

客戶端輸出緩衝區支持普通客戶端、發佈訂閱客戶端、複製客戶端配置,但不會受到 maxmemory 限制。

Redis 的 timeout 配置可以自動關閉閒置客戶端,tcp-keepalive 參數可以週期性檢查關閉無效 TCP 連接。

monitor 命令雖然好用,但是在高併發下存在輸出緩衝區暴漲的可能性。

info clients 幫助開發和運維找到客戶端可能存在的問題。


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章