網站首頁 編程語言 正文
以下內容是基于Redis 6.2.6 版本整理總結
一、對象
前面幾篇文章,我們介紹了Redis用到的主要的數據結構,如:sds、list、dict、ziplist、skiplist、inset等。
但是,Redis并沒有直接使用這些數據結構來實現key-value數據庫,而是基于這些數據結構構建了一個對象系統。包括字符串對象、列表對象、哈希對象、集合對象和有序集合對象五種類型的對象。每種對象都使用了至少一種前面提到的數據結構。
通過對對象的區分,Redis可以在執行命令前判斷該對象是否能夠執行該條命令。為對象設置不同的數據結構實現,只要是為了提高效率。
二、對象的類型及編碼
Redis使用對象來表示數據中的key和value,每當我們在Redis數據庫中創建一個新的鍵值對時,至少會創建兩個對象,一個作用域key,另一個作用于value。
舉個栗子:set msg “hello world” 表示分別創建了一個字符串對象保存“msg”,另一個字符串對象保存“hello world”:
redisObject 結構體
Redis中的每個對象由 redisObject 結構體來描述,對象的類型、編碼、內存回收、共享對象都需要redisObject的支持,redisObject 結構體定義如下:
#define LRU_BITS 24
typedef struct redisObject {
unsigned type:4; // 類型
unsigned encoding:4; // 編碼
unsigned lru:LRU_BITS; /* LRU time (relative to global lru_clock) or
* LFU data (least significant 8 bits frequency
* and most significant 16 bits access time). */
int refcount;
void *ptr;
} robj;
下面我們來看看每個字段的含義:
(1)type: 占4個比特位,表示對象的類型,有五種類型。當我們執行type命令時,便是通過type字段獲取對象的類型。
/* The actual Redis Object */
#define OBJ_STRING 0 /* String object. */
#define OBJ_LIST 1 /* List object. */
#define OBJ_SET 2 /* Set object. */
#define OBJ_ZSET 3 /* Sorted set object. */
#define OBJ_HASH 4 /* Hash object. */
type命令使用示例:
(2)encoding: 占4個比特位,表示對象使用哪種編碼,redis會根據不同的場景使用不同的編碼,大大提高了redis的靈活性和效率。
字符串對象不同編碼類型示例:
(3)lru: 占 24 個比特位,記錄該對象最后一次被訪問的時間。千萬別以為這只能在LRU淘汰策略中才用,LFU也是復用的個字段。當使用LRU時,它保存的上次讀寫的24位unix時間戳(秒級);使用LFU時,24位會被分為兩個部分,16位的分鐘級時間戳和8位特殊計數器,這里就不展開了,詳細可以注意我后續的文章。
(4)refcount: 對象的引用計數,類似于shared_ptr 智能指針的引用計數,當refcount為0時,釋放該對象。
(5)ptr: 指向對象具體的底層實現的數據結構。
三、不同對象編碼規則
四、redisObject結構各字段使用范例
Redis中操作key的命令大致可以分為兩類:一種是可以操作任何類型的key,如:del type object等等,另外一種是針對特定類型的key只能使用特定的命令。如:LLEN只能用來獲取列表對象的長度。
4.1 類型檢查(type字段)
比如對于LLEN命令,Redis服務器在執行命令之前會先檢查輸入的key對應的的value對象是否為列表類型,即檢查該value對象的type類型是不是OBJ_LIST,如果是才會執行LLEN命令。否則就拒絕執行命令并返回操作類型錯誤。
4.2 多態命令的實現(encoding)
Redis除了會根據value對象的類型來判斷對應key能否執行執行命令外,還會根據value對象的**編碼方式(encoding字段)**選擇正確的方式來執行命令。比如:列表對象的編碼方式有quicklist 和 ziplist兩種,Redis服務器除了判斷對應value對象的類型為列表對象還要根據具體的編碼選擇正確的LLEN執行。
借用面向對象的術語來說,可以認為LLEN命令是多態的。只要執行LLEN命令的列表鍵,無論value對象的編碼是哪種方式,LLEN命令都可以正常執行。實際上del type 等也是多態命令。他們和LLEN的區別在于,前者是基于類型的多態,后者是基于編碼的多態。
4.3 內存回收和共享對象(refcount)
C語言不具備自動回收功能,Redis就通過引用計數實現了自己的內存回收機制。具體是由redisObject結構中的refcount字段記錄。對象的引用計數會隨著對象的使用狀態而不斷變化。
創建一個新對象時,refcount會被初始化為1,;當對象被另一個新程序使用時 refcount加1;不被一個程序使用時減1;當refcount==0時,該對象所占的空間會被回收。
引用計數除了被用來實現內存回收外,還被用來實現對象共享。比如:
上面我們創建可一個value為100的key A,并使用object refcount來查看key A的引用計數,會看到此時的refcount為2,這是為什么呢?此時有兩個地方引用了這個value對象,一個是持有該對象的服務器程序,另外一個是共享該value對象的key A。如果,我們再創建一個value為100 的 key B,那么鍵B也會指向這個value對象,使得該對象的引用計數變為3。由此,可以看到,共享value對象的鍵越多,節約的內存就越多。
在創建鍵B的時候,服務器考到鍵B要創建的對象是int編碼的字符串對象100,而剛好有個value為100的共享對象匹配,就直接將鍵B指向該共享對象。因為是整數的字符串對象,直接比較即可,如果共享對象是字符串值的對象,要從頭到尾比較每個字符,時間復雜度O(n)。
簡單來說就是,要能使用共享對象,需要先驗證該共享對象和要創建的目標對象是不是完全一致,如果共享對象保存的值越復雜,消耗的CPU也就越多,所以Redis值對整數類型的字符串對象做了共享。沒有共享保存字符串值的字符串對象。
Redis在初始化服務器是,就創建了一萬個字符串對象,這些對象包含了0~9999的所有整數值。當你創建了這個范圍內的 字符串對象時,服務器就會使用這些共享對象,而不是創建新對象,以節約內存。
4.4 對象的空轉時長(lru)
redisObject結構中的lru字段保存,該對象最后一次被訪問的時間。 使用object idletime 來查看,注意這個命令不會修改對象的lru屬性。
當Redis開啟最大內存限制,一般為機器內存的一半,如果redis使用的內存達到這個值,且內存淘汰策略使用的是volatile-lru 或者 allkeys-lru,空轉時長較高的那些鍵會被優先釋放。
使用object idletime 查看鍵的空間時間,單位:秒:
127.0.0.1:6379[1]> keys *
1) "msg"
2) "teacher"
127.0.0.1:6379[1]> object idletime msg
(integer) 71166
127.0.0.1:6379[1]>
五、對象在源碼中的使用
5.1 字符串對象
5.1.1字符串對象創建
// code location: src/object.c
#define OBJ_ENCODING_EMBSTR_SIZE_LIMIT 44
// 創建 strintg 對象
robj *createStringObject(const char *ptr, size_t len) {
// 如果待保存的字符串的長度小于等于44,使用 embstr 編碼
if (len <= OBJ_ENCODING_EMBSTR_SIZE_LIMIT)
return createEmbeddedStringObject(ptr,len);
else // 否則使用 raw 編碼
return createRawStringObject(ptr,len);
}
robj *createEmbeddedStringObject(const char *ptr, size_t len) {
// 申請 robj + sdshdr + data + 1 的空間
robj *o = zmalloc(sizeof(robj)+sizeof(struct sdshdr8)+len+1);
struct sdshdr8 *sh = (void*)(o+1);
o->type = OBJ_STRING; // 設置類型
o->encoding = OBJ_ENCODING_EMBSTR; // 設置編碼
o->ptr = sh+1;
o->refcount = 1;
if (server.maxmemory_policy & MAXMEMORY_FLAG_LFU) {
o->lru = (LFUGetTimeInMinutes()<<8) | LFU_INIT_VAL;
} else {
o->lru = LRU_CLOCK();
}
sh->len = len;
sh->alloc = len;
sh->flags = SDS_TYPE_8;
if (ptr == SDS_NOINIT)
sh->buf[len] = '\0';
else if (ptr) {
memcpy(sh->buf,ptr,len);
sh->buf[len] = '\0';
} else {
memset(sh->buf,0,len+1);
}
return o;
}
從 createEmbeddedStringObject 函數可以看到,該對象是robj和sds的結合體,將sds直接放入到robj里,這也是嵌入式編碼embstr的由來。
為什么要限制44字節呢?因為robj結構體占16個字節,sdshdr結構體占3個字節,最后結尾的‘\0’占一個字節,限制44個字節,就能保證64個字節里能放下所有內容(16+3+1+44 = 64)。
5.1.2 字符串對象編碼
Redis將節省內存做到了極致,它的作者對字符串對象又做了特殊的編碼處理,以進一步達到節省空間的目的。編碼處理過程及代碼注釋如下:
/* Try to encode a string object in order to save space */
robj *tryObjectEncoding(robj *o) {
long value;
sds s = o->ptr;
size_t len;
/* Make sure this is a string object, the only type we encode
* in this function. Other types use encoded memory efficient
* representations but are handled by the commands implementing
* the type. */
// 這里只對string對象進行編碼,其他類型的編碼都有對應的具體實現處理
serverAssertWithInfo(NULL,o,o->type == OBJ_STRING);
/* We try some specialized encoding only for objects that are
* RAW or EMBSTR encoded, in other words objects that are still
* in represented by an actually array of chars. */
// 非sds string對象,直接返回原對象
if (!sdsEncodedObject(o)) return o;
/* It's not safe to encode shared objects: shared objects can be shared
* everywhere in the "object space" of Redis and may end in places where
* they are not handled. We handle them only as values in the keyspace. */
// 如果該對象由其他對象共享,不能編碼,如果編碼可能影響到其他對象的使用
if (o->refcount > 1) return o;
/* Check if we can represent this string as a long integer.
* Note that we are sure that a string larger than 20 chars is not
* representable as a 32 nor 64 bit integer. */
// 檢查能否把一個字符串表示為長整型數,長度要小于等于20
len = sdslen(s);
if (len <= 20 && string2l(s,len,&value)) {
/* This object is encodable as a long. Try to use a shared object.
* Note that we avoid using shared integers when maxmemory is used
* because every object needs to have a private LRU field for the LRU
* algorithm to work well. */
// 如果可以被編碼為long型,且編碼后的值小于OBJ_SHARED_INTEGERS(10000),且未配
// 置LRU替換淘汰策略, 就使用這個數的共享對象,相當于所有小于10000的數都是用的同一個robj
if ((server.maxmemory == 0 ||
!(server.maxmemory_policy & MAXMEMORY_FLAG_NO_SHARED_INTEGERS)) &&
value >= 0 &&
value < OBJ_SHARED_INTEGERS)
{
decrRefCount(o);
incrRefCount(shared.integers[value]);
return shared.integers[value];
} else {
if (o->encoding == OBJ_ENCODING_RAW) {
sdsfree(o->ptr);
o->encoding = OBJ_ENCODING_INT;
o->ptr = (void*) value;
return o;
} else if (o->encoding == OBJ_ENCODING_EMBSTR) {
decrRefCount(o);
return createStringObjectFromLongLongForValue(value);
}
}
}
// 不能轉為long的字符串
/* If the string is small and is still RAW encoded,
* try the EMBSTR encoding which is more efficient.
* In this representation the object and the SDS string are allocated
* in the same chunk of memory to save space and cache misses. */
// 如果字符串的長度太小,小于等于44
if (len <= OBJ_ENCODING_EMBSTR_SIZE_LIMIT) {
robj *emb;
// 如果當前編碼是embstr,直接返回原對象,否則轉為embstr編碼,返回
if (o->encoding == OBJ_ENCODING_EMBSTR) return o;
emb = createEmbeddedStringObject(s,sdslen(s));
decrRefCount(o);
return emb;
}
/* We can't encode the object...
*
* Do the last try, and at least optimize the SDS string inside
* the string object to require little space, in case there
* is more than 10% of free space at the end of the SDS string.
*
* We do that only for relatively large strings as this branch
* is only entered if the length of the string is greater than
* OBJ_ENCODING_EMBSTR_SIZE_LIMIT. */
// 如果前面編碼沒有成功,這里做最后一步,當編碼類型為raw,且它的sds可用空間超過10%,
// 嘗試釋放調這部分內存
trimStringObjectIfNeeded(o);
/* Return the original object. */
// 返回原對象
return o;
}
5.1.3 字符串對象解碼
有編碼就有解碼,實際上只需要那些可以轉為整型類型的字符串傳進行解碼,解碼代碼及注釋如下:
robj *getDecodedObject(robj *o) {
robj *dec;
// 如果編碼是 embstr 和 raw ,只是把引用計數加一,然后返回原對象
if (sdsEncodedObject(o)) {
incrRefCount(o);
return o;
}
// 如果編碼是 int 進行解碼,返回新的對象
if (o->type == OBJ_STRING && o->encoding == OBJ_ENCODING_INT) {
char buf[32];
ll2string(buf,32,(long)o->ptr);
dec = createStringObject(buf,strlen(buf));
return dec;
} else {
serverPanic("Unknown encoding type");
}
}
5.1.4 redis對象引用計數及自動清理
void incrRefCount(robj *o) {
if (o->refcount < OBJ_FIRST_SPECIAL_REFCOUNT) {
o->refcount++; // 引用計數加一
} else {
if (o->refcount == OBJ_SHARED_REFCOUNT) {
/* Nothing to do: this refcount is immutable. */
} else if (o->refcount == OBJ_STATIC_REFCOUNT) {
serverPanic("You tried to retain an object allocated in the stack");
}
}
}
// 減少引用計數
void decrRefCount(robj *o) {
// 釋放空間
if (o->refcount == 1) {
switch(o->type) {
case OBJ_STRING: freeStringObject(o); break;
case OBJ_LIST: freeListObject(o); break;
case OBJ_SET: freeSetObject(o); break;
case OBJ_ZSET: freeZsetObject(o); break;
case OBJ_HASH: freeHashObject(o); break;
case OBJ_MODULE: freeModuleObject(o); break;
case OBJ_STREAM: freeStreamObject(o); break;
default: serverPanic("Unknown object type"); break;
}
zfree(o);
} else {
if (o->refcount <= 0) serverPanic("decrRefCount against refcount <= 0");
if (o->refcount != OBJ_SHARED_REFCOUNT) o->refcount--; // 計數減一
}
}
六、總結
- redis對象為所有類型的value提供了統一的封裝
- 為對象的淘汰策略保存相關信息
- 實現引用計數及內存自動釋放功能
原文鏈接:https://blog.csdn.net/weixin_46935110/article/details/127983569
相關推薦
- 2023-01-21 VmWare安裝Centos后配置Net網絡SSH鏈接問題及解決_VMware
- 2022-07-22 Math.ceil() 函數使用介紹
- 2022-07-12 hadoop 在jps中找不到namenode的原因
- 2022-05-06 Linq中ToList()和CopyToDataTable()用法詳解_實用技巧
- 2022-07-01 Go官方限流器的用法詳解_Golang
- 2022-07-08 python中的annotate函數使用_python
- 2022-11-02 react組件中過渡動畫的問題解決_React
- 2022-03-16 C++中的Lambda函數詳解_C 語言
- 最近更新
-
- window11 系統安裝 yarn
- 超詳細win安裝深度學習環境2025年最新版(
- Linux 中運行的top命令 怎么退出?
- MySQL 中decimal 的用法? 存儲小
- get 、set 、toString 方法的使
- @Resource和 @Autowired注解
- Java基礎操作-- 運算符,流程控制 Flo
- 1. Int 和Integer 的區別,Jav
- spring @retryable不生效的一種
- Spring Security之認證信息的處理
- Spring Security之認證過濾器
- Spring Security概述快速入門
- Spring Security之配置體系
- 【SpringBoot】SpringCache
- Spring Security之基于方法配置權
- redisson分布式鎖中waittime的設
- maven:解決release錯誤:Artif
- restTemplate使用總結
- Spring Security之安全異常處理
- MybatisPlus優雅實現加密?
- Spring ioc容器與Bean的生命周期。
- 【探索SpringCloud】服務發現-Nac
- Spring Security之基于HttpR
- Redis 底層數據結構-簡單動態字符串(SD
- arthas操作spring被代理目標對象命令
- Spring中的單例模式應用詳解
- 聊聊消息隊列,發送消息的4種方式
- bootspring第三方資源配置管理
- GIT同步修改后的遠程分支