Redis開發與運維:SDS

STRING

咱們會常常打交道的string類型,在redis中擁有普遍的使用。也是開啓redis數據類型的基礎。git

在我最最開始接觸的redis的時候,老是覺得字符串類型就是值的類型是字符串。github

好比:SET key valueredis

個人理解是value數據類型是stirng類型,如今來看呢,這句話說得不夠具體全面。算法

  • 全部的鍵都是字符串類型sql

  • 字符串類型的值能夠是字符串、數字、二進制數據庫

這裏也就引出了,另外一個概念:外部類型和內部類型數組

外部類型 vs 內部類型

這裏的外部類型,就是咱們所熟知的:字符串(string)、哈希(hash)、列表(list)、集合(set)、有序結合(zset)等

Q1:那麼什麼是內部類型呢?緩存

Q2:外部類型和內部類型是何時出現的?安全

Q3:爲何要這樣設計?數據結構

咱們先來看問題1,能夠這樣理解,對外數據結構就像是咱們的API,對外提供着必定組織結構的數據。

對內來講,咱們能夠更換裏面的邏輯算法,甚至更換數據存儲方式,好比將Mysql換成Redis.

內部類型其實就是數據存儲的形式。舉如今咱們所討論的stirng來講。

string的外部類型就是string,而它對應的數據內部存儲結構分爲三種。

int:8個字節的長整形
embstr:<=39個字節的字符串(3.2 版本變成了44)
raw:>39個字節的字符串(3.2 版本變成了44)

因此,string類型會根據當前字符串的長度來決定到底使用哪一種內部數據結構。

如今咱們再回到問題上:什麼是內部類型?

就是數據真正存儲在內存上的數據結構。

其實第二個問題:外部類型和內部類型是何時出現的?

這裏也算是有答案了,外部類型就是對外公開的數據類型也能夠說是API,內部類型根據長度判斷哪一種內部結構。

第三個問題:爲何這樣設計?

先後分離,若是有更好地內部數據類型,咱們能夠替換後面的數據類型,但不影響前面的Api.

還有一點也是根據不一樣狀況,選擇更好地數據結構,節省內存。畢竟是內存數據庫,資源珍貴。

如何查看外部類型和內部類型

查看外部類型:type

127.0.0.1:6999[1]> SET sc sunchong   // 對外類型:string
OK
127.0.0.1:6999[1]> type sc
string
127.0.0.1:6999[1]> HSET hsc sun chong    // 對外類型:hash
(integer) 1
127.0.0.1:6999[1]> type hsc
hash
127.0.0.1:6999> RPUSH rsc s un ch hong
(integer) 4
127.0.0.1:6999> TYPE rsc
list

查看內部類型:object

int

127.0.0.1:6999[1]> set sc 1234567890123456789   // 對內類型:int
OK
127.0.0.1:6999[1]> STRLEN sc
(integer) 19
127.0.0.1:6999[1]> OBJECT encoding sc
"int"

int -> embstr

(int 8位的長整形,最大存儲十進制位數爲19位)

127.0.0.1:6999[1]> set sc 12345678901234567890   // 對內類型:embstr
OK
127.0.0.1:6999[1]> STRLEN sc
(integer) 20
127.0.0.1:6999[1]> OBJECT encoding sc   
"embstr"

embstr -> raw

127.0.0.1:6999[1]> set sc 123456789012345678901234567890123456789  
OK
127.0.0.1:6999[1]> STRLEN sc
(integer) 39
127.0.0.1:6999[1]> OBJECT encoding sc
"embstr"
127.0.0.1:6999[1]> set sc 12345678901234567890123456789012345678901  
OK
127.0.0.1:6999[1]> STRLEN sc
(integer) 41
127.0.0.1:6999[1]> OBJECT encoding sc
"embstr"

額,這裏我看《Redis 開發與運維》一書

39字節,embstr 轉raw。寫錯了?

個人本機redis版本是5.0+,這本書是3.0,中間確定是有了版本更新。

試試看看源碼和提交記錄 (https://github.com/antirez/redis/commit/f15df8ba5db09bdf4be58c53930799d82120cc34#diff-43278b647ec38f9faf284496e22a97d5)

繼續嘗試 embstr -> raw

127.0.0.1:6999[1]> set sc 12345678901234567890123456789012345678901234
OK
127.0.0.1:6999[1]> STRLEN sc
(integer) 44
127.0.0.1:6999[1]> OBJECT encoding sc
"embstr"
127.0.0.1:6999[1]> set sc 123456789012345678901234567890123456789012345  // 對內類型:raw
OK
127.0.0.1:6999[1]> STRLEN sc
(integer) 45
127.0.0.1:6999[1]> OBJECT encoding sc
"raw"

經常使用命令

set key value [EX seconds] [PX milliseconds] [NX|XX]

-- ex 秒級過時時間

-- px 毫秒級過時時間

-- nx 不存在才能執行成功,相似添加

-- xx 必須存在才能執行成功,相似修改

nx

127.0.0.1:6999[1]> EXISTS bus
(integer) 0
127.0.0.1:6999[1]> SET bus Q xx
(nil)
127.0.0.1:6999[1]> SET bus Q nx
OK

xx

127.0.0.1:6999[1]> EXISTS car
(integer) 0
127.0.0.1:6999[1]> SET car B
OK
127.0.0.1:6999[1]> SET car C nx
(nil)
127.0.0.1:6999[1]> SET car C xx
OK
127.0.0.1:6999[1]> GET car
"C"

setnx / setxx

這兩個命令會逐步棄用

String類型源碼分析

SDS 數據結構

爲何Redis要本身實現一套簡單的動態字符串?

1. 效率
2. 安全(二進制安全:C語言中的字符串已 「\0」 爲結束標誌。)
3. 擴容

若是說,有一輛車,到站前提早告知車站乘客,本次列車還有多少餘座。

此時,若是有個計數器能夠計算一下當前坐了多少乘客,同時還有多少空位就行了。

這樣司機師傅就沒必要每次停車上客前,數數還有多少座位能夠坐。能夠專心開車。

一樣,Redis SDS 也使用了這樣一些小小的記錄,

使用時候獲取這個記錄,時間複雜度是O(1),效率是很高的。不用每次都去統計。

redis作了這樣的設計:

struct sdshdr {
    unsigned int len;
    unsigned int free;
    char buf[];
};
len 已用字節數

free 未用字節數

buf[]  字符數組

這樣設計有什麼好處?
1. 方便統計當前長度等,時間複雜度是O(1)
2. 有了長度這些關鍵屬性,能夠不依賴「\0」 終止符。二進制安全。
3. 指針返回的是buf[],這樣能夠複用C字符串相關的函數。避免重複造輪子,兼容C字符串操做
4. 前面的len和free以及數組指針buf,內存分配上地址是連續的。因此很容易使用buf地址找到len和free.

咱們先來看看,這個數據結構:

問題來了,是否還有優化的空間呢?

這樣問比較籠統。咱們思考一種場景:是否是全部的字符串存儲都須要這樣的結構?

到這裏,有經驗的你已經想到,全部的狀況用沒問題,可是Redis是內存數據庫,

內存是資源,如何在資源上斤斤計較是Redis必須權衡的問題。

如今咱們坐下來仔細分析一下:

unsigned int len 能夠存的數據範圍是:0 ~ 4294967295 (4 Bytes)

Redis中的字符串長度每每不須要這麼大,多大合適呢?

1字節(Byte)? 這樣?
struct sdshdr {
    char len;
    char free;
    char buf[];
};

呀, 1字節是0~255,通常長度的字符串足夠用。

若是真的存儲了1個字節的字符串,len和free加起來也佔了兩個字節。

原本數據就1字節大,我爲了存數據,額外信息都佔2字節。

再優化,只能使用位來存儲長度

假設,咱們從全局來看,將字符串長度(小於1KB,1KB,2KB,4KB,8KB)來表示。

對於1字節,至少要拿出3個位,才能覆蓋這5種狀況( 2^3=8),那麼剩下的5位才能存儲長度。

如今咱們已經進入到了Redis5.0 數據結構時代:

struct __attribute__ ((__packed__)) sdshdr5 {
    unsigned char flags; /* 3 lsb of type, and 5 msb of string length */
    char buf[];
};

3個低位標識類型,5個高位存長度(2^5=32)

說到這,長度大於31('\0'結束符)的字符串,1個字節是存不下的。

咱們仍是按照以前的邏輯 len和free再結合剛纔的按位看長度類型,來看看大於1字節的數據結構:

struct __attribute__ ((__packed__)) sdshdr8 {
    uint8_t len; /* used */
    uint8_t alloc; /* excluding the header and null terminator */
    unsigned char flags; /* 3 lsb of type, 5 unused bits */
    char buf[];
};
使用了多少(len)、分配了多大(alloc)、長度類型標識(flags) ---- 這些表頭= 1字節+1字節+1字節 ,共3字節
因此Redis對:字符串大小的界限就有了對應的宏定義
#define SDS_TYPE_5  0     // 小於1KB
#define SDS_TYPE_8  1     
#define SDS_TYPE_16 2    
#define SDS_TYPE_32 3    
#define SDS_TYPE_64 4
對應的數據結構就是:
struct __attribute__ ((__packed__)) sdshdr5 {
    unsigned char flags; /* 3 lsb of type, and 5 msb of string length */
    char buf[];
};
struct __attribute__ ((__packed__)) sdshdr8 {
    uint8_t len; /* used */
    uint8_t alloc; /* excluding the header and null terminator */
    unsigned char flags; /* 3 lsb of type, 5 unused bits */
    char buf[];
};
struct __attribute__ ((__packed__)) sdshdr16 {
    uint16_t len; /* used */
    uint16_t alloc; /* excluding the header and null terminator */
    unsigned char flags; /* 3 lsb of type, 5 unused bits */
    char buf[];
};
struct __attribute__ ((__packed__)) sdshdr32 {
    uint32_t len; /* used */
    uint32_t alloc; /* excluding the header and null terminator */
    unsigned char flags; /* 3 lsb of type, 5 unused bits */
    char buf[];
};
struct __attribute__ ((__packed__)) sdshdr64 {
    uint64_t len; /* used */
    uint64_t alloc; /* excluding the header and null terminator */
    unsigned char flags; /* 3 lsb of type, 5 unused bits */
    char buf[];
};

建立字符串

sds sdsnewlen(const void *init, size_t initlen);

看看註釋,很是明白:

/* Create a new sds string with the content specified by the 'init' pointer
 * and 'initlen'.
 * If NULL is used for 'init' the string is initialized with zero bytes.
 * If SDS_NOINIT is used, the buffer is left uninitialized;
 *
 * The string is always null-termined (all the sds strings are, always) so
 * even if you create an sds string with:
 *
 * mystring = sdsnewlen("abc",3);
 *
 * You can print the string with printf() as there is an implicit \0 at the
 * end of the string. However the string is binary safe and can contain
 * \0 characters in the middle, as the length is stored in the sds header. */

獲取SDS類型

char type = sdsReqType(initlen);

SDS_TYPE_5 通常用於字符串追加,因此仍是用8這個。

if (type == SDS_TYPE_5 && initlen == 0) type = SDS_TYPE_8;

獲取頭長度

int hdrlen = sdsHdrSize(type);

申請內存(頭+數據體+終止符)

sh = s_malloc(hdrlen+initlen+1);

s=數據體buf[]指針

s = (char*)sh+hdrlen;

buf[]指針-1,就找到了長度類型flag

fp = ((unsigned char*)s)-1;

最後綴上結束符,而後返回的是buf[]指針,兼容C語言字符串

s[initlen] = '\0';
    return s;

釋放字符串

void sdsfree(sds s)

直接釋放內存

if (s == NULL) return;
  s_free((char*)s-sdsHdrSize(s[-1]));
}

爲了不頻繁申請關釋放內存, 把使用量len重置爲0,同時清空數據

void sdsclear(sds s) {
  sdssetlen(s, 0); 
  s[0] = '\0';
}

好處數據能夠複用,避免從新申請內存

應用場景

用戶信息

最近用戶中心的訪問壓力極大,數據庫已經扛不住。

咱們使用比家裏快並且成熟的技術,就是再加一層緩存。

好比:

uid:ui01

username: sunchong

nickname:二中

roletype:01

level:0

需求是:用戶中心的用戶數據,能夠用uid拿到,也能夠根據username拿到(uid和username 都是惟一不重複的)

我根據uid能夠獲取查詢到用戶,也能夠根據username獲取到用戶。

首先,使用哈希進行數據的緩存 — HSET user:ui01 key1 value1 key2 value2 key3 value3 ...

127.0.0.1:6999> HSET user:ui01 username sunchong nickname 二中 roletype 01 level 0
(integer) 4
127.0.0.1:6999> HKEYS user:ui01
1) "username"
2) "nickname"
3) "roletype"
4) "level"

而後建立映射關係:

127.0.0.1:6999> SET user:sunchong ui01
OK
127.0.0.1:6999> GET user:sunchong
"ui01"

經過 username 找到主鍵uid,而後根據主鍵獲取用戶信息。

數據量較多時,過時時間設置爲必定區間內的隨機數。避免緩存穿透。

接口請求次數

當前咱們有對用戶開放的API,用戶充值後使用,使用次數累加,剩餘次數遞減。

127.0.0.1:6999> SET user-ui01:times 1000
OK

127.0.0.1:6999> INCR user-ui01:times
(integer) 1001

127.0.0.1:6999> GET user-ui01:times
"1001"
127.0.0.1:6999> DECR user-ui01:times
(integer) 1000

短信驗證碼

就在前幾天,咱們剛剛對接了阿里雲短信碼服務。

起初,我本身認爲短信驗證碼爲了實時性不須要進行實際的緩存處理。

可是徹底能夠根據實際狀況進行設計魂村策略。

爲了防止接口的頻繁調用,咱們能夠像網關同樣進行設置。

如今就有這樣一個需求:1個手機號,1分鐘最多獲取10次驗證碼

SET Catch:Limit:13355222226 1 ex 60 nx

初始化手機號,起始次數是1,默認過時時間60秒

再剩下的就是代碼判斷次數便可。

總結

字符串類型結合命令有不少的應用場景,這個有待去收集和發現。

Redis 比較容易上手,文檔全,代碼整潔高效。

固然更須要咱們去深刻其運行原理,來更好使用這個工具來服務咱們的業務。

相關文章
相關標籤/搜索