Redis Redis 常見數據類型

Redis 提供了 5 種數據結構,理解每種數據結構的特點對于 Redis 開發運維非常重要,同時掌握每種數據結構的常見命令,會在使用?Redis 的時候做到游刃有余。

一、預備知識

官方文檔:Commands | Docs (redis.io)

?1、最核心的兩個命令

Redis?是按照鍵值對的方式存儲數據的。

  1. get:根據 key 來取 value
  2. set:把 key 和 value 存儲進去

注意:這里的 key 和 value 本質上都是字符串。

對于上面的 key value,不需要加上引號就是代表字符串的類型(加上也是可以的,單引號或雙引號都行)

直接按下 Tab,可以發現:系統會為我們自動補全命令(大寫,Redis 中的命令不區分大小寫)。

?get 命令直接輸入 key,就能得到 value。如果當前的 key 不存在,會返回 nil(nil 和 null / NULL 是一個意思)。

在學習?5 種數據結構之前,了解一下 Redis 的一些全局命令、數據結構和內部編碼、單線程命令處理機制是十分必要的,它們能為后面內容的學習打下一個良好的基礎。

主要體現在兩個方面:

  1. Redis 的命令有上百個,如果純靠死記硬背比較困難,但是如果理解 Redis 的一些機制,會發現這些命令有很強的通用性。

  2. Redis 不是萬金油,有些數據結構和命令必須在特定場景下使用,一旦使用不當可能對 Redis 本身或者應用本身造成致命傷害。

?2、基本全局命令

Redis 有 5 種數據結構,它們都是鍵值對中的值,對于鍵來說有一些通用的命令(能夠搭配任意一個數據結構來使用的命令),叫作全局命令。

(1)KEYS

用來查詢當前服務器上匹配的 key。通過一些特殊符號(通配符)來描述 key 的模樣,匹配上述模樣的 key 就能被查詢出來。

?返回所有滿足樣式(pattern)的 key。支持如下統配樣式:

  • h?llo 匹配 hello , hallo 和 hxllo(? 匹配任意一個字符)
  • h*llo 匹配 hllo 和 heeeello(* 匹配 0 個或者多個任意字符)
  • h[ae]llo 匹配 hello 和 hallo 但不匹配 hillo(只能匹配到a、e,其它的不行,相當于給出固定選項)
  • h[^e]llo 匹配 hallo , hbllo , ... 但不匹配 hello(排除 e,只有 e 匹配不了,其它的都能匹配)
  • h[a-b]llo 匹配 hallo 和 hbllo(匹配 a~b 這個范圍內的字符,包含兩側邊界)

語法:

KEYS pattern

pattern 表示包含特殊符號的字符串。

命令有效版本:

1.0.0 之后

?時間復雜度:

O(N)

需要把 Redis 里的所有 key 都遍歷一遍,依次去看每一個 key 是否符合 pattern,符合就留下,不符合就跳過。

在生產環境上,一般都會禁止使用 keys 命令,尤其是 keys *(生產環境上的 key 可能非常多,而 Redis 是一個單線程的服務器,那么執行 keys * 的時間非常長,就會使 Redis 服務器被阻塞了,而無法給其他客戶端提供服務)。

返回值:

匹配 pattern 的所有 key。

示例:

(2)EXISTS

判斷某個 key?是否存在(也可以一次判斷多個)。

?Redis 支持很多數據結構,指的是一個 value 可以是一些復雜的數據結構。Redis 自身的這些鍵值對是通過哈希表的方式來組織的。Redis 具體的某個值又可以是一些數據結構。

語法:

EXISTS key [key ...]

命令有效版本:

1.0.0 之后

時間復雜度:

O(1)

Redis 組織這些 key 是按照哈希表的方式組織的,哈希表查詢的復雜度就是 O(1),嚴謹來說,應該是查詢 N 個 key 就是 O(N)。

返回值:

key 存在的個數。

?示例:

Redis 是一個客戶端服務器結構的程序,客戶端和服務器之間通過網絡來進行通信。

兩種寫法的區別:

紅色框(一次請求和一次響應):

藍色框(一次請求和一次響應 + 一次請求和一次響應,四次網絡通信,也就是兩個輪次):

分開的寫法會產生更多輪次的網絡通信(效率低、成本高,和直接操作內存比)。

(3)DEL

刪除指定的 key。

Redis 的主要應用場景就是作為緩存。此時,Redis 里存的只是一個熱點數據,全量數據是在 MySQL 數據庫中的。此時,如果刪除了 Redis 中的幾個 key,一般來說問題不大。但是,如果把 Redis 中一大半數據甚至是全部數據全刪了,那么影響就很大(Redis 本來是幫 MySQL 負重前行的,而現在 Redis 數據沒了,那么大部分的請求就會直接打給 MySQL,然后就容易把 MySQL 搞掛)。所以在相比之下,如果是 MySQL 中誤刪了一個數據,都可能影響很大。

如果把 Redis 作為數據庫,此時誤刪數據的影響也是很大。

如果是把 Redis 作為消息隊列(mq),此時誤刪數據的影響就應該根據具體問題來具體分析了。

語法:

DEL key [key ...]

命令有效版本:

1.0.0 之后

時間復雜度:

O(1)

返回值:

刪除掉的 key 的個數。

示例:

(4)EXPIRE

為指定的 key(key 已存在,否則設置失敗)添加秒級的過期時間(Time To Live TTL)。

PEXPIRE(毫秒級)

key 的存活時間超出這個指定值就會被自動刪除。業務場景舉例:手機發送驗證碼(60s)、外賣優惠券(7天)、基于 Redis 實現的分布式鎖(給 Redis 里寫一個特殊的 key value,刪除就是解鎖。為了避免出現不能正確解鎖的情況,通常都會在加鎖的時候設置過期時間)。?

語法:

EXPIRE key seconds

命令有效版本:

1.0.0 之后

時間復雜度:

O(1)

返回值:

1 表示設置成功,0 表示設置失敗。

示例:

(5)TTL

?獲取指定 key 的過期時間秒級

IP 協議報頭中有一個字段:TTL,它不是用時間來衡量過期的,而是用次數。

語法:

TTL key

命令有效版本:

1.0.0 之后

時間復雜度:

O(1)

返回值:

剩余過期時間。-1 表示沒有關聯過期時間,-2 表示?key 不存在。

示例:

?

鍵的過期機制:

tips?:EXPIRE 和 TTL 命令都有對應的?持毫秒為單位的版本:PEXPIRE 和 PTTL。

Redis 的 key 的過期策略是如何實現的呢?(一個 Redis 中可能同時存在很多 key,這些 key 中可能有很大一部分都有過期時間,那么此時 Redis 如何知道哪些 key 已經過期要被刪除,哪些 key 還沒過期呢?)?

如果直接遍歷所有的 key 顯然是不行的,效率非常低。Redis 整體的策略是:

  • 定期刪除(每次抽取一部分進行驗證過期時間,保證這個抽取檢查的過程足夠快)
  • 惰性刪除(假設這個 key 已經到過期時間了,但是暫時還沒刪除,key 還存在,緊接著后面又有一次訪問,正好用到了這個 key,于是這次訪問就會讓 Redis 服務器觸發刪除 key 的操作,同時再返回一個 nil)

為什么這里對于定期刪除的時間有明確的要求呢?

因為 Redis 是單線程的程序,它的主要任務有:處理每個命令的任務、掃描過期的 key 等等,如果掃描過期 key 消耗的時間太多,那么正常處理請求命令就被阻塞了(產生了類似于執行 keys* 這樣的效果。

雖然有上面講到的兩種策略結合,但整體的結果一般,仍然可能會有很多過期的 key 被殘留,沒有及時刪除掉。Redis 為了對上述進行補充,還提供了一系列的內存淘汰策略。?

如果有多個 key 過期,也可以通過一個定時器(基于優先級隊列或者時間輪都可以實現比較高效的定時器)來高效 / 節省 CPU 的前提下來處理多個 key。但 Redis 并沒有采取定時器的方式來實現過期 key 刪除。(個人猜測:基于定時器實現,就需要引入多線程,但 Redis 的早起版本就奠定了單線程的基調,如果引入多線程就打破了初衷)。

定時器:在某個時間到達之后,執行指定的任務,它是基于優先級隊列 / 堆的(一般的隊列是先進先出,而優先級隊列則是按照指定的優先級(自定義)先出)。在 Redis 過期 key 的場景中,就可以通過 “過期時間越早,就是優先級越高”。此時定時器只需要分配一個線程,不需要遍歷所有的 key,只需要讓這個線程去檢查隊首元素,看是否過期即可。如果隊首元素還沒過期,那么后續元素一定沒過期。另外,在掃描線程檢查隊首元素過期時間時,也不能檢查的太頻繁,此時可以根據時刻和隊首元素的過期時間設置一個等待,當時間差不多到了,系統再喚醒這個線程(可以節省 CPU 的開銷)。

萬一在線程休眠時,來了一個新的任務呢?可以在新任務添加時,喚醒剛才的線程,重新檢查一下隊首元素,再根據時間差距重新調整阻塞時間即可。

基于時間輪實現的定時器(把時間劃分成很多小段,具體劃分的粒度看實際需求):

每個小段都掛著一個鏈表,每個鏈表都代表一個要執行的任務(相當于一個函數指針以及對應的參數)。

假設需要添加一個 key,這個 key 在 300ms 之后過期。此時這個指針就會每隔固定的時間間隔(此處約定時 100ms)往后走一個,每次走到一個格子就會把這個格子上鏈表的任務嘗試執行一下。

對于時間輪來說,每個格子是多少時間,一共有多少個格子都是需要根據實際場景來靈活調配的。

(6)TYPE

返回 key 對應的數據類型。

此處 Redis 所有的 key 都是 string,key 對應的 value 可能會存在多種類型。

語法:

TYPE key

命令有效版本:

1.0.0 之后?

時間復雜度:

O(1)

返回值:

none,string,list,set,zset,hash,stream

Redis 作為消息隊列時,使用 stream 作為返回值類型。

在 Redis 中,上述幾種類型的操作方式差別很大,使用的命令都是完全不同的。

示例:

?

?3、數據結構和內部編碼

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

Redis 的 5 種主要的數據類型:

Redis 底層在實現上述數據結構時,會在源碼底層針對上述實現進行特定的優化(內部具體實現的數據結構(編碼方式)還會有變數),來達到節省時間 / 空間的效果。

實際上 Redis 針對每種數據結構都有自己的底層內部編碼實現,而且是多種實現,這樣 Redis 會在合適的場景選擇合適的內部編碼,如下表所示:

Redis 數據結構和內部編碼:

從 Redis 3.2 開始,list 引入了新的實現方式:quicklist,它同時兼顧了 linkedlist 和 ziplist 的優點。quicklist 就是一個鏈表,每個元素又是一個 ziplist(空間和效率都折中兼顧到),類似于 C++ 中的 std::deque。

可以看到每種數據結構都有至少兩種以上的內部編碼實現,例如 list 數據結構包含了 linkedlist 和 ziplist 兩種內部編碼。同時有些內部編碼,例如 ziplist,可以作為多種數據結構的內部實現,可以通過 object encoding 命令查詢內部編碼:

Redis 這樣設計有兩個好處

  1. 可以改進內部編碼,而對外的數據結構和命令沒有任何影響,這樣一旦開發出更優秀的內部編碼,無需改動外部數據結構和命令。例如 Redis 3.2 提供了 quicklist,結合了 ziplist 和 linkedlist 兩者的優勢,為列表類型提供了一種更為優秀的內部編碼實現,而對用戶來說基本無感知。
  2. 多種內部編碼實現可以在不同場景下發揮各自的優勢,例如 ziplist 比較節省內存,但是在列表元素比較多的情況下,性能會下降,這時候 Redis 會根據配置選項將列表類型的內部實現轉換為 linkedlist,整個過程用戶同樣無感知。

4、單線程架構

Redis 使用了單線程架構來實現高性能的內存數據庫服務。

Redis 只使用一個線程處理所有的命令請求,并不是說一個 Redis 服務器進程內部真的就只有一個線程,其實也有多個線程,但這多個線程是在處理網絡 IO。

下面將先通過多個客戶端命令調用的例子說明 Redis 單線程命令處理機制,接著分析 Redis 單線程模型為什么性能如此之高,最終給出為什么理解單線程模型是使用和運維 Redis 的關鍵。


(1)引出單線程模型

現在開啟了兩個 redis-cli 客戶端同時執行命令。

?客戶端 1?對 counter 做自增操作:

127.0.0.1:6379> incr counter

客戶端 2?對 counter 做自增操作:

127.0.0.1:6379> incr counter

宏觀上,2 個客戶端是同時請求 Redis 服務的:?

incr 就是 increase 自增,作用是把 key 的 value 進行 +1 操作。

線程安全問題:在多線程中,針對類似于這樣的場景,兩個線程嘗試同時對同一個變量進行自增,表面上看是自增兩次,實際上可能只自增了一次。

從客戶端發送的命令經歷:發送命令、執行命令、返回結果三個階段,其中重點關注第 2 步。所謂的 Redis 是采用單線程模型執行命令的是指:雖然兩個客戶端看起來是同時要求 Redis 去執行命令的,也相當于 “并發” 的發起了上述的請求。但從微觀角度來看,Redis 是串行 / 順序執行這多個命令的,這些命令還是采用線性方式去執行的,只是原則上命令的執行順序是不確定的,但一定不會有兩條命令被同步執行,如下圖(Redis 的單線程模型)所示,可以想象 Redis 內部只有一個服務窗口,多個客戶端按照它們達到的先后順序被排隊在窗口前,依次接受 Redis 的服務,所以兩條 incr 命令無論執行順序,結果一定是 2,不會發生并發問題,這個就是 Redis 的單線程執行模型,保證了當前收到的這多個請求是串行執行的,所以不會發生上述類似的線程安全問題。多個請求同時到達 Redis 服務器,也是要先在隊列中排隊,再等待 Redis 服務器一個個的取出里面的命令再執行。

微觀上,客戶端發送命令的時間有先后次序的:

Redis 的單線程模型:

Redis 雖然是單線程模型,但為什么效率這么高,速度還能這么快呢?(參照物:MySQL、Oracle、Sql Server)

?通常來講,單線程處理能力要比多線程差,例如有 10000 公斤貨物,每輛車的運載能力是每次 200 公斤,那么要 50 次才能完成;但是如果有 50 輛車,只要安排合理,只需要依次就可以完成任務。那么為什么 Redis 使用單線程模型會達到每秒萬級別的處理能力呢?可以將其歸結為三點:

  1. Redis 是純內存訪問,而數據庫是訪問硬盤。Redis 將所有數據放在內存中,內存的響應時長大約為 100ns,這是 Redis 達到每秒萬級別訪問的重要基礎。
  2. Redis 的核心功能比數據庫的核心功能更簡單。(數據庫對于數據的插入刪除查詢... 都有更復雜的功能支持,這樣的功能勢必要花費更多的開銷。比如,針對插入刪除,數據庫中的各種約束都會使數據庫做額外的工作)
  3. Redis 是單線程模型,避免了線程切換和競態產生的消耗。Redis 的每個基本操作都是 “短平快” 的,就是簡單操作一下內存數據,不是特別消耗 CPU 的操作。就算搞多個線程,提升也不大。單線程可以簡化數據結構和算法的實現,讓程序模型更簡單;其次多線程避免了在線程競爭同一份共享數據時帶來的切換和等待消耗。
  4. 非阻塞 IO。Redis 使用 epoll 作為 I/O 多路復用技術的實現,再加上 Redis 自身的事件處理模型將 epoll 中的連接、讀寫、關閉都轉換為事件,不在網絡 I/O 上浪費過多的時間,如下圖所示。
  5. (本質上就是一個線程可以管理多個 socket。針對 TCP 來說,服務器這邊每次要服務一個客戶端都需要給這個客戶端安排一個 socket。假設一個服務器服務多個客戶端,同時就會有很多個 socket,但這些 socket 上并不是無時不刻都在傳輸數據。很多情況下,每個客戶端和服務器之間的通信并沒有那么頻繁,此時這么多的 socket 大部分時間都是靜默的,上面是沒有數據需要傳輸的。也就是說,同一時刻只有少數 socket 是活躍的)。

Redis 使用?I/O 多路復用模型:

雖然單線程給 Redis 帶來很多好處,但還是有一個致命的問題:對于單個命令的執行時間都是有要求的。如果某個命令執行過長,會導致其他命令全部處于等待隊列中,遲遲等不到響應,造成客戶端的阻塞,對于 Redis 這種高性能的服務來說是非常嚴重的,所以 Redis 是面向快速執行場景的數據庫。

二、String 字符串

字符串類型是 Redis 最基礎的數據類型,關于字符串需要特別注意:

  1. 首先 Redis 中所有的鍵的類型都是字符串類型,而且其他幾種數據結構也都是在字符串類似基礎上構建的,例如列表和集合的元素類型是字符串類型,所以字符串類型能為其他 4 種數據結構的學習奠定基礎。
  2. 其次,如下圖所示,字符串類型的值實際可以是字符串,包含一般格式的字符串或者類似 JSON、XML 格式的字符串;數字,可以是整型或者浮點型;甚至是二進制流數據,例如圖片、?頻、視頻等。不過一個字符串的最大值不能超過 512 MB。

由于 Redis 內部存儲字符串完全是按照二進制流的形式保存的,所以 Redis 是不處理字符集編碼問題的,客戶端傳入的命令中使用的是什么字符集編碼,就存儲什么字符集編碼。??

字符串數據類型:?

1、常見命令

(1)SET

將 string 類型的 value 設置到 key 中。如果 key 之前存在,則覆蓋,無論原來的數據類型是什么。之前關于此 key 的 TTL 也全部失效。

語法:

SET key value [expiration EX seconds|PX milliseconds] [NX|XX]

命令有效版本:

1.0.0 之后

時間復雜度:

O(1)

選項:

SET 命令支持多種選項來影響它的行為:

  • EX seconds —— 使用秒作為單位設置 key 的過期時間。
  • PX milliseconds —— 使用毫秒作為單位設置 key 的過期時間。
  • NX —— 只在 key 不存在時才進行設置,創建新的鍵值對,即如果 key 之前已經存在,設置不執行。
  • XX —— 只在 key 存在時才進行設置,讓新的 value 覆蓋舊的 value,可能會改變原來的數據類型,即如果 key 之前不存在,設置不執行。 ?

注意:由于帶選項的 SET 命令可以被?SETNX?、?SETEX?、?PSETEX?等命令代替,所以之后的版本中,Redis 可能進行合并。

返回值:

  • 如果設置成功,返回 OK。
  • 如果由于 SET 指定了 NX 或者 XX 但條件不滿足,SET 不會執行,并返回 (nil)。

FLUSHALL:表示清空所有數據(類似于 MySQL 里的 drop database)。?

示例:


(2)GET

獲取 key 對應的 value。如果 key 不存在,返回 nil。如果 value 的數據類型不是 string,會報錯。

對于 GET 來說,只是支持字符串類型的 value,如果 value 是其他類型,那么使用 GET 獲取就會出錯。

語法:

GET key

命令有效版本:

1.0.0 之后

時間復雜度:

O(1)

返回值:

key 對應的 value,或者 nil 當 key 不存在。

示例:


(3)MGET

一次性獲取多個 key 的值。如果對應的 key 不存在或者對應的數據類型不是 string,返回 nil。

語法:

MGET key [key ...]

命令有效版本:

1.0.0 之后

時間復雜度:

O(N) N 是 key 數量

返回值:

對應 value 的列表。

示例:

(4)MSET

一次性設置多個 key 的值。

語法:

MSET key value [key value ...]

命令有效版本:

1.0.1 之后

時間復雜度:

O(N) N 是 key 數量

返回值:

永遠是 OK

示例:

多次 get VS?單次 mget:

使用?mget / mset 由于可以有效地減少了網絡時間,所以性能相較更高。假設網絡耗時 1 毫秒,命令執行時間耗時 0.1 毫秒,則執行時間如下表所示:

學會使用批量操作,可以有效提高業務處理效率,但是要注意,每次批量操作所發送的鍵的數量也不是無節制的,否則可能造成單一命令執行時間過長,導致 Redis 阻塞。


(5)SETNX

設置 key-value 但只允許在 key 之前不存在的情況下。

語法:

SETNX key value

命令有效版本:

1.0.0 之后

時間復雜度:

O(1)

返回值:

1 表示設置成功,0?表示沒有設置。

示例:

SET、SET NX 和 SET XX 執行流程:

2、計數命令

(1)INCR

將 key 對應的 string 表示的數字加一。如果 key 不存在,則視為 key 對應的 value 是 0。如果 key 對應的 string 不是一個整型或者范圍超過了 64 位有符號整型(相當于 C++ 中的 long long),則報錯。

語法:

INCR key

命令有效版本:

1.0.0 之后

時間復雜度:

O(1)

返回值:

integer 類型的加完后的數值。

示例:

incr 操作的 key 如果不存在,就會把這個 key 的 value 當作 0 來使用。


(2)INCRBY

將 key 對應的 string 表示的數字加上對應的值。如果 key 不存在,則視為 key 對應的 value 是 0。如果 key 對應的 string 不是一個整型或者范圍超過了 64 位有符號整型,則報錯。

語法:

INCRBY key decrement

命令有效版本:

1.0.0 之后

時間復雜度:

O(1)

返回值:

integer 類型的加完后的數值。

示例:


(3)DECR

將 key 對應的 string 表示的數字減一。如果 key 不存在,則視為 key 對應的 value 是 0。如果 key 對應的 string 不是一個整型或者范圍超過了 64 位有符號整型,則報錯。運算結果也是計算之后的值。

語法:

DECR key

命令有效版本:

1.0.0 之后

時間復雜度:

O(1)

返回值:

integer 類型的減完后的數值。

示例:

(4)DECYBY

將 key 對應的 string 表示的數字減去對應的值。如果 key 不存在,則視為 key 對應的 value 是 0。如果 key 對應的 string 不是一個整型或者范圍超過了 64 位有符號整型,則報錯。

語法:

DECRBY key decrement

命令有效版本:

1.0.0 之后

時間復雜度:

O(1)

返回值:

integer 類型的減完后的數值。

示例:

(5)INCRBYFLOAT

將 key 對應的 string 表示的浮點數加上對應的值。如果對應的值是負數,則視為減去對應的值。如果 key 不存在,則視為 key 對應的 value 是 0。如果 key 對應的不是 string,或者不是一個浮點數,則報錯。允許采用科學計數法表示浮點數。

語法:

INCRBYFLOAT key increment

命令有效版本:

2.6.0 之后

時間復雜度:

O(1)

返回值:

加 / 減完后的數值。

示例:

很多存儲系統和編程語言內部使用?CAS 機制實現計數功能,會有一定的 CPU 開銷,但在 Redis 中完全不存在這個問題,因為 Redis 是單線程架構,任何命令到了 Redis 服務端都要順序執行。

3、其他命令

(1)APPEND

如果 key 已經存在并且是?個 string,命令會將 value 追加到原有 string 的后邊。如果 key 不存在,則效果等同于 SET 命令。

語法:

?APPEND KEY VALUE

命令有效版本:

2.0.0 之后

時間復雜度:

O(1) 追加的字符串一般長度較短,可以視為 O(1)

返回值:

追加完成之后 string 的長度。

append 的返回值長度的單位是字節,Redis 的字符串不會對字符編碼做任何處理。

示例:

當前 XShell 終端默認的字符編碼是 utf-8,在終端中輸入漢字之后,也就是按照 utf8 編碼。一個漢字在 utf8 字符集中通常是 3 個字節的。

在啟動 Redis 客戶端時,加上一個 --raw 這樣的選項,就可以使 Redis 客戶端能夠自動的把二進制數據嘗試翻譯。


(2)GETRANGE

返回 key 對應的 string 的子串,由 start 和 end 確定(左閉右閉,是閉區間)。可以使用負數表示倒數,-1 代表倒數第一個字符(下標為 len - 1 的元素),-2 代表倒數第二個,其他的與此類似。超過范圍的偏移量會根據 string 的長度調整成正確的值。

語法:

GETRANGE key start end

命令有效版本:

2.4.0 之后

時間復雜度:

O(N)?N 為 [start, end] 區間的長度,由于 string 通常比較短,可以視為是 O(1)

返回值:

string 類型的子串

示例:

如果字符串中保存的是漢字,此時進行子串切分很可能切出來的就不是完整的漢字了。上述的代碼是強行切出了中間的四個字節,這么一切,切出的結果在 utf8 碼表上就不知道能查出什么了。上述問題在 C++ 中也同樣存在(C++ 字符串中的基本單位是字節),需要我們手動處理。但 Java 就不會(Java 中字符串的基本單位是字符,占 2 個字節的字符),Java 中相當于 String 幫我們把漢字的編碼轉換都處理好了。

(3)SETRANGE

覆蓋字符串的一部分,從指定的偏移開始。

語法:

SETRANGE key offset value

命令有效版本:

2.2.0 之后

時間復雜度:

O(N) N 為 value 的長度,由于一般給的 value 比較短,通常視為 O(1)。

返回值:

替換后的 string 的長度。

示例:

如果 value 是一個中文字符串,進行 setrange 時是可能會出問題的。

這里憑空生成了一個字節,這個字節里的內容就是 "0x00",aaa 就被追加到 "0x00" 后面了。setange 針對不存在的 key 也是可以操作的,不過會把 offset 之前的內容填充成 "0x00"。


(4)STRLEN

獲取 key 對應的 string 的長度。當 key 存放的類型不是 string 時,報錯。

語法:

STRLEN key

命令有效版本:

2.2.0 之后

時間復雜度:

O(1)

返回值:

string 的長度。或者當 key 不存在時,返回 0。

單位是字節。(在 C++ 中,字符串的長度本身就是用字節為單位的)

在 MySQL 中,varchar(N) 的 N 的單位就是字符,MySQL 中的字符也是完整的漢字,這樣的一個字符也可能是多個字節。

示例:


4、命令小結

下表是字符串類型命令的效果、時間復雜度:


5、內部編碼

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

  • int:64 位 / 8 個字節的長整型。
  • embstr:小于等于 39 個字節的字符串,壓縮字符串,適用于表示比較短的字符串。
  • raw:大于 39 個字節的字符串,普通字符串,適用于表示更長的字符串,只是單純的持有字節數組。

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

整型類型示例如下:

短字符串示例如下:

Redis 存儲小數,本質上還是當作字符串來存儲,這就和整數相比差別很大了。整數直接使用 int 來存儲(準確來說是一個 long long(C++)),比較方便進行算術運算。小數則是使用字符串來存儲,意味著每次進行算術運算都需要把字符串轉成小數來進行運算,結果再轉回字符串保存。

長字符串示例如下:


6、典型使用場景

(1)緩存(Cache)功能

下圖是比較典型的緩存使用場景,其中 Redis 作為緩沖層,MySQL 作為存儲層,絕大部分請求的數據都是從 Redis 中獲取。由于 Redis 具有支撐高并發的特性,所以緩存通常能起到加速讀寫和降低后端壓力的作用。

Redis + MySQL 組成的緩存存儲架構:

整體思路:應用服務器訪問數據時,先查詢 Redis。如果 Redis 上數據存在,就直接從 Redis 中取出數據交給應用服務器,不繼續訪問數據庫了。如果 Redis 上數據不存在,此時再讀取 MySQL,把讀到的結果返回給應用服務器,同時把這個數據也寫入到 Redis 中。

上述策略存在一個明顯的問題:隨著時間的推移,會有越來越多的 key 在 Redis 上訪問不到,從而從 MySQL 中讀取并寫入 Redis 了,此時 Redis 中的數據不是會越來越多嗎?

  1. 在把數據寫給 Redis 的同時,給這個 key 設置一個過期時間。
  2. Redis 也在內存不足時,提供了淘汰策略。

下面的偽代碼模擬了上圖的業務數據訪問過程:

A. 假設業務是根據用戶?uid 獲取用戶信息

UserInfo getUserInfo(long uid) {...
}

B. 首先從 Redis 獲取用戶信息,我們假設用戶信息保存在 "user:info:<uid>" 對應的鍵中

// 根據 uid 得到 Redis 的鍵
String key = "user:info:" + uid;// 嘗試從 Redis 中獲取對應的值
String value = Redis 執?命令:get key;// 如果緩存命中(hit)
if (value != null) {// 假設我們的??信息按照 JSON 格式存儲UserInfo userInfo = JSON 反序列化(value);return userInfo;
}

C.?如果沒有從 Redis 中得到用戶信息,及緩存 miss,則進一步從 MySQL 中獲取對應的信息,隨后寫入緩存并返回

// 如果緩存未命中(miss)
if (value == null) {// 從數據庫中,根據 uid 獲取??信息UserInfo userInfo = MySQL 執? SQL:select * from user_info where uid = <uid>// 如果表中沒有 uid 對應的??信息if (userInfo == null) {響應 404return null;}// 將??信息序列化成 JSON 格式String value = JSON 序列化(userInfo);// 寫?緩存,為了防?數據腐爛(rot),設置過期時間為 1 ?時(3600 秒)Redis 執?命令:set key value ex 3600// 返回??信息return userInfo;
}

(2)計數(Counter)功能

許多應用都會使用?Redis 作為計數的基礎工具,它可以實現快速計數、查詢緩存的功能,同時數據可以異步處理或者落地到其他數據源。如下圖所示,例如視頻網站的視頻播放次數可以使用 Redis 來完成:用戶每播放?次視頻,相應的視頻播放數就會自增 1。

記錄視頻播放次數:

這里寫入統計數據倉庫(可能是 MySQL,也可能是 HDFS)的步驟往往是異步的,所以并不是說來一個播放請求,這里就必須立即馬上寫一個數據。

// 在 Redis 中統計某視頻的播放次數
long incrVideoCounter(long vid) {key = "video:" + vid;long count = Redis 執?命令:incr keyreturn counter;
}

實際中要開發一個成熟、穩定的真實計數系統,要面臨的挑戰遠不止如此簡單:防作弊、按照不同維度計數、避免單點問題、數據持久化到底層數據源等。

(3)共享會話(Session)

如下圖所示,一個分布式 Web 服務將用戶的 Session 信息(例如用戶登錄信息)保存在各自的服務器中,但這樣會造成一個問題:出于負載均衡的考慮,分布式服務會將用戶的訪問請求均衡到不同的服務器上,并且通常無法保證用戶每次請求都會被均衡到同一臺服務器上,這樣當用戶刷新一次訪問是可能會發現需要重新登錄,這個問題是用戶無法容忍的。

Session 分散存儲:

為了解決這個問題,可以使用 Redis 將用戶的 Session 信息進行集中管理,如下圖所示,在這種模式下,只要保證 Redis 是高可用和可擴展性的,無論用戶被均衡到哪臺 Web 服務器上,都集中從 Redis 中查詢、更新 Session 信息。

Redis 集中管理 Session:


【手機驗證碼】

很多應用出于安全考慮,會在每次進行登錄時,讓用戶輸入手機號并且配合給手機發送驗證碼,然后讓用戶再次輸入收到的驗證碼并進行驗證,從而確定是否是用戶本人。為了短信接口不會頻繁訪問,會限制用戶每分鐘獲取驗證碼的頻率,例如一分鐘不能超過 5 次,如下圖所示:

短信驗證碼:

此功能可以用以下偽代碼說明基本實現思路:

String 發送驗證碼(phoneNumber) {key = "shortMsg:limit:" + phoneNumber;// 設置過期時間為 1 分鐘(60 秒)// 使? NX,只在不存在 key 時才能設置成功bool r = Redis 執?命令:set key 1 ex 60 nxif (r == false) {// 說明之前設置過該手機的驗證碼了long c = Redis 執?命令:incr keyif (c > 5) {// 說明超過了?分鐘 5 次的限制了// 限制發送return null;}}// 說明要么之前沒有設置過手機的驗證碼;要么次數沒有超過 5 次String validationCode = ?成隨機的 6 位數的驗證碼();validationKey = "validation:" + phoneNumber;// 驗證碼 5 分鐘(300 秒)內有效Redis 執?命令:set validationKey validationCode ex 300;// 返回驗證碼,隨后通過手機短信發送給用戶return validationCode ;
}// 驗證用戶輸入的驗證碼是否正確
bool 驗證驗證碼(phoneNumber, validationCode) {validationKey = "validation:" + phoneNumber;String value = Redis 執?命令:get validationKey;if (value == null) {// 說明沒有這個手機的驗證碼記錄,驗證失敗return false;}if (value == validationCode) {return true;} else {return false;}
}

以上介紹了使用?Redis 的字符串數據類型可以使用的幾個場景,但其適用場景遠不止于此,開發人員可以結合字符串類型的特點以及提供的命令,充分發揮自己的想象力,在自己的業務中去找到合適的場景去使用?Redis 的字符串類型。

三、哈希

字符串和哈希類型對比:

幾乎所有的主流編程語言都提供了哈希(hash)類型,它們的叫法可能是哈希、字典、關聯數組、映射。在 Redis 中,哈希類型是指值本身又是?個鍵值對結構,形如 key = "key",value = { { field1, value1 }, ..., { fieldN, valueN } },Redis 鍵值對和哈希類型二者的關系可以用下圖來表示。

哈希類型中的映射關系通常稱為 field-value,用于區分 Redis 整體的鍵值對(key-value),注意這里的?value 是指 field 對應的值,不是鍵(key)對應的值,請注意 value 在不同上下文的作用。

1、命令

(1)HSET

設置 hash 中指定的字段(field)的值(value)。

語法:

HSET key field value [field value ...]

命令有效版本:

2.0.0 之后

時間復雜度:

插?一組 field 為 O(1),插? N 組 field 為 O(N)

返回值:

添加的字段的個數,也就是設置成功的鍵值對的個數。

示例:

(2)HGET

獲取 hash 中指定字段的值。

語法:

HGET key field

命令有效版本:

2.0.0 之后

時間復雜度:

O(1)

返回值:

字段對應的值或者 nil。

示例:


(3)HEXISTS

判斷 hash 中是否有指定的字段。

語法:

HEXISTS key field

命令有效版本:

2.0.0 之后

時間復雜度:

O(1)

返回值:

1 表示存在,0 表示不存在。

示例:

(4)HDEL

刪除 hash 中指定的字段。

語法:

HDEL key field [field ...]

命令有效版本:

2.0.0 之后

時間復雜度:

刪除一個元素為 O(1),刪除 N 個元素為 O(N)。

返回值:

本次操作刪除的字段個數。

示例:

(5)HKEYS

獲取 hash 中的所有字段。

語法:

HKEYS key

命令有效版本:

2.0.0 之后

時間復雜度:

O(N)?N 為 field 的個數,當前的 O(N) 可以說成是 O(1)。

返回值:

字段列表。

示例:


(6)HVALS

獲取 hash 中的所有的值。

語法:

HVALS key??

命令有效版本:

2.0.0 之后

時間復雜度:

O(N) N 為 field 的個數。

如果 field(哈希)非常大,那么這個操作就可能導致 Redis 服務器被阻塞住。

返回值:

所有的值。

示例:


(7)HGETALL

獲取 hash 中的所有字段以及對應的值。

這個操作的風險比較大,但多數情況下,我們不需要查詢所有的 field,可能只查其中幾個 field。

語法:

HGETALL key?

命令有效版本:

2.0.0 之后

時間復雜度:

O(N) N 為 field 的個數。

返回值:

字段和對應的值。

示例:

此處前面的序號僅僅是標識下返回元素的順序,和下標無關,hash 類型沒有下標的概念。

(8)HMGET

一次獲取 hash 中多個字段的值。

語法:

HMGET key field [field ...]

命令有效版本:

2.0.0 之后

時間復雜度:

只查詢?個元素為 O(1),查詢多個元素為 O(N) N 為查詢元素個數。

返回值:

字段對應的值或者 nil。

示例:

示例:

注意:多個 value 的順序和 field 的順序是匹配的。

在使用命令 HKEYS,HVALS,HGETALL 完成所有的遍歷操作時,都是存在一定風險的,如果 hash 的元素個數太多,執行的耗時就比較長,那么就會存在阻塞 Redis 的可能。

如果開發人員只需要獲取部分 field,可以使用 HMGET,如果一定要獲取全部 field,可以嘗試使用 HSCAN 命令,該命令采用漸進式遍歷哈希類型(敲一次命令,遍歷一小部分,時間是可控的,連續執行多次就可以完成整個遍歷過程)。

是否有 hmset 一次設置多個 field 和 value 呢?

有的,但是并不需要使用,因為 hset 已經支持一次設置多個 field 和 value 了。?

(9)HLEN

獲取 hash 中的所有字段的個數。

語法:

HLEN key

命令有效版本:

2.0.0 之后

時間復雜度:

O(1)

返回值:

字段個數。

示例:


(10)HSETNX

在字段不存在的情況下,設置 hash 中的字段和值。

語法:

HSETNX key field value

命令有效版本:

2.0.0 之后

時間復雜度:

O(1)

返回值:

1 表示設置成功,0 表示失敗。

示例:


(11)HINCRBY

將 hash 中字段對應的數值添加指定的值。

語法:

HINCRBY key field increment?

命令有效版本:

2.0.0 之后

時間復雜度:

O(1)

返回值:

該字段變化之后的值。

示例:

(12)HINCRBYFLOAT

HINCRBY 的浮點數版本。

語法:

HINCRBYFLOAT key field increment

命令有效版本:

2.6.0 之后

時間復雜度:

O(1)

返回值:

該字段變化之后的值。

示例:


2、命令小結

下表是哈希類型命令的效果、時間復雜度:


3、內部編碼

哈希的內部編碼有兩種:

  1. ziplist(壓縮列表):當哈希類型元素個數小于 hash-max-ziplist-entries 配置(默認 512 個)、同時所有值都小于 hash-max-ziplist-value 配置(默認 64 字節)時(這兩個配置項是可以寫到 redis.conf 文件中的),Redis 會使用 ziplist 作為哈希的內部實現,ziplist 使用更加緊湊的結構實現多個元素的連續存儲,所以在節省內存方面比hashtable 更加優秀。
  2. hashtable(哈希表):當哈希類型無法滿足?ziplist 的條件時,Redis 會用?hashtable 作為哈希的內部實現,因為此時 ziplist 的讀寫效率會下降,而?hashtable 的讀寫時間復雜度為 O(1)。

下面的示例演示了哈希類型的內部編碼,以及響應的變化。

(1)當 field 個數比較少且沒有大的 value 時,內部編碼為 ziplist


(2)當有 value 大于 64 字節時,內部編碼會轉換為 hashtable


(3)當 field 個數超過 512 時,內部編碼也會轉換為 hashtable

4、使用場景

下圖為關系型數據表記錄的兩條用戶信息,用戶的屬性表現為表的列,每條用戶信息表現為行。

關系型數據表保存用戶信息:

如果映射關系表示這兩個用戶信息,則下圖所示:

映射關系表示用戶信息:

這里的 uid 不存儲可以嗎?直接使用 key 中的 id 來進行區分,存儲空間不是又進一步的節省了嗎?

不存儲這個 uid 也可以。但是在工程實踐中,一般都會把 uid 在 value 中再存一份,后續寫到相關的代碼時,使用起來會比較方便。

如果使用 string(JSON)的格式來表示 UserInfo,萬一只想要獲取其中的某個 field 或者修改某個 field,就需要把整個 JSON 都讀出來,解析成對象,操作 field,再重寫轉成 JSON 字符串,再寫回去。

相比于使用 JSON 格式的字符串緩存用戶信息,哈希類型變得更加直觀,并且在更新操作上變得更靈活,可以使用 field 表示對象的每個屬性(數據表的每個列),此時就可以很方便的修改 / 獲取任何一個屬性的值了。可以將每個用戶的 id 定義為鍵后綴,多對 field-value 對應用戶的各個屬性,類似如下偽代碼:
?

UserInfo getUserInfo(long uid) {// 根據 uid 得到 Redis 的鍵String key = "user:" + uid;// 嘗試從 Redis 中獲取對應的值userInfoMap = Redis 執?命令:hgetall key;// 如果緩存命中(hit)if (value != null) {// 將映射關系還原為對象形式UserInfo userInfo = 利?映射關系構建對象(userInfoMap);return userInfo;}// 如果緩存未命中(miss)// 從數據庫中,根據 uid 獲取??信息UserInfo userInfo = MySQL 執? SQL:select * from user_info where uid = <uid>// 如果表中沒有 uid 對應的??信息if (userInfo == null) {響應 404return null;}// 將緩存以哈希類型保存Redis 執?命令:hmset key name userInfo.name age userInfo.age city userInfo.city// 寫?緩存,為了防?數據腐爛(rot),設置過期時間為 1 ?時(3600 秒)Redis 執?命令:expire key 3600// 返回??信息return userInfo;
}

但是需要注意的是哈希類型和關系型數據庫有兩點不同之處:

  • 哈希類型是稀疏的,而關系型數據庫是完全結構化的,例如哈希類型每個鍵可以有不同的 field,而關系型數據庫一旦添加新的列,所有行都要為其設置值,即使為 null,如圖 2-18 所示。
  • 關系數據庫可以做復雜的關系查詢,而 Redis 去模擬關系型復雜查詢,例如聯表查詢、聚合查詢等基本不可能,維護成本高。

關系型數據庫稀疏性:

5、緩存方式對比

截至目前為止,我們已經能夠用三種方法緩存用戶信息,下面給出三種方案的實現方法和優缺點分析。

(1)原生字符串類型 ——?使用字符串類型,每個屬性一個鍵

set user:1:name James
set user:1:age 23
set user:1:city Beijing
  • 優點:實現簡單,針對個別屬性變更也很靈活。
  • 缺點:占用過多的鍵,內存占用量較大,同時用戶信息在 Redis 中比較分散,缺少內聚性,所以這種方案基本沒有實用性。

(2)序列化字符串類型,例如 JSON 格式?

set user:1 經過序列化后的??對象字符串
  • 優點:針對總是以整體作為操作的信息比較合適,編程也簡單。同時,如果序列化方案選擇合適,內存的使用效率很高。
  • 缺點:本身序列化和反序列需要一定開銷,同時如果總是操作個別屬性則非常不靈活。

(3)哈希類型

hmset user:1 name James age 23 city Beijing
  • 優點:簡單、直觀、靈活。尤其是針對信息的局部變更或者獲取操作。
  • 缺點:需要控制哈希在 ziplist 和 hashtable 兩種內部編碼的轉換,可能會造成內存的較大消耗。

    四、List 列表

    列表兩端插入和彈出操作:

列表類型是用來存儲多個有序的字符串,如上圖所示,a、b、c、d、e 五個元素從左到右組成了一個有序的列表,列表中的每個字符串稱為元素(element),一個列表最多可以存儲個元素。

在 Redis 中,可以對列表兩端插入(push)和彈出(pop),還可以獲取指定范圍的元素列表、獲取指定索引下標的元素等,如下圖所示。

列表的獲取、刪除等操作:

列表是一種比較靈活的數據結構,它可以充當棧和隊列的角色,在實際開發上有很多應用場景。

列表類型的特點:

  • 列表中的元素是有序的(指的是順序很關鍵,不是指升序 / 降序),這意味著可以通過索引下標獲取某個元素或者某個范圍的元素列表,例如要獲取上圖中的第 5 個元素,可以執行 lindex user:1:messages 4 或者倒數第 1 個元素,lindex user:1:messages -1 就可以得到元素 e。
  • 區分獲取和刪除的區別,例如上圖中的 lrem 1 b 是從列表中把從左數遇到的前 1 個 b 元素刪除,這個操作會導致列表的長度從 5 變成 4;但是執行 lindex 4 只會獲取元素,但列表長度是不會變化的。
  • 列表中的元素是允許重復的,例如下圖中的列表中是包含了兩個 a 元素的。

    列表中允許有重復元素:

1、命令

(1)LPUSH

將一個或者多個元素從左側放入(頭插)到 list 中。

語法:

LPUSH key element [element ...]

命令有效版本:

1.0.0 之后

時間復雜度:

只插入一個元素為 O(1),插入多個元素為 O(N),N 為插入元素個數。

返回值:

插入后 list 的長度。

示例:

前面的序號時專門給結果集使用的序號,和 list 下標無關。


(2)LPUSHX

在 key 存在時,將一個或者多個元素從左側放入(頭插)到 list 中。不存在,直接返回。

LPUSHX 指的是:left push exists

語法:

LPUSHX key element [element ...]

命令有效版本:

2.0.0 之后

時間復雜度:

只插入一個元素為 O(1),插入多個元素為 O(N),N 為插入元素個數。

返回值:

插入后 list 的長度。

示例:

(3)RPUSH

將一個或者多個元素從右側放入(尾插)到 list 中。

語法:

命令有效版本:

1.0.0 之后

時間復雜度:

只插入一個元素為 O(1),插入多個元素為 O(N),N 為插入元素個數。

返回值:

插入后 list 的長度。

示例:

(4)RPUSHX

在 key 存在時,將一個或者多個元素從右側放入(尾插)到 list 中。

語法:

RPUSHX key element [element ...]?

命令有效版本:

2.0.0 之后

時間復雜度:

只插入一個元素為 O(1),插入多個元素為 O(N),N 為插入元素個數。

返回值:

插入后 list 的長度。

示例:


(5)LRANGE

獲取從 start 到 end 區間的所有元素,左閉右閉(閉區間),下標支持負數。

LRANGE 指的是:list range

語法:

LRANGE key start stop

命令有效版本:

1.0.0 之后

時間復雜度:

O(N)

返回值:

指定區間的元素。

示例:

Redis 的做法是直接盡可能的獲取到給定區間范圍內的元素,如果給定區間非法,比如超出下標,就會盡可能的獲取對應的內容。


(6)LPOP

從 list 左側取出元素(即頭刪)。

語法:

LPOP key?

Redis 5 版本中在這后面是沒有 [count] 參數的,從 Redis 6.2 版本開始,新增了一個 count 參數,用來描述此次要刪除幾個元素。

命令有效版本:

1.0.0 之后

時間復雜度:

O(1)

返回值:

取出的元素或者 nil。

示例:

(7)RPOP

從 list 右側取出元素(即尾刪)。

語法:

RPOP key

命令有效版本:

1.0.0 之后

時間復雜度:

O(1)

返回值:

取出的元素或者 nil。

示例:

  • 搭配使用 rpush 和 lpop 就相當于隊列
  • 搭配使用 rpush 和 rpop 就相當于棧

(8)LINDEX

獲取從左數第 index 位置的元素。

LINDEX 指的是:list index

語法:

LINDEX key index

命令有效版本:

1.0.0 之后

時間復雜度:

O(N)

返回值:

取出的元素或者 nil。

示例:


(9)LINSERT

在特定位置插入元素。

語法:

LINSERT key <BEFORE | AFTER> pivot element

命令有效版本:

2.2.0 之后

時間復雜度:

O(N),N 表示列表長度。

返回值:

插入后的 list 長度。

示例:

insert 進行插入時,要根據基準值找到對應的位置,從左往右找,找到第一個符合基準值的位置即可。


(10)LLEN

獲取 list 長度。

語法:

LLEN key

命令有效版本:

1.0.0 之后

時間復雜度:

O(1)

返回值:

list 的長度。

示例:


(11)LREM

根據參數 count 的值,移除列表中與參數 element 相等的元素。

  • count > 0 : 從表頭開始向表尾搜索,移除與 element 相等的元素,數量為 count。
  • count < 0 : 從表尾開始向表頭搜索,移除與 element 相等的元素,數量為 count的絕對值。
  • count = 0 : 移除表中所有與 element 相等的值。

語法:

LREM key count element

命令有效版本:

1.0.0 之后

時間復雜度:

O(N)

返回值:

被移除元素的數量。 列表不存在時返回 0 。

示例:

(12)LTRIM

Redis Ltrim 對一個列表進行修剪(trim),也就是說,讓列表只保留 start 和 stop 區間內(閉區間)的元素,不在區間之內的元素都將被直接刪除。

語法:

LTRIM key start stop

命令有效版本:

1.0.0 之后

時間復雜度:

O(N)

返回值:

命令執行成功時,返回 OK。

示例:


(13)LSET

通過索引來設置元素的值。當索引參數超出范圍,或對一個空列表進行 LSET 時,返回一個錯誤。

語法:

LSET key index element

命令有效版本:

1.0.0 之后

時間復雜度:

O(N)

返回值:

操作成功返回 OK,否則返回錯誤信息。

示例:

  • lindex 可以很好的處理下標越界的情況,直接返回 nil。
  • lset 則會報錯,不會像 js 一樣,直接在 10 這個下標搞出一個元素。

2、阻塞版本命令

blpop?和?brpop?是 lpop 和 rpop 的阻塞版本,和對應非阻塞版本的作用基本一致,除了:

  • 在列表中有元素的情況下,阻塞和非阻塞表現是一致的。但如果列表中沒有元素,非阻塞版本會直接返回 nil,但阻塞版本會根據 timeout 阻塞?段時間(使用 blpop 和 brpop 時,這里是可以顯示設置阻塞時間的,不一定是無休止的等待),期間 Redis 可以執行其他命令(此處的 blpop 和 brpop 看起來好像耗時很長,但實際上并不會對 Redis 服務器產生負面影響),但要求執行該命令的客戶端會表現為阻塞狀態(如下圖所示)。
  • 命令中如果設置了多個鍵(key),那么會從左向右進行遍歷鍵,一旦有一個鍵對應的列表中可以彈出元素,命令立即返回。
  • 如果多個客戶端同時多一個鍵執行 pop,則最先執行命令的客戶端會得到彈出的元素。

阻塞版本的 blpop 和非阻塞版本 lpop 的區別:?

?


(1)BLPOP

LPOP 的阻塞版本。

語法:

BLPOP key [key ...] timeout

此處還可以指定超時時間,單位是秒(Redis 6 中,超時時間允許設定成小數,Redis 5 得是整數)。

命令有效版本:

1.0.0 之后

時間復雜度:

O(1)

返回值:

取出的元素或者 nil。

示例:

(2)BRPOP

RPOP 的阻塞版本。

效果和 BLPOP 類似,只不過這里是頭刪。

語法:

BRPOP key [key ...] timeout

命令有效版本:

1.0.0 之后

時間復雜度:

O(1)

返回值:

取出的元素或者 nil。

BLPOP 和 BRPOP 這兩個阻塞命令的用途主要就是用來作為 “消息隊列”。雖然這兩個命令可以在一定程度上滿足 “消息隊列” 這樣的需求,但整體來說,功能還是比較有限。?

3、命令小結

下表是這些命令的作用和時間復雜度:

列表命令:


4、內部編碼

列表類型的內部編碼有兩種(舊版本,現在已經不再使用,了解即可):

  • ziplist(壓縮列表):當列表的元素個數小于 list-max-ziplist-entries 配置(默認 512 個),同時列表中每個元素的長度都小于 list-max-ziplist-value 配置(默認 64 字節)時,Redis 會選用 ziplist 來作為列表的內部編碼實現來減少內存消耗。
  • linkedlist(鏈表):當列表類型無法滿足 ziplist 的條件時,Redis 會使用 linkedlist 作為列表的內部實現。

現在采用的內部編碼都是?quicklist。quicklist 相當于是鏈表和壓縮列表的結合,整體還是一個鏈表,鏈表的每個節點是一個壓縮列表。每個壓縮列表都不讓它太大,同時再把多個壓縮列表通過鏈式結構連起來。

5、使用場景

(1)消息隊列

如下圖所示,Redis 可以使用 lpush + brpop 命令組合實現經典的阻塞式生產者-消費者模型隊列,生產者客戶端使用 lpush 從列表左側插入元素,多個消費者客戶端使用 brpop 命令阻塞式地從隊列中 “爭搶” 隊首元素。通過多個客戶端來保證消費的負載均衡和高可用性。

阻塞消息隊列模型:

brpop 是阻塞操作,當列表為空時,brpop 就會阻塞等待,一直等到其他客戶端 push 了元素為止。當新元素到達之后,首先是第一個消費者拿到元素(按照執行 brpop 命令的先后順序來決定是誰獲取到)。第一個消費者拿到元素之后,也就從 brpop 中返回了(相當于這個命令執行完了)。如果第一個消費者還想繼續消費,就需要重新執行 brpop,排在最后。此時,再來一個新的元素過來,就是第二個消費者拿到該元素,以此類推。

(2)分頻道的消息隊列

如下圖所示,Redis 同樣使用?lpush + brpop 命令,但通過不同的鍵模擬頻道的概念,不同的消費者可以通過 brpop 不同的鍵值,實現訂閱不同頻道的理念。

Redis 分頻道阻塞消息隊列模型:

多個列表(channel)/ 頻道(topic),這種場景很常見,日常使用的一些程序,比如抖音。有一個通道用來傳輸短視頻數據,還可以有一個通道來傳輸彈幕,一個通道來傳輸點贊、轉發、收藏數據,一個通道來傳輸評論數據... ... 弄成多個頻道就可以在某種數據發生問題時,不會對其他數據造成影響(解耦合)。

(3)微博 Timeline

每個用戶都有屬于自己的 Timeline(微博列表),現需要分頁展示文章列表。此時可以考慮使用列表,因為列表不但是有序的,同時支持按照索引范圍獲取元素。

A. 每篇微博使用哈希結構存儲,例如微博中 3 個屬性:title、timestamp、content

hmset mblog:1 title xx timestamp 1476536196 content xxxxx
...
hmset mblog:n title xx timestamp 1476536196 content xxxxx

B.?向用戶?Timeline 添加微博,user:<uid>:mblogs 作為微博的鍵

lpush user:1:mblogs mblog:1 mblog:3
...
lpush user:k:mblogs mblog:9

C. 分頁獲取用戶的 Timeline,例如獲取用戶?1 的前 10 篇微博

keylist = lrange user:1:mblogs 0 9
for key in keylist {hgetall key
}

此方案在實際中可能存在兩個問題:

  1. 1 + n 問題。即如果每次分頁獲取的微博個數較多(不確定當前一頁中有多少數據,可能會導致下面的循環次數很多),需要執行多次 hgetall 操作,此時可以考慮使用 pipeline(流水線 / 管道)模式批量提交命令,或者微博不采用哈希類型,而是使用序列化的字符串類型,使用 mget 獲取。雖然這里是多個 Redis 命令,但是把這些命令合并成一個網絡請求進行通信,這樣就大大降低了客戶端和服務器之間的交互次數了。
  2. 分裂獲取文章時,lrange 在列表兩端表現較好,獲取列表中間的元素表現較差,此時可以考慮將列表做拆分。
    ?

選擇列表類型時,請參考:

  1. 同側存取(lpush + lpop 或者 rpush + rpop)為棧。
  2. 異側存取(lpush + rpop 或者 rpush + lpop)為隊列。

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/diannao/94681.shtml
繁體地址,請注明出處:http://hk.pswp.cn/diannao/94681.shtml
英文地址,請注明出處:http://en.pswp.cn/diannao/94681.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

金融風控實戰:Spring Boot + LightGBM 貸款預測模型服務化(超詳細版)

金融風控實戰&#xff1a;Spring Boot LightGBM 貸款預測模型服務化&#xff08;超詳細版&#xff09;一、整體架構設計二、模型訓練與優化1. 特征工程&#xff08;Python&#xff09;2. 模型評估與優化三、Spring Boot 服務實現1. 項目結構2. ONNX 模型服務3. 特征工程服務4.…

前端學習 7:EDA 工具

目錄 EDA 工具 Design Ware Synopsys CoreTools 套件 VCS verdi Design Compiler EDA 工具 常用的EDA工具主要來自三家公司&#xff1a;Synopsys、Cadence和Mentor&#xff08;已被Siemens收購&#xff09;。EDA&#xff0c;全稱電子設計自動化&#xff08;Electronics …

windows有一個企業微信安裝包,腳本執行并安裝到d盤。

以下是將本地已有的企業微信安裝包安裝到D盤的完整PowerShell腳本&#xff0c;包含詳細的錯誤處理和進度反饋&#xff1a; <# .SYNOPSIS使用本地企業微信安裝包安裝到D盤 .DESCRIPTION自動檢測本地安裝包&#xff0c;靜默安裝到指定目錄支持.exe和.msi格式安裝包 #># 強制…

[LVGL] 布局系統 lv_flex, lv_grid | 輸入設備 lv_indev | union

第五章&#xff1a;布局系統&#xff08;lv_flex, lv_grid&#xff09; 歡迎回來&#xff01; 在第四章&#xff1a;樣式&#xff08;lv_style&#xff09;中&#xff0c;我們掌握了如何通過色彩、字體和圓角等特性美化部件。當界面元素具備視覺吸引力后&#xff0c;如何優雅…

Linux中的mkdir命令

基本語法mkdir 命令的基本語法如下&#xff1a;mkdir [選項] 目錄名創建單個目錄要創建一個新目錄&#xff0c;只需在 mkdir 后跟上目錄名稱。例如&#xff1a;mkdir new_folder這會在當前工作目錄下創建一個名為 new_folder 的目錄。創建多個目錄可以一次性創建多個目錄&#…

基于大數據的美食視頻播放數據可視化系統 Python+Django+Vue.js

本文項目編號 25003 &#xff0c;文末自助獲取源碼 \color{red}{25003&#xff0c;文末自助獲取源碼} 25003&#xff0c;文末自助獲取源碼 目錄 一、系統介紹二、系統錄屏三、啟動教程四、功能截圖五、文案資料5.1 選題背景5.2 國內外研究現狀 六、核心代碼6.1 查詢數據6.2 新…

微信小程序精品項目-基于springboot+Android的計算機精品課程學習系統(源碼+LW+部署文檔+全bao+遠程調試+代碼講解等)

博主介紹&#xff1a;??碼農一枚 &#xff0c;專注于大學生項目實戰開發、講解和畢業&#x1f6a2;文撰寫修改等。全棧領域優質創作者&#xff0c;博客之星、掘金/華為云/阿里云/InfoQ等平臺優質作者、專注于Java、小程序技術領域和畢業項目實戰 ??技術范圍&#xff1a;&am…

(五)系統可靠性設計

2024年博主考軟考高級系統架構師沒通過&#xff0c;于是決定集中精力認真學習系統架構的每一個環節&#xff0c;并在2025年軟考中取得了不錯的成績&#xff0c;雖然做信息安全的考架構師很難&#xff0c;但找對方法&#xff0c;問題就不大&#xff01; 本文主要是博主在學習過程…

Shuffle SOAR使用學習經驗

Shuffle SOAR 1. 基礎操作與配置1.1 環境搭建與系統要求1.1.1 硬件與操作系統要求Shuffle SOAR 平臺作為一款開源的安全編排、自動化與響應&#xff08;SOAR&#xff09;工具&#xff0c;其部署方式靈活&#xff0c;支持云端和自托管兩種模式。對于自托管部署&#xff0c;官方推…

騰訊云 EdgeOne 產品分析與免費套餐體驗指南

本文圍繞騰訊云 EdgeOne 展開&#xff0c;全方位介紹它的核心能力、免費套餐內容&#xff0c;以及如何快速上手、監控和排查常見問題&#xff0c;幫助個人開發者和中小企業在不產生額外成本的前提下體驗高性能的邊緣加速與安全防護。 一、產品概述 EdgeOne 定位 一體化云服務平…

npm ERR! Unsupported URL Type “workspace:“: workspace:./lib

如下 npm install npm ERR! code EUNSUPPORTEDPROTOCOL npm ERR! Unsupported URL Type "workspace:": workspace:./libnpm ERR! A complete log of this run can be found in: D:\IDEA\nodejs\node_cache\_logs\2025-08-06T08_21_32_592Z-debug-0.log原因及解決 pac…

微積分: 變化與累積

微積分,這門研究變化與累積的數學分支,其核心思想竟與東方哲學中"易"的概念不謀而合。《易經》有云:“易有太極,是生兩儀”,而微積分正是通過"微分"與"積分"這對辯證統一的操作,揭示了世間萬物變化與永恒的奧秘。 #mermaid-svg-UjO6qqMm0h…

web-vue工作流程

接續bmcweb流程。 當登錄openbmc web頁面后,瀏覽器會根據index.html中的js文件中的routes信息,自動獲取信息,比如當前的網絡設置信息、Datetime時區時間信息等。 以獲取網絡配置信息為例: 瀏覽器從app.js獲取到settins->network的route:”/settings/network”,加載對應…

全球化2.0 | 泰國IT服務商攜手云軸科技ZStack重塑云租賃新生態

在全球數字化轉型不斷加速的今天&#xff0c;泰國企業對于高質量云服務的需求日益旺盛。作為深耕本地市場逾二十年的行業領先IT服務商&#xff0c;泰國IT服務商不僅覆蓋了IT系統、軟件、硬件及網絡等多個領域&#xff0c;還持續引領當地技術服務創新。近期&#xff0c;該泰國IT…

一文搞懂Hive臨時表操作秘籍

Hive 臨時表&#xff1a;數據處理的得力助手 在大數據處理的廣闊領域中&#xff0c;Hive 憑借其強大的數據倉庫功能&#xff0c;成為了眾多數據分析師和開發者的得力工具。Hive 提供了類似 SQL 的查詢語言 HiveQL&#xff0c;讓我們能夠方便地對存儲在 Hadoop 分布式文件系統&a…

瞬態吸收光譜儀的基本原理

目錄 1. 基態與激發態 2. 時間上的動力學信息 3. pump-probe探測技術 4. 時間延遲和同一光源 5. 延時線和OPA 6. 差分信號 7. 斬波器 原視頻鏈接&#xff1a;瞬態吸收光譜儀的基本原理_嗶哩嗶哩_bilibili 1. 基態與激發態 當光照射在物質上時&#xff0c;組成物質的微觀…

迭代器與生成器:Python 中的高效數據遍歷機制

一、迭代器和生成器的基本概念 1. 迭代器的定義和工作原理 &#xff08;1&#xff09;迭代器的概念 迭代器&#xff08;Iterator&#xff09; 是 Python 中一種支持逐個訪問元素的對象&#xff0c;它遵循 迭代器協議&#xff08;Iterator Protocol&#xff09;&#xff0c;即實…

Java 發送 HTTP POST請求教程

Java 發送 HTTP POST 請求的方法使用 HttpURLConnection&#xff08;原生 Java 支持&#xff09; 創建一個 HttpURLConnection 對象&#xff0c;設置請求方法為 POST&#xff0c;并寫入請求體數據。以下是一個簡單示例&#xff1a;import java.io.OutputStream; import java.ne…

計算機英語詳細總結

計算機英語作為信息技術領域的專用語言&#xff0c;融合了專業術語、縮寫、行業表達及技術文檔規范&#xff0c;是學習編程、從事 IT 工作的核心工具。以下從核心分類、應用場景、學習方法三方面詳細梳理&#xff1a;一、核心術語分類與高頻詞匯1. 編程語言與語法基礎基礎概念&…

「日拱一碼」045 機器學習-因果發現算法

目錄 基于約束的方法 (Constraint-based) 基于評分的方法 (Score-based) 基于函數因果模型的方法 (Functional Causal Models) 基于梯度的方法 (Gradient-based) 因果發現是機器學習中一個重要的研究方向&#xff0c;它旨在從觀測數據中推斷變量之間的因果關系 基于約束的…