文章導航-readme
前言
上一篇文章Redis之對象篇——Redis對象系統簡介簡單介紹了Redis
的對象系統。Redis
使用對象來表示數據庫中的鍵和值每個對象都由一個redisObject
結構表示,該結構中和保存數據有關的三個屬性分別是type
屬性、 encoding
屬性和ptr
屬性。
typedef struct redisObiect{
//類型
unsigned type:4;
//編碼
unsigned encoding:4;
//指向底層數據結構的指針
void *ptr;
}
字符串對象是 Redis 中最基本的數據類型,也是我們工作中最常用的數據類型。redis中的鍵都是字符串對象,而且其他幾種數據結構都是在字符串對象基礎上構建的。字符串對象的值實際可以是字符串、數字、甚至是二進制,最大不能超過512MB 。
一、內部實現
Redis字符串對象底層的數據結構實現主要是int和簡單動態字符串SDS(這個字符串,和我們認識的C字符串不太一樣,了解具體請看圖解Redis之數據結構篇——簡單動態字符串SDS),其通過不同的編碼方式映射到不同的數據結構。
字符串對象的內部編碼有3種 :int
、raw
和embstr
。Redis會根據當前值的類型和長度來決定使用哪種編碼來實現。
-
如果一個字符串對象保存的是整數值,並且這個整數值可以用
long
類型來表示,那么字符串對象會將整數值保存在字符串對象結構的ptr
屬性里面(將void*
轉換成1ong
),並將字符串對象的編碼設置為int
。 -
如果字符串對象保存的是一個字符串值,並且這個字符串值的長度大於32字節,那么字符串對象將使用一個簡單動態字符串(SDS)來保存這個字符串值,並將對象的編碼設置為
raw
。 -
如果字符串對象保存的是一個字符串值,並且這個字符申值的長度小於等於32字節,那么字符串對象將使用一個簡單動態字符串(SDS)來保存這個字符串值,並將對象的編碼設置為
embstr
embstr
編碼是專門用於保存短字符串的一種優化編碼方式,我們可以看到embstr
和raw
編碼都會使用SDS
來保存值,但不同之處在於embstr
會通過一次內存分配函數來分配一塊連續的內存空間來保存redisObject
和SDS
。而raw
編碼會通過調用兩次內存分配函數來分別分配兩塊空間來保存redisObject
和SDS
。Redis這樣做會有很多好處。
embstr
編碼將創建字符串對象所需的內存分配次數從raw編碼的兩次降低為一次- 釋放
embstr
編碼的字符串對象同樣只需要調用一次內存釋放函數 - 因為
embstr
編碼的字符串對象的所有數據都保存在一塊連續的內存里面可以更好的利用CPU緩存提升性能。
Redis中根據數據類型和長度來使用不同的編碼和數據結構存儲存在於Redis中的每一種對象類型上。其這種小細節上的優化令我嘆服不止,后續我們會看到Redis中到處都是這種內存與性能上的小細節優化!
二、常用命令
字符串類型的命令比較多 ,我們先來了解幾個日常開發中常用的。
1 設置值
redis> set testKey testValue
OK
set key value [ex seconds] [px milliseconds] [nx|xx]
-
ex seconds
:為鍵設置秒級過期時間。如命令:
set username xiaoming ex 100
相當於執行下面兩條命令SET username xiaoming EXPIRE username 100
set key value [ex seconds]
操作是原子性的,相比連續執行上面兩個命令,它更快。 -
px milliseconds
:為鍵設置毫秒級過期時間。 -
nx
:鍵必須不存在,才可以設置成功,用於添加。//mykey 不存在 redis> set mykey "Hello" nx (integer) 1 //mykey 已經存在 redis> set mykey "World" nx (integer) 0 redis> GET mykey "Hello" redis>
由於
set key value nx
同樣是原子性的操作,因此可以作為分布式鎖的一種實現方案。 -
xx
:與nx相反,鍵必須存在,才可以設置成功,用於更新
以上幾個命令的替代命令是SETNX
, SETEX
,PSETEX
,但是由於SET
命令加上選項已經可以完全取代SETNX
, SETEX
,PSETEX
的功能,所以在將來的版本中,redis可能會不推薦使用並且最終拋棄這幾個命令。
2 獲取值
get key
返回key
的value
。如果key不存在,返回特殊值nil
。如果key
的value
不是string,就返回錯誤,因為GET
只處理string類型的values
。
redis> GET nokey
(nil)
redis> SET mykey "Hello World"
OK
redis> GET mykey
"Hello World"
3 批量設置值
由於Redis目前的應用非常廣泛,目前大多數公司對Redis的調用基本都會有一層自己的封裝,看起來就像是在調用本地緩存一樣,對於批量性的操作,一些對於Redis不太了解的可能就像使用本地緩存一樣進行循環set。這樣對性能是有很大的損耗的。實際上Redis提供了批量操作的命令。
MSET key value [key value ...]
對應給定的keys到他們相應的values上。MSET
會用新的value替換已經存在的value,就像普通的SET
命令一樣。如果不想覆蓋已經存在的values,可以使用MSETNX key value [key value ...]
注意:MSET
是原子的,所以所有給定的keys是一次性set的。客戶端不可能看到這種一部分keys被更新而另外的沒有改變的情況。
redis> MSET key1 "Hello" key2 "World"
OK
redis> GET key1
"Hello"
redis> GET key2
"World"
4 批量獲取值
MGET key [key ...]
結果是按照傳入鍵的順序返回所有指定的key的value。對於每個不對應string或者不存在的key,都返回特殊值nil。
redis> SET key1 "Hello"
OK
redis> SET key2 "World"
OK
redis> MGET key1 key2 nokey
1) "Hello"
2) "World"
3) (nil)
Redis可以支撐每秒數萬的讀寫操作,但是這指的是Redis服務端的處理能力,對於客戶端來說,一次命令除了命令時間還是有網絡時間,如n次get操作
使用get
命令
n次get時間 = n次網絡時間 + n次命令時間
而mget
操作
n次get時間 = 1次網絡時間 + n次命令時間
而在實際開發中因為Redis的處理能力已經足夠高,性能瓶頸的因素往往是網絡。
學會使用批量操作,有助於提高效率,但是要掌握一個平衡的度,每次批量操作所發送的命令數並不是無節制的由於Redis是單線程架構,如果數量過多可能造成Redis阻塞或者網絡擁塞。
5 計數
incr key
對存儲在指定key
的數值執行原子的加1操作。
如果指定的key不存在,那么在執行incr操作之前,會先將它的值設定為0
。
如果指定的key中存儲的值不是字符串類型(fix:)或者存儲的字符串類型不能表示為一個整數,
那么執行這個命令時服務器會返回一個錯誤(eq:(error) ERR value is not an integer or out of range)。
這個操作僅限於64位的有符號整型數據。
注意: 由於redis並沒有一個明確的類型來表示整型數據,所以這個操作是一個字符串操作。
執行這個操作的時候,key對應存儲的字符串被解析為10進制的64位有符號整型數據。
事實上,Redis 內部采用整數形式(Integer representation)來存儲對應的整數值,所以對該類字符串值實際上是用整數保存,也就不存在存儲整數的字符串表示(String representation)所帶來的額外消耗。
redis> SET mykey "1"
OK
redis> INCR mykey
(integer) 2
redis> GET mykey
"3"
redis>
除了incr
命令, Redis提供了decr(自減)
、 incrby(自增指定數字)
、decrby(自減指定數字)
、 incrbyfloat(自增浮點數)
decr key
incrby key increment
decrby key decrement
incrbyfloat key increment
6 其它
對於常用的redis字符串命令和一些其它的命令我們列一個表格以便來更直觀的看到。
命令 | 描述 | 時間復雜度 |
---|---|---|
set key value [ex seconds] [px milliseconds] [nx|xx] |
設置值 | O(1) |
get key |
獲取值 | O(1) |
del key [key ...] |
刪除key | O(N)(N是鍵的個數) |
mset key [key value ...] |
批量設置值 | O(N)(N是鍵的個數) |
mget key [key ...] |
批量獲取值 | O(N)(N是鍵的個數) |
incr key |
將 key 中儲存的數字值增一 | O(1) |
decr key |
將 key 中儲存的數字值減一 | O(1) |
incrby key increment |
將 key 所儲存的值加上給定的增量值(increment) | O(1) |
decrby key increment |
key 所儲存的值減去給定的減量值(decrement) | O(1) |
incrbyfloat key increment |
將 key 所儲存的值加上給定的浮點增量值(increment) | O(1) |
append key value |
如果 key 已經存在並且是一個字符串, APPEND 命令將指定的 value 追加到該 key 原來值(value)的末尾 | O(1) |
strlen key |
返回 key 所儲存的字符串值的長度。 | O(1) |
setrange key offset value |
用 value 參數覆寫給定 key 所儲存的字符串值,從偏移量 offset 開始 | O(1) |
getrange key start end |
返回 key 中字符串值的子字符 | O(N)(N是字符串的長度) |
三、常用場景
reids字符串的使用場景應該是最為廣泛的,甚至有些對redis其它幾種對象不太熟悉的人,基本所有場景都會使用字符串(序列化一下直接扔進去)。在眾多的使用場景中總結一下大概分以下幾種。
1. 作為緩存層
如上圖,Redis經常作為緩存層,來緩存一些熱點數據。來加速讀寫性能從而降低后端的壓力。一般在讀取數據的時候會先從Redis中讀取,如果Redis中沒有,再從數據庫中讀取。在Redis作為緩存層使用的時候,必須注意一些問題,如:緩存穿透、雪崩以及緩存更新問題......
2. 計數器\限速器\分布式系統ID
計數器\限速器\分布式ID等主要是利用Redis字符串自增自減的特性。
- 計數器:經常可以被用來做計數器,如微博的評論數、點贊數、分享數,抖音作品的收藏數,京東商品的銷售量、評價數等。
- 限速器:如驗證碼接口訪問頻率限制,用戶登陸時需要讓用戶輸入手機驗證碼,從而確定是否是用戶本人,但是為了短信接口不被頻繁訪問,會限制用戶每分鍾獲取驗證碼的頻率,例如一分鍾不能超過5次。
- 分布式ID:由於Redis自增自減的操作是原子性的因此也經常在分布式系統中用來生成唯一的訂單號、序列號等。
3. 分布式系統共享session
通常在單體系統中,Web服務將會用戶的Session信息(例如用戶登錄信息)保存在自己的服務器中。但是在分布式系統中,這樣做會有問題。因為分布式系統通常有很多個服務,每個服務又會同時部署在多台機器上,通過負載均衡機制將將用戶的訪問均衡到不同服務器上。這個時候用戶的請求可能分發到不同的服務器上,從而導致用戶登錄保存Session是在一台服務器上,而讀取Session是在另一台服務器上因此會讀不到Session。
這種問題通常的做法是把Session存到一個公共的地方,讓每個Web服務,都去這個公共的地方存取Session。而Redis就可以是這個公共的地方。(數據庫、memecache等都可以各有優缺點)。
4. 二進制存儲
由於Redis字符串可以存儲二進制數據的特性,因此也可以用來存儲一些二進制數據。如圖片、 音頻、 視頻等。
參考
《Redis設計與實現》
《Redis開發與運維》
《Redis官方文檔》