一、前言
1. 基本概念
理解:字符串對象是 Redis 中最基本的數據類型,也是我們工作中最常用的數據類型。redis中的鍵都是字符串對象,而且其他幾種數據結構都是在字符串對象基礎上構建的。字符串對象的值實際可以是字符串、數字、甚至是二進制,最大不能超過512MB
-
Key:所有 key 都是二進制安全的字符串(binary-safe string)。可以包含任意字符(包括中文、空格、特殊符號等),例如:
SET user:1:name "張三" SET log:2024-01-01 "\x01\x02\x03"
-
Value:雖然 Redis 的底層統一使用
robj
(Redis Object)來表示對象,但 value 可以是:string、list、hash、set等
?? 但所有這些數據結構,其“元素”本質上也都是字符串,比如列表中的每個元素是字符串,集合中的每個成員也是字符串
2. string 存儲方式
🔥 由于Redis內部存儲字符串完全是按照 二進制流 的形式保存的,所以Redis是 不處理字符集編碼問題 的,客戶端傳入的命令中使用的是什么字符集編碼,就存儲什么字符集編碼
① 二進制安全(Binary Safe)
- Redis 的 String 是二進制安全 的,意味著它可以存儲任何形式的數據:
- 文本:如
"hello"
、"你好"
、"{"name":"Tom"}"
- 數值:整數或浮點數,如
123
,3.14
- 二進制數據:圖片、音頻、視頻、序列化后的對象等
- 文本:如
- Redis 不會對寫入的數據進行編碼轉換或處理。
② Redis 不處理字符集編碼
- Redis 不關心你傳進來的是 UTF-8、GBK 還是其他編碼格式。
- 客戶端發送什么編碼,Redis 就存儲什么編碼。
- 解碼工作由客戶端負責完成。
? 因此,在使用 Redis 存儲文本數據時,務必確保客戶端使用的字符集與讀取時一致,否則會出現亂碼(避免亂碼)
3. 支持的數據類型
數據類型 | 示例 | 特點 |
---|---|---|
文本數據 | "Hello World" ,"{\"id\":1}" | 可以是普通文本或 JSON/XML 等結構化文本 |
數字 | 123 ,3.14 | Redis 自動識別為整數或浮點數,并優化存儲為int 編碼 |
二進制數據 | 圖片、視頻、序列化對象 | Redis 會以原始字節形式存儲 |
4. 與 MySQL 字符串對比
對比項 | Redis | MySQL |
---|---|---|
字符集處理 | 不處理字符集,原樣存儲 | 默認使用特定字符集(如 latin1 或 utf8mb4) |
編碼轉換 | 不做任何轉換 | 插入/查詢時可能自動進行編碼轉換 |
亂碼問題 | 客戶端控制,Redis 不參與 | 如果配置不當容易出現亂碼 |
二進制存儲 | 支持任意二進制數據 | BLOB 類型可存二進制,但操作不如 Redis 簡便 |
最大容量 | 單個 value 最大 512MB | TEXT/LONGTEXT/BLOB 有大小限制,但通常更大 |
5. 性能考慮
Redis 是單線程模型(核心命令處理)
- 所有命令都在一個主線程中執行(Redis 6.0+ 多線程僅用于 I/O)
- 因此,不能執行耗時過長的操作 ,否則會影響整個服務的響應速度。
?? 不建議:
- 存儲過大字符串(如幾百 MB 的文件)
- 執行復雜計算(如大范圍遍歷、正則匹配)
- 使用慢命令(如
KEYS *
、SMEMBERS
等)
二、命令
1. 常用命令
① SET
?? 將 string
類型的 value
設置到key中。**如果key之前存在,則覆蓋,無論原來的數據類型是什么。**之前關于此key的TTL也全部失效。命令如下:
SET key value [expiration EX seconds|PX milliseconds] [NX|XX]
SET 命令支持多種選項來影響其行為,如下:
EX seconds
:以秒為單位,設置超時時間PX milliseconds
:以毫秒為單位,設置超時時間NX
:只有key不存在才設置,如果存在返回nil
XX
:只有key存在就更新,如果不存在返回nil
注意:由于帶選項的SET命令可以被 SETNX 、 SETEX 、 PSETEX 等命令代替,所以之后的版本中,Redis可能進行合并
示例
127.0.0.1:6379> set mykey "Hello"
OK
127.0.0.1:6379> get mykey
"Hello"
127.0.0.1:6379> set mykey "World" NX
(nil)
127.0.0.1:6379> set mykey "World" XX
OK
127.0.0.1:6379> ttl mykey
(integer) -1
127.0.0.1:6379> set mykey "Island" EX 10
OK
127.0.0.1:6379> ttl mykey
(integer) 8
② GET
獲取key對應的value。如果key不存在,返回nil。如果value的數據類型不是string,會報錯。
127.0.0.1:6379> hset mykey name Bob
(integer) 1
127.0.0.1:6379> get mykey
(error) WRONGTYPE Operation against a key holding the wrong kind of value
127.0.0.1:6379> del mykey
(integer) 1127.0.0.1:6379> set mykey Bob
OK
127.0.0.1:6379> get mykey
"Bob"
③ MGET
?次性獲取多個key的值。如果對應的key不存在或者對應的數據類型不是string,返回 nil
,語法如下:
MGET key [key ...]
示例
127.0.0.1:6379> sey key1 1
(error) ERR unknown command `sey`, with args beginning with: `key1`, `1`,
127.0.0.1:6379> set key1 1
OK
127.0.0.1:6379> set key2 2
OK
127.0.0.1:6379> MGET key1 key2 key3
1) "1"
2) "2"
3) (nil)
④ MSET
?次性設置多個key的值。語法如下:
MSET key value [key value ...]
示例
127.0.0.1:6379> MSET key1 1 key2 2
OK
127.0.0.1:6379> MGET key1 key2
1) "1"
2) "2"
多次 GET 和單次MGET比較:使用 MGET/MSET 可有效減少網絡時間,性能較高
結論:學會使用批量操作,可以有效提高業務處理效率
- 注意:每次批量操作所發送的鍵的數量也不是無節制的,否則可能造成單?命令執行時間過長,導致
Redis
阻塞
⑤ SETNX|SETEX
下面將介紹 2 個針對 set 的一些常見用法, 進行了縮寫.
- 之所以這樣, 就是為了讓操作更符合人的直覺. (使用者的門檻就越低, 要背的東西就越少)
- 編程語言中, 很多的關鍵詞, 都是和自然語言相關的
- 后續咱們去設計一些 庫, 設計一些工具, 代碼給別人使用的時候, 也要盡量符合直覺,不要設計的 “反人類” / “反直覺”
setnx
-
命令:
setnx key value
-
功能:如果鍵不存在,則設置鍵值對。可以理解為 no exist 設置~
-
示例:
setnx key1 value1 # 返回 0,因為 key1 已存在
setex
-
命令:
setex key seconds value
-
功能:設置鍵值對并指定過期時間(秒)。
-
示例:
setex key4 10 value4 ttl key4 # 返回剩余時間
2. 計數命令
由于string
內部還可以存儲數字,所以Redis
還提供了數字操作的命令。時間復雜度:O(1)
① INCR
命令:incr key
功能:將鍵的值加1,如果鍵不存在則創建鍵并初始化為0。如果 key 對應的string不是?個整型或者范圍超過了64位有符號整型,則報錯。
示例
127.0.0.1:6379> set key bar
127.0.0.1:6379> incr key # 非整形
(error) ERR value is not an integer or out of range127.0.0.1:6379> del key
127.0.0.1:6379> set key 1
127.0.0.1:6379> incr key # 存在
(integer) 2127.0.0.1:6379> del key
127.0.0.1:6379> incr key # 不存在
(integer) 1
② INCRBY
命令:incrby key increment
功能:和 INC
R 使用類似,將鍵的值增加指定的整數。
示例:
incrby key2 7 # 返回 8
③ DECR
命令:deby key
功能:和 INC
R 使用類似,將鍵的值減1,如果鍵不存在則創建鍵并初始化為0。
示例:
set key2 8
decr key2 # 返回 7
④ DECYBY
命令:decrby key decrement
功能:將鍵的值減少指定的整數。
示例:
set key2 8
decrby key2 2 # 返回 6
⑤ INCRBYFLOAT
命令:incrbyfloat key increment
功能:將鍵的值增加指定的浮點數(允許采用 科學計數法 表示浮點數)
示例
set key1 1
INCRBYFLOAT key1 0.5 # 返回 1.5
注意:
- Redis存儲整數,是直接使用int類型存的,而存儲小數,本質上是當作字符串來存儲
- Redis的int比較方便算術運算
- 小數意味著每次進行算術運算,都需要把字符串轉成小數,進行運算,再把結果轉回字符串保存
很多存儲系統和編程語言內部使用 CAS 機制實現計數功能,會有?定的CPU開銷
- 但在Redis中完全不存在這個問題,因為Redis是單線程架構,任何命令到了Redis服務端都要順序執行
- 由于Redis處理命令的時候,是單線程模型,多個客戶端同時針對同一個key進行INCR等操作,不會引起"線程安全"問題
3. 其他命令
① APPEND
命令:append key value
功能:如果key已經存在并且是?個string,命令會將value追加到原有string的后邊。如果key不存在,則效果等同于SET命令(返回追加完成之后string的長度)
示例:
127.0.0.1:6379> exists mykey
(integer) 0
127.0.0.1:6379> append mykey "Hello"
(integer) 5
127.0.0.1:6379> get mykey
"Hello"
127.0.0.1:6379> append mykey " World"
(integer) 11
127.0.0.1:6379> get mykey
"Hello World"
在啟動 redis 客戶端的時候,加上一個 --raw 這樣的選項就可以使 redis 客戶端能夠自動的把二進制數據嘗試翻譯
演示如下:
127.0.0.1:6379> set name "張三"
OK
127.0.0.1:6379> get name
"\xe5\xbc\xa0\xe4\xb8\x89"lighthouse@VM-8-10-ubuntu:~$ redis-cli --raw
127.0.0.1:6379> get name
張三
② GETRANGE
命令:getrange key start end(左閉右閉,[0, len - 1])
功能:獲取鍵值在指定范圍內的子字符串。
注意:
- 如果字符串中保存的是漢字,此時進行字串切分,切出來的很可能不是完成的漢字,因為 redis 是以 字節 為單位的
- 其中redis的getrange操作與python一樣是支持負數下標的,其中-1表示倒數第一個字符串,-2表示倒數第二個字符串
示例:
127.0.0.1:6379> get mykey
Hello World
127.0.0.1:6379> getrange mykey 0 3
Hell
127.0.0.1:6379> getrange mykey -3 -1
rld
127.0.0.1:6379> getrange mykey 0 -1
Hello World
127.0.0.1:6379> getrange mykey 20 100127.0.0.1:6379> getrange mykey 5 10World
③ SETRANGE
命令:setrange key offset value
功能:從指定偏移量開始設置鍵值的一部分。返回 string 長度
注意:針對不存在的key
,也可以操作,不過會把offset
之前的內容填充成 0x00
127.0.0.1:6379> get mykey
Hello World
127.0.0.1:6379> setrange mykey 6 "Redis"
11
127.0.0.1:6379> get mykey
Hello Redis
④ STRLEN
命令:strlen key
功能:獲取鍵值的字節長度。
127.0.0.1:6379> strlen mykey
11
127.0.0.1:6379> strlen non
0
4. 小結
下表是字符串類型命令的效果、時間復雜度,開發人員可以參考此表,結合自身業務需求和數據大小選擇合適的命令。
三、內部編碼
Redis字符串對象底層的數據結構實現主要是int和簡單動態字符串SDS(這個字符串,和我們認識的C字符串不太一樣,其通過不同的編碼方式映射到不同的數據結構
字符串對象的內部編碼有3種 :int
、raw
和embstr
。Redis會根據當前值的類型和長度來決定使用哪種編碼來實現。
默認情況下,值以字符串形式傳入,如果Redis檢測到字符串為數字,則轉換為int存儲,從而節省空間。例如,字符串"1234567890"若作為字符串存儲需要10字節,而轉換為int后僅需8字節
① int:當存儲的值為整數,且值的大小可以用 long 類型表示時,那么字符串對象會將整數值保存在字符串對象結構的ptr
屬性里面(將void*
轉換成1ong
),并將字符串對象的編碼設置為int
- 優點:存儲空間小,且無需進行額外的解碼操作( 只有整數才會使用int,如果是浮點數, Redis內部其實先將浮點數轉化為字符串值,然后再保存)
② raw:當存儲的值為字符串,且長度小于等于 44 字節時,Redis 使用 raw
編碼。在 raw 編碼中,String 對象的實際值會被存儲在一個簡單的 字符串對象(SDS) 中,該對象包含了字符串的長度和字符數組的指針。
- 優點:存儲空間小,且無需進行額外的解碼操作。
③ embstr:當存儲的值為字符串,且長度大于 44 字節時,Redis 使用 embstr 編碼。在 embstr 編碼中,String 對象的實際值會被存儲在一個特殊的字符串對象中,該對象包含了字符串的長度和字符數組的指針,但是不包含額外的空間。
- 優點:存儲空間小,且無需進行額外的解碼操作,但是由于需要額外的內存分配,可能會影響性能。
embstr
編碼 是專門用于保存短字符串的一種優化編碼方式,我們可以看到embstr
和raw
編碼都會使用SDS
來保存值,但不同之處在于embstr
會通過一次內存分配函數來分配一塊連續的內存空間來保存redisObject
和SDS
。而raw
編碼會通過調用兩次內存分配函數來分別分配兩塊空間來保存redisObject
和SDS
。Redis這樣做會有很多好處。
embstr
編碼將創建字符串對象所需的內存分配次數從raw編碼的兩次降低為一次- 釋放
embstr
編碼的字符串對象同樣只需要調用一次內存釋放函數 - 因為
embstr
編碼的字符串對象的所有數據都保存在一塊連續的內存里面可以更好的利用CPU緩存提升性能。
明明沒有超過閾值,為什么變成raw?
- 對于
embstr
,由于其實現是只讀的,因此在對embstr
對象進行修改時,都會先 轉化為 raw 再進行修改。因此,只要是修改embstr
對象,修改后的對象一定是 raw 的,無論是否達到了 44 個字節。
Redis中根據數據類型和長度來使用不同的編碼和數據結構存儲存在于Redis中的每一種對象類型上。其這種小細節上的優化令我嘆服不止,后續我們會看到Redis中到處都是這種內存與性能上的小細節優化!
Redis會根據當前值的類型和長度 動態決定使用哪種內部編碼實現
# 整形
> set key 2333
OK
> object encoding key
"int"# 短字符串
> set key "hello"
OK
> object encoding key
"embstr"# ?于39個字節的字符串
> set key "one string greater than 39 bytes ........"
OK
> object encoding key
"raw
思考:
- 某個業務場景,有很多很多的 key,類型都是 string,但是每個 value 的 string 長度都是 100 左右。
- 更關注整體的內存空間。因此,這樣的字符串使用 embstr 來存儲也不是不能考慮。
上述效果具體怎么實現?
-
先看 redis 是否提供了對應的配置項,可以修改 39 這個數字。
-
如果沒有提供配置型,就需要針對 redis 源碼進行魔改。
為啥很多大廠,往往是自己造輪子,而不是直接使用業界成熟的呢?
- 開源的組件,往往考慮的是通用性,但是大廠往往會遇到一些極端的業務場景,往往就需要根據當前的極端業務,針對上述的開源組件進行 定制化。
關于 SDS
🌤 Redis默認并未直接使用C字符串(C字符串僅僅作為字符串字面量,用在一些無需對字符串進行修改的地方,如打印日志)。而是以Struct的形式構造了一個SDS的抽象類型。當Redis需要一個可以被修改的字符串時,就會使用SDS來表示。在Redis數據庫里,包含字符串值的鍵值對都是由SDS實現的(Redis中所有的鍵都是由字符串對象實現的 即底層是由SDS實現,Redis中所有的值對象中包含的字符串對象底層也是由SDS實現)
Copystruct sdshdr{//int 記錄buf數組中未使用字節的數量 如上圖free為0代表未使用字節的數量為0int free;//int 記錄buf數組中已使用字節的數量即sds的長度 如上圖len為5代表未使用字節的數量為5int len;//字節數組用于保存字符串 sds遵循了c字符串以空字符結尾的慣例目的是為了重用c字符串函數庫里的函數char buf[];
}
為什么要使用SDS
上圖表示了SDS與C字符串的區別,關于為什么Redis要使用SDS而不是C字符串,我們可以從以下幾個方面來分析。
1. 緩沖區溢出
?? C字符串,如果程序員在字符串修改的時候如果忘記給字符串重新分配足夠的空間,那么就會發生內存溢出,如上圖所示,忘記給s1分配足夠的內存空間,s1的數據就會溢出到s2的空間, 導致s2的內容被修改。而Redis提供的SDS其內置的空間分配策略則可以完全杜絕這種事情的發生。當API需要對SDS進行修改時,API會首先會檢查SDS的空間是否滿足條件,如果不滿足, API會自動對它動態擴展, 然后再進行修改。
2. 內存重分配
C字符串內存重分配
在C字符串中,如果對字符串進行修改,那么我們就不得不面臨內存重分配。因為C字符串是由一個N+1長度的數組組成,如果字符串的長度變長,我們就必須對數組進行擴容,否則會產生內存溢出。而如果字符串長度變短,我們就必須釋放掉不再使用的空間,否則會發生內存泄漏。
SDS空間分配策略
對于Redis這種具有高性能要求的內存數據庫,如果每次修改字符串都要進行內存重分配,無疑是巨大的性能損失。而Redis的SDS提供了兩種空間分配策略來解決這個問題。
-
空間預分配:我們知道在數組進行擴容的時候,往往會申請一個更大的數組,然后把數組復制過去。為了提升性能,我們在分配空間的時候并不是分配一個剛剛好的空間,而是分配一個更大的空間。Redis同樣基于這種策略提供了空間預分配。當執行字符串增長操作并且需要擴展內存時,程序不僅僅會給SDS分配必需的空間還會分配額外的未使用空間,其長度存到free屬性中。其分配策略如下:
- 如果修改后len長度將小于1M,這時分配給free的大小和len一樣,例如修改過后為10字節, 那么給free也是10字節,buf實際長度變成了10+10+1 = 21byte
- 如果修改后len長度將大于等于1M,這時分配給free的長度為1M,例如修改過后為30M,那么給free是1M.buf實際長度變成了30M+1M+1byte
-
惰性空間釋放:惰性空間釋放用于字符串縮短的操作。當字符串縮短是,程序并不是立即使用內存重分配來回收縮短出來的字節,而是使用free屬性記錄起來,并等待將來使用。
Redis通過空間預分配和惰性空間釋放策略在字符串操作中一定程度上減少了內存重分配的次數。但這種策略同樣會造成一定的內存浪費,因此Redis SDS API提供相應的API讓我們在有需要的時候真正的釋放SDS的未使用空間。
3. 二進制安全
C字符串中的字符必須符合某種編碼(比如ASCII),并且除了字符串的末尾之外,字符串里面不能包含空字符,否則最先被程序讀入的空字符將被誤認為是字符串結尾,這些限制使得C字符串只能保存文本數據,而不能保存像圖片、音頻、視頻、壓縮文件這樣的二進制數據。如果有一種使用空字符來分割多個單詞的特殊數據格式,就不能用C字符串來表示,如"Redis\0String",C字符串的函數會把’\0’當做結束符來處理,而忽略到后面的"String"。而SDS的buf字節數組不是在保存字符,而是一系列二進制數組,SDS API都會以二進制的方式來處理buf數組里的數據,使用len屬性的值而不是空字符來判斷字符串是否結束。
4. 時間復雜度
我們來看幾個Redis常見操作的時間復雜度。
- 獲取SDS長度:由于SDS中提供了len屬性,因此我們可以直接獲取時間復雜度為O(1),C字符串為O(n)。
- 獲取SDS未使用空間長度:時間復雜度為0(1),原因同1
- 清除SDS保存的內容:由于惰性空間分配策略,復雜度為O(1)
- 創建一個長度為N的字符串:時間復雜度為O(n)
- 拼接一個長度為N的C字符串:時間復雜度為O(n)
- 拼接一個長度為N的SDS字符串:時間復雜度為O(n)
Redis在獲取字符串長度上的時間復雜度為常數級O(1)
5. 小結
通過以上分析,我們可以得到,SDS這種數據結構相對于C字符串有以下優點:
- 杜絕緩沖區溢出
- 減少字符串操作中的內存重分配次數
- 二進制安全
- 由于SDS遵循以空字符結尾的慣例,因此兼容部門C字符串函數
Redis定位于一個高性能的內存數據庫,其面向的就是大數據量,大并發,頻繁讀寫,高響應速度的業務。因此在保證安全穩定的情況下,性能的提升非常重要。而SDS這種數據結構屏蔽了C字符串的一些缺點,可以提供安全高性能的字符串操作。
四、使用場景
String 類型的具體應用場景
- 緩存功能:作為緩存層,提高讀寫速度,減輕后端數據庫壓力。
- 計數功能:實現快速計數,如視頻播放次數統計。
- 共享會話:集中管理用戶會話,支持分布式系統。
- 手機驗證碼:存儲驗證碼,設置過期時間,確保安全性。
1. 緩存(Cache)功能
下面是一個比較典型的緩存使用場景,其中 Redis 作為緩沖層,MySQL 作為存儲層,大多數請求的數據從 Redis 中獲取。由于 Redis 支持 高并發 的特性,所以緩存通常能 加速讀寫 和 降低后端壓力 的作用
Redis+MySQL 組成的緩存存儲架構
模擬業務數據訪問過程
// 1. 根據用戶 Uid 獲取用戶信息
UserInfo GetUserInfo(long uid) {// 2. 從 Redis 獲取用戶信息, 假設 信息保存在 user:info 對應鍵中// 根據 uid 得到 Redis 的鍵String key = "user:info:" + uid;// 嘗試從 Redis 中獲取對應的值String value = Redis 執行命令:get key;// 3. 如果沒有從Redis中得到??信息,及緩存miss,則進?步從MySQL中獲取對應的信息,隨后寫?緩存并返回// 如果緩存命中 (hit)if (value != null) {// 假設用戶信息按照 JSON 格式存儲UserInfo userInfo = JSON 反序列化 (value);return userInfo;}// 如果緩存未命中 (miss)if (value == null) {// 從數據庫中,根據 uid 獲取用戶信息UserInfo userInfo = MySQL 執行 SQL:select * from user_info where uid = <uid>;// 如果表中沒有 uid 對應的用戶信息if (userInfo == null) {響應 404;return null;}// 將用戶信息序列化成 JSON 格式String value = JSON 序列化 (userInfo);// 寫入緩存,為了防止數據腐爛 (rot),設置過期時間為 1 小時 (3600 秒)Redis 執行命令:set key value ex 3600;// 返回用戶信息return userInfo;}
}
通過增加緩存功能,在理想情況下,每個用戶信息,?個小時期間只會有?次MySQL查詢,極大地提升了查詢效率,也降低了MySQL的訪問數
注意:Redis 沒有表、字段等命名空間,鍵名沒有強制要求(除了一些特殊字符)
設計合理的鍵名,有利于防止鍵沖突和項目的可維護性。推薦使用 “業務名:對象名:唯一標識:屬性” 作為鍵名。
- 例如:MySQL 的數據庫名為 vs,用戶表名為 user_info,鍵名可以是 “vs:user_info:2333” 或 “vs:user_info:2333:name”。
- 如果當前 Redis 只會被一個業務使用,可以省略業務名,如 “user:2333:friends:messages:6666” 可以被 “u:2333: fr??666” 代替。
- 簡寫的原因:鍵名過長會影響 Redis 性能,網絡傳輸需要成本
思考:Redis 緩存策略
- 熱點數據:經常用來存儲頻繁被訪問的數據。
- 緩存定義:結合業務場景有很多種方式。
- 把最近使用到的數據作為熱點數據。(隱含了一層假設:某個數據一旦被用到了,那么很可能在這段時間就會反復用到)
存在一個明顯的問題:隨著時間的推移,肯定會有越來越多的 key 在 redis 上訪問不到,從而從 mysql 讀取并寫入 redis 了。此時 redis 中的數據是不是就越來越多嘛??
- 過期時間: 在把數據寫給 redis 的同時,給這個 key 設置一個過期時間。詳見[Redis#3] 通用命令 | 數據類型 | 內部編碼 | 單線程 | 快的原因 定時器部分的介紹
- 淘汰策略: Redis 也在內存不足的時候,提供了淘汰策略。(后面再說)
2. 計數器\限速器\分布式系統ID
計數器\限速器\分布式ID等主要是利用Redis字符串自增自減的特性。
- 計數器:經常可以被用來做計數器,如微博的評論數、點贊數、分享數,抖音作品的收藏數,京東商品的銷售量、評價數等。
- 限速器:如驗證碼接口訪問頻率限制,用戶登陸時需要讓用戶輸入手機驗證碼,從而確定是否是用戶本人,但是為了短信接口不被頻繁訪問,會限制用戶每分鐘獲取驗證碼的頻率,例如一分鐘不能超過5次。
- 分布式ID:由于Redis自增自減的操作是原子性的因此也經常在分布式系統中用來生成唯一的訂單號、序列號等
如下:視頻網站的視頻播放次數可以使用 Redis 來完成:用戶每播放?次視頻,相應的視頻播放數就會自增 1
示例:統計視頻播放次數
long IncrVideoCounter(long vid) {String key = "video:" + vid;long count = Redis 執行命令:incr key;return count;
}
- 注意:實際開發一個成熟、穩定的計數系統面臨更多挑戰,如 防作弊、按不同維度計數、避免單點問題、數據持久化到底層數據源等
- 根據實際的 業務需求 設計場景
3. 共享會話(Session)
會話的概念:客戶端和服務端在交互過程中產生的專屬于該客戶端的中間狀態數據。
- 目的:確保服務器能夠識別和記住客戶端的多次訪問狀態。
Cookie 和 Session
- Cookie: 瀏覽器存儲數據的機制
- Session: 服務器存儲數據的機制
Session ID 一般保存在 Cookie 中,客戶端每次請求都會攜帶這個 Session ID(通過 Cookie),服務器根據 Session ID 查找對應的服務端 Session 數據
?? 通常在單體系統中,Web服務將會用戶的Session信息(例如用戶登錄信息)保存在自己的服務器中。但是在分布式系統中,這樣做會有問題。因為分布式系統通常有很多個服務,每個服務又會同時部署在多臺機器上,通過負載均衡機制將將用戶的訪問均衡到不同服務器上。這個時候用戶的請求可能分發到不同的服務器上,從而導致用戶登錄保存Session是在一臺服務器上,而讀取Session是在另一臺服務器上因此會讀不到Session。
實際案例理解:醫院就診
- 初次就診:我(客戶端)生病了,聲帶發炎,發燒到完全說不出話。到醫院掛號,掛了個專家號。醫生(服務器)開了霧化理療,先開了一周的藥量,并建議一周后再來復查。
- 復查:一周后去復查,發現初診的醫生不在。新的醫生之前沒有給我看過病,不了解我的情況。新醫生通過刷我的就診卡(會話標識),看到了我之前的病例和治療情況。
問題:同一個客戶端多次訪問可能遇到不同的服務器。解決方案如下:
- 共享會話數據 :借助一個集中的會話存儲系統(如 Redis),所有服務器都可以讀取和更新同一個用戶的會話信息。客戶端只需攜帶會話標識(如 Session ID),無論請求被分發到哪臺服務器,都能正確識別用戶身份和狀態。
- 使用 Cookie 存儲 Session ID :瀏覽器自動攜帶包含 Session ID 的 Cookie 到服務端,服務端通過該 ID 查詢 Redis 獲取完整會話信息。
Session 分散存儲 圖如下:
會話管理的重要性
- 一致性:確保客戶端在多次訪問中的一致性體驗。
- 數據共享:多個服務器之間共享會話數據,避免因服務器切換導致的信息丟失。
解決方案:這種問題通常的做法是把Session存到一個公共的地方,讓每個Web服務,都去這個公共的地方存取Session。而Redis就可以是這個公共的地方。(數據庫、memecache等都可以各有優缺點)。
Redis 集中管理 Session
4. 示例:手機驗證碼
- 用戶在登錄的時候,為了保證用戶賬號的安全,我們會使用驗證碼.
- 當用戶登錄的時候,redis就會在服務器中保存一個與用戶對應的驗證碼,這個驗證碼具有過期時間(比如在5分鐘內有效).
- 在用戶輸入驗證碼之后,會從redis中查詢對應的鍵值對,校驗用戶的驗證碼.
- 當然為了用戶反復接收驗證碼,導致redis壓力過大,一般規定在一分鐘之內,最多接收一次驗證碼,如果手機沒有驗證碼,可以嘗試在一分鐘之后重新獲取驗證碼.
此功能可以用以下偽代碼說明基本實現思路:
String SendCapcha(String phoneNumber) {String key = "shortMsg:limit:" + phoneNumber;// 設置過期時間為 1 分鐘// 使用 NX,只在不存在 key 時才能設置成功bool r = Redis 執行命令:set key ex 60 nx;if (r == false) {// 說明之前設置過該手機的驗證碼了long c = Redis 執行命令:incr key;if (c > 5) {// 說明超過一分鐘 5 次的限制了// 限制發送return null;}}// 說明要么之前沒有設置過手機的驗證碼;要么次數沒有超過 5 次String validationCode = 生成隨機的 6 位數的驗證碼();String validationKey = "validation:" + phoneNumber;// 驗證碼 5 分鐘內有效Redis 執行命令:set validationKey validationCode ex 300;// 返回驗證碼,隨后通過手機短信發送給用戶return validationCode;
}// 驗證用戶輸入的驗證碼是否正確
bool VerifyCode(String phoneNumber, String validationCode) {String validationKey = "validation:" + phoneNumber;String value = Redis 執行命令:get validationKey;if (value == null) {// 說明沒有這個手機的驗證碼記錄,驗證失敗return false;}if (value.equals(validationCode)) {return true;} else {return false;}
}
🏑 小結:Redis 的 String 是一種二進制安全的字符串類型,支持任意格式的數據存儲(文本、數字、二進制),不處理字符集編碼,適用于緩存、計數器、Session 存儲等多種場景,但在使用時應注意控制數據大小和字符集一致性,以保證性能和正確性。