文章目錄
今天來聊聊
Redis
的string
,這一數據結構。
string
簡介
string
是Redis
中最基本,也是最簡單的數據結構。一個鍵(key
) 對應着一個string
類型的值(value
). 我們都知道redis
是使用C
語言來編寫的,但是 string
這一個數據結構並非是使用C
語言的 string(char[])
來實現的,要想先了解,那就做電梯吧->( 電梯直達 ).
現在,先暫且拋開內部實現,我們先看看有怎麼使用這一數據結構。
string
相關常用命令
set
命令
SET key value [expiration EX seconds|PX milliseconds] [NX|XX]
使用示例:
# 1.設置一個鍵值對 f1=>f1
127.0.0.1:6379> set k1 v1
OK
# 根據鍵查詢值
127.0.0.1:6379> get k1
"v1"
# 2.設置一個鍵值對(f2=>f2),設置超時時間爲10s
# EX 表示秒
127.0.0.1:6379> set k2 v2 EX 10
OK
127.0.0.1:6379> get k2
"v2"
# 等待10s之後去查詢f2
127.0.0.1:6379> get k2
(nil)
# 3.設置一個鍵值對(f3=>f3),設置超時時間爲 10000毫秒
# PX 表示爲毫秒
127.0.0.1:6379> set k3 v3 PX 10000
OK
127.0.0.1:6379> get k3
"v3"
127.0.0.1:6379> get k3
(nil)
# 4.設置鍵值對k4=>v4,驗證"存在相同的key就設置失敗"
# setnx 命令也可實現,注意返回值。
127.0.0.1:6379> set k4 v4 NX
OK
# 如果存在相同的key就設置失敗(與下面的注意對比)
127.0.0.1:6379> set k4 v4 NX
(nil)
# 5.驗證"不存在相同的key就設置失敗"
127.0.0.1:6379> set k5 v5 XX
(nil)
# 先設置一個鍵值對,
127.0.0.1:6379> set k5 v5
OK
# 設置不存在相同的key就設置失敗
127.0.0.1:6379> set k5 v5 XX
OK
setnx
命令
setnx key value
set if not exists
的縮寫。如果已存在key,返回0, 不存在返回1. 常用於分佈式鎖。
使用實例
# 設置一個不存在的鍵值對 k6=>v6
127.0.0.1:6379> setnx k6 v6
(integer) 1
# 如果key已經存在,則返回0。
127.0.0.1:6379> setnx k6 v6
(integer) 0
setEx
命令
setex key seconds value
給鍵值對設置生存時間(秒級別)。
# 設置k7=>v7這個鍵值對的生存時間爲5s
127.0.0.1:6379> setex k7 5 v7
OK
127.0.0.1:6379> get k7
"v7"
# 過5s秒鐘之後,再查看。
127.0.0.1:6379> get k7
(nil)
127.0.0.1:6379>
psetEx
命令
psetex key milliseconds value
tip
: 命令助記:psetex
,p
直接的是毫秒。可以參考set
命令的PX
選項。
給鍵值對設置生存時間(毫秒級別)。
# 設置鍵值對
127.0.0.1:6379> psetex k8 5000 v8
OK
# 獲取k8的值
127.0.0.1:6379> get k8
"v8"
# 5s之後,獲取k8的值
127.0.0.1:6379> get k8
(nil)
get
命令
這個命令不多說了, 獲取key
相關聯的value
. get key
getset
命令
getset key value
設置鍵值對, key=>value
, 如果key
已經存在,返回舊值。不存在返回 nil
# 設置鍵值對
127.0.0.1:6379> getset k9 v9
(nil)
# 獲取值
127.0.0.1:6379> get k9
"v9"
# 在設置一次k9,值爲vv9,返回舊值 v9
127.0.0.1:6379> getset k9 vv9
"v9"
ps: 如果原來的存在key,但是value的類型與新設置的類型不一致,會拋出命令錯誤。
# 設置一個list類型,key爲k9_1, Value中只有一個元素v9_1
127.0.0.1:6379> lpush k9_1 v9_1
(integer) 1
# 使用getset命令載設置一次,拋出命令錯誤。
127.0.0.1:6379> getset k9_1 vv9_1
(error) WRONGTYPE Operation against a key holding the wrong kind of value
strlen
命令
strlen key
返回字符串的長度. 如果key不存在的時候,返回0,如果key對應的不是一個字符串時,返回錯誤.
127.0.0.1:6379> set k10 v10
OK
127.0.0.1:6379> strlen k10
(integer) 3
# 演示報錯
127.0.0.1:6379> lpush k10_1 v10
(integer) 1
127.0.0.1:6379> strlen k10_1
(error) WRONGTYPE Operation against a key holding the wrong kind of value
APPEND
命令
APPEND key value
命令
根據key,給key對應的值追加字符串。如果key不存在,就設置一對鍵值對。
# 如果key不存在則設置鍵值對
127.0.0.1:6379> append k11 v11
(integer) 3
127.0.0.1:6379> get k11
"v11"
# 如果存在,則追加
127.0.0.1:6379> append k11 v11
(integer) 6
127.0.0.1:6379> get k11
"v11v11"
setrange
命令
setrange key offset value
從偏移量 offset
開始覆寫原來key
的值。如果key
不存的時候當作空字符串處理。返回被設置後Value
的長度。
# 設置不存在的key
127.0.0.1:6379> setrange k12 3 v12
(integer) 6
# 在offset前的空位置會用 \x00 填充
127.0.0.1:6379> get k12
"\x00\x00\x00v12"
# 設置已經存在的key
127.0.0.1:6379> setrange k12 4 v12
(integer) 7
127.0.0.1:6379> get k12
"\x00\x00\x00vv12"
getrange
命令
getrange key start end
獲取指定區間的值.報錯start和end位置。索引位置是從0開始的。
負數偏移量表示從字符創的末位開始計數。
127.0.0.1:6379> set k13 v13v13v13
OK
127.0.0.1:6379> getrange k13 2 5
"3v13"
# 從索引爲2處,到倒數第4位。
127.0.0.1:6379> getrange k13 2 -4
"3v13"
# 如果end大於Value的長度,返回目前start到結束的部分
127.0.0.1:6379> getrange k13 3 10
"v13v13"
# 超過Value的長度返回爲 ""
127.0.0.1:6379> getrange k13 100 120
""
incr
命令
incr key
在key對應的Value上進行自增1. 如果Value可以解釋爲數據,則自增,反之,返回錯誤。
返回值爲自增後的值。
如果ke不存在,則先初始化 key對應的Value=0, 然後再自增。
相對的是: DECR
命令
127.0.0.1:6379> incr k14
(integer) 1
127.0.0.1:6379> get k14
"1"
127.0.0.1:6379> incr k14
(integer) 2
incrby
命令
incrby key increment
帶有步長的自增命令。
相對的命令是: DECRBY
命令
127.0.0.1:6379> incrby k15 5
(integer) 5
127.0.0.1:6379> INCRBY k15 5
(integer) 10
127.0.0.1:6379> INCRBY k15 5
(integer) 15
INCRBYFLOAT
命令
INCRBYFLOAT key increment
帶有步長的浮點數自增
127.0.0.1:6379> INCRBYFLOAT k16 5.0
"5"
127.0.0.1:6379> INCRBYFLOAT k16 5.2
"10.2"
127.0.0.1:6379> INCRBYFLOAT k16 5.4
"15.6"
DECR
命令
DECR key
自減1
.
# 如果key,不存在,同樣會初始化爲0,然後自減1
127.0.0.1:6379> DECR k17
(integer) -1
127.0.0.1:6379> DECR k17
(integer) -2
127.0.0.1:6379> DECR k17
(integer) -3
DECRBY
命令
帶有步長的自減命令, 與 INCRBY
命令相對。
# 如果key不存在,會初始化爲0,在進行自減。
127.0.0.1:6379> DECRBY k18 5
(integer) -5
127.0.0.1:6379> DECRBY k18 5
(integer) -10
mget
命令
mget key [key ...]
一次性返回多個key
的值。 如果key
不存在,返回 (nil)
127.0.0.1:6379> set k19_0 v19_0
OK
127.0.0.1:6379> set k19_1 v19_1
OK
127.0.0.1:6379> mget k19_0 k19_1
1) "v19_0"
2) "v19_1"
# 如果key不存在的時候,返回 (nil)
127.0.0.1:6379> mget k19_0 k19_1 k10_2
1) "v19_0"
2) "v19_1"
3) (nil)
mset
命令
同時爲設置多個鍵值對。 如果key已經存在,直接覆蓋掉。
注意: 這個原子性操作. 所有給定的key都會在同一時間內被設置。
tips: 如果希望,已經存在的key不被覆蓋,可以參考
msetnx
命令
# 一下設置三對
127.0.0.1:6379> mset k20_0 v20_0 k20_1 v20_1 k20_2 v20_2
OK
127.0.0.1:6379> mget k20_0 k20_1 k20_2
1) "v20_0"
2) "v20_1"
3) "v20_2"
# 演示已有的key對應的值會被覆蓋掉。
127.0.0.1:6379> mset k20_2 vv20_2 k20_3 v20_3
OK
127.0.0.1:6379> mget k20_2 k20_3
1) "vv20_2"
2) "v20_3"
msetnx
命令
MSETNX key value [key value ...]
當且僅當所有給定的key不存在的時候,纔會設置鍵值對。即使有一個key存在,該命令也不會設置其他的key對應的鍵值對.
# 演示設置成功
127.0.0.1:6379> MSETNX k21_0 v21_0 k21_1 v21_1
(integer) 1
127.0.0.1:6379> MGET k21_0 k21_1
1) "v21_0"
2) "v21_1"
# 存在其中的一個給定key,就不能設置成功
127.0.0.1:6379> msetnx k21_1 vv21_1 k21_2 v21_2
(integer) 0
127.0.0.1:6379> MGET k21_1 k21_2
1) "v21_1"
2) (nil)
Redis
如何實現String
這一數據結構
在 string
的相關命令介紹的時候,我其實使用一個錯誤的描述。就是將Redis
的String
類型稱爲字符串。這種說法其實不正確的。
在 redis
中, string
這一數據結構使用sds
來表示的。
sds
sds
是 simple dynamic string
的簡稱。 意思是 簡單的動態字符串
。 這裏面的string
就是實打實的C
語言中的字符串(char[]
). Redis
也並非一點也沒有使用 C
語言的字符串,像一些字面量常亮,日誌都是使用C
語言的字符串。
那 sds
到底是一個什麼樣的結構呢?
在源碼的 src
目錄下,我找到了 sds.h
這樣一個文件。這裏規定了 sds
結構。
struct __attribute__ ((__packed__)) sdshdr64 {
// 表示已使用的長度,即buf[]的長度。
uint64_t len;
// 已分配的長度(包括未使用的長度)
// alloc-len,對應着之前版本的free
uint64_t alloc;
unsigned char flags;
char buf[];
};
tips: 如果你注意到了這個結構體的命名.那麼來看下這篇文章吧。
sds
保留了 C
字符串以空字符結尾的慣例。保留的這個空字符的長度不會保存在 len
字段中。保留這一慣例的好處就是可以使用C
字符串函數庫的一些方法。
假設我們分配了10
個字節空間,只保存了 redis
這個C
字符串,那麼 在sds
中,是這麼表示的:
使用sds
比使用C
字符串有什麼好處呢?
獲取字符長度的時間複雜度爲 O(1)
C
語言獲取一個字符串的長度爲 O(N)
. 需要遍歷字符串並累加,判斷字符是否爲 '\0'
來獲得字符串的長度。
sds
只需要根據 len
字段獲取即可。怎麼獲取的呢?
我們來看下源碼。
// 定義char類型的指針類型。
typedef char *sds;
// 獲取長度的結構體指針的宏.
// 可根據指向buf的指針返回指向sdshdr結構體首地址的宏
#define SDS_HDR(T,s) ((struct sdshdr##T *)((s)-(sizeof(struct sdshdr##T))))
// sds 直接指向結構體裏的buf
static inline size_t sdslen(const sds s) {
// sds是直接指向結構體裏的buf數據, 當獲取len等字段的信息,只需要減去結構體長度,回退一下指針就可以了。
// 這裏使用的尾指針法。
unsigned char flags = s[-1];
// 判斷屬於那種 sdshdr,對應減去不同的地址。
switch(flags&SDS_TYPE_MASK) {
case SDS_TYPE_5:
return SDS_TYPE_5_LEN(flags);
case SDS_TYPE_8:
return SDS_HDR(8,s)->len;
case SDS_TYPE_16:
return SDS_HDR(16,s)->len;
case SDS_TYPE_32:
return SDS_HDR(32,s)->len;
case SDS_TYPE_64:
return SDS_HDR(64,s)->len;
}
return 0;
}
可以杜絕緩衝區溢出
C
語言是不會判斷數組是否越界的。比如 strcat
方法, 如果當前的數據不能容納拼接之後字符時,必然會發生緩存區溢出。
但是 sds
則不會。我們來看下 sds
的字符串拼接的方法 sdscat
。
// s 原來的字符串,t是要拼接的字符串
sds sdscat(sds s, const char *t) {
return sdscatlen(s, t, strlen(t));
}
sds sdscatlen(sds s, const void *t, size_t len) {
// 獲取原來字符串的長度。(見上面的方法)
size_t curlen = sdslen(s);
// 擴大sds字符串末尾的可用空間,
//以便調用者確保在調用此函數後可以覆蓋字符串末尾的addlen字節,
//再爲null項再加上一個字節。 具體實現,參考源碼(sds.c:204)。
s = sdsMakeRoomFor(s,len);
// 如果內存分配失敗,就會返回null
if (s == NULL) return NULL;
// 調用C語言的分配
memcpy(s+curlen, t, len);
// sds設置 sdshdr的len字段的值。
sdssetlen(s, curlen+len);
// 添加最後一個字符爲: '\0'
s[curlen+len] = '\0';
return s;
}
sds 優化了C語言的內存分配策略
空間預分配
空間預分配策略遵循下面的公式:
- 如果
SDS
的長度小於最大的預分配空間(1MB
),那麼會分配兩倍的新空間,再加上結尾的空字符'\0'
舉個例子: 原有的sds
的len
爲5
,alloc
爲5
, 要拼接的字符串長度爲15
, 那麼新分配的空間大小是:(5byte+15byte)*2 + 1byte = 41byte
. - 如果
sds
的長度大於等於默認的預分配空間, 那麼就在新分配的空間大小基礎上,在分配1MB
的空間。如果修改後的,SDS
的len
是20M
,那麼alloc
就是20M + 1M + 1byte
具體分配過程見下面的源碼
// SDS 默認最大的預分配空間爲1M
#define SDS_MAX_PREALLOC (1024*1024)
// sds 預分配空間
sds sdsMakeRoomFor(sds s, size_t addlen) {
void *sh, *newsh;
size_t avail = sdsavail(s);
size_t len, newlen;
char type, oldtype = s[-1] & SDS_TYPE_MASK;
int hdrlen;
/* 如果空間足夠,直接返回 */
if (avail >= addlen) return s;
len = sdslen(s);
sh = (char*)s-sdsHdrSize(oldtype);
newlen = (len+addlen);
if (newlen < SDS_MAX_PREALLOC)
newlen *= 2;
else
newlen += SDS_MAX_PREALLOC;
type = sdsReqType(newlen);
/* Don't use type 5: the user is appending to the string and type 5 is
* not able to remember empty space, so sdsMakeRoomFor() must be called
* at every appending operation. */
if (type == SDS_TYPE_5) type = SDS_TYPE_8;
hdrlen = sdsHdrSize(type);
if (oldtype==type) {
newsh = s_realloc(sh, hdrlen+newlen+1);
if (newsh == NULL) return NULL;
s = (char*)newsh+hdrlen;
} else {
/* Since the header size changes, need to move the string forward,
* and can't use realloc */
newsh = s_malloc(hdrlen+newlen+1);
if (newsh == NULL) return NULL;
memcpy((char*)newsh+hdrlen, s, len+1);
s_free(sh);
s = (char*)newsh+hdrlen;
s[-1] = type;
sdssetlen(s, len);
}
sdssetalloc(s, newlen);
return s;
}
惰性空間釋放
當對sds進行縮短操作時,程序並不會立馬對內存重分配來回收收縮的空間,而是僅僅改變len
屬性,並且在隊對應的位置上將字符設置爲: '\0'
以 函數 sdstrim
爲例。
sds sdstrim(sds s, const char *cset) {
char *start, *end, *sp, *ep;
size_t len;
sp = start = s;
ep = end = s+sdslen(s)-1;
while(sp <= end && strchr(cset, *sp)) sp++;
while(ep > sp && strchr(cset, *ep)) ep--;
len = (sp > ep) ? 0 : ((ep-sp)+1);
// 進行移位
if (s != sp) memmove(s, sp, len);
// 設置字符串結束符
s[len] = '\0';
// 設置len字段的值
sdssetlen(s,len);
return s;
}
上述實現中,並沒有進行內存回收。sds
也提供了內存回收的函數sds_free
.具體可以看Redis 5.0.7
版源碼. sds.c
第1120
行。這裏不再深入學習了。
二進制安全
sds
的API
都是二進制安全的。因爲Redis
對 sds
結構中的buf
數組中的數據都是以二進制的方式處理的。
兼容部分的C
字符串函數
Redis
還是遵循了C
字符串以 '\0'
結尾的習慣,所以保存了文本數據的sds
是可以複用 <string.h>
庫中的函數。
總結
-
string
是redis
中最簡單的數據結構.string
不是C
字符串,而是對C
字符串進行了封裝. -
學習了
string
類型相關的api
。set
,setnx
,setex
,get
,getset
,incr
,decr
,… -
sds
這種設計的好處,提高了性能,優化內存分配,二進制安全,兼容C
字符串。
最後
希望和你成爲朋友!我們一起學習~
最新文章盡在公衆號【方家小白】,期待和你相逢在【方家小白】