咱們會常常打交道的string類型,在redis中擁有普遍的使用。也是開啓redis數據類型的基礎。git
在我最最開始接觸的redis的時候,老是覺得字符串類型就是值的類型是字符串。github
好比:SET key valueredis
個人理解是value數據類型是stirng類型,如今來看呢,這句話說得不夠具體全面。算法
全部的鍵都是字符串類型sql
字符串類型的值能夠是字符串、數字、二進制數據庫
這裏也就引出了,另外一個概念:外部類型和內部類型數組
這裏的外部類型,就是咱們所熟知的:字符串(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. 還有一點也是根據不一樣狀況,選擇更好地數據結構,節省內存。畢竟是內存數據庫,資源珍貴。
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
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"
-- 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"
這兩個命令會逐步棄用
爲何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[]; };
#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 比較容易上手,文檔全,代碼整潔高效。
固然更須要咱們去深刻其運行原理,來更好使用這個工具來服務咱們的業務。