Redis的數據結構1 - string


今天來聊聊 Redisstring,這一數據結構。

string簡介

stringRedis中最基本,也是最簡單的數據結構。一個鍵(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 的相關命令介紹的時候,我其實使用一個錯誤的描述。就是將RedisString類型稱爲字符串。這種說法其實不正確的。

redis 中, string 這一數據結構使用sds來表示的。

sds

sdssimple 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' 舉個例子: 原有的sdslen5,alloc5, 要拼接的字符串長度爲15, 那麼新分配的空間大小是: (5byte+15byte)*2 + 1byte = 41byte.
  • 如果sds的長度大於等於默認的預分配空間, 那麼就在新分配的空間大小基礎上,在分配1MB的空間。如果修改後的,SDSlen20M,那麼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.c1120行。這裏不再深入學習了。

二進制安全

sdsAPI都是二進制安全的。因爲Redissds結構中的buf數組中的數據都是以二進制的方式處理的。

兼容部分的C字符串函數

Redis還是遵循了C字符串以 '\0'結尾的習慣,所以保存了文本數據的sds是可以複用 <string.h>庫中的函數。

總結

  • stringredis中最簡單的數據結構. string不是C字符串,而是對C字符串進行了封裝.

  • 學習了string類型相關的apiset,setnx,setex, get, getset, incr, decr,…

  • sds這種設計的好處,提高了性能,優化內存分配,二進制安全,兼容C字符串。

最後

希望和你成爲朋友!我們一起學習~
最新文章盡在公衆號【方家小白】,期待和你相逢在【方家小白】

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