Redis核心对象
reids中定义了一个数据结构用来统一表示各种数据类型,它叫做redisObject
typedef struct redisObject {
unsigned type:4; //记录数据值的类型:string、list、hash、set、zset
unsigned encoding:4; //记录数据值的编码格式
unsigned lru:LRU_BITS; //记录操作时间,当redis内存超限时,该值可辅助lru算法清理数据
int refcount; //记录当前对象被引用的次数
void *ptr; //记录存储数据位置的指针
} robj;
Redis数据类型
截止到redis6版本,一共定义了以下七种基本数据类型
#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. */
#define OBJ_MODULE 5 /* Module object. */ 下面这两个数据类型是之前没有的
#define OBJ_STREAM 6 /* Stream object. */
可通过命令type key来查看数据值的所属类型
SDS
要了解redis字符串的编码格式,首先来了解以下SDS
redis使用SDS(“简单动态字符串”)结构体来存储字符串,redis源码中定义了如下几种结构体,根据字符串长度使用不同结构体来存储,尽可能对底层进行优化
/* Note: sdshdr5 is never used, we just access the flags byte directly.
* However is here to document the layout of type 5 SDS strings. */
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结构体相比c语言字符串的好处
- 获取字符长度的复杂度不同,C语言字符串没有记录字符长度,获取字符串长度时必须遍历整个字符串,时间为O(n),而SDS结构体中len字段记录了字符长度,直接读取即可,时间为O(1)
- 杜绝缓冲区溢出,C语言字符串拼接时,处理不当可能会因为分配的内存不够而造成缓冲区溢出,而SDS会根据长度来判断空间是否足够,空间不足时进行空间扩展,所以不会出现缓冲区溢出(注:redis单个key的值不能超过512M)
- 非文本内容存储,C语言字符串的字节数组是以\0结尾,而在处理图片或其他非文本数据时,可能会读取到该字符而进行提前结束,从而导致识别失败,而Redis记录了字符长度,所以不会出现提前结束而失败的情况
- 空间预分配,由于C语言字符串不记录自身长度,所以每个字符串的底层都是N+1个字符长的数组(包含末尾的结束符\0),而每次操作(增长或缩短)字符串时,就需要对这个字符串的数组进行内存重新分配,而Redis通过空间预分配策略,可以减少增长字符串时对SDS字符串内存的重分配次数
- 惰性空间释放,当SDS的字符串缩短时,程序并不立即使用重新分配来回收空出来的字节,而是内存大小及使用字符长度记录下来,等待下次使用
以下是sds结构体和C语言字符串的结构图
Redis编码格式
redis数据值在底层存储的编码格式,对用户是完全透明的
redis针对每种数据类型都设计了多种编码格式进行数据存储,然后根据具体情况更改为更合适的编码格式来进行数据存储,以下是定义的所有编码格式:
#define OBJ_ENCODING_RAW 0 /* Raw representation */
#define OBJ_ENCODING_INT 1 /* Encoded as integer */
#define OBJ_ENCODING_HT 2 /* Encoded as hash table */
#define OBJ_ENCODING_ZIPMAP 3 /* Encoded as zipmap */
#define OBJ_ENCODING_LINKEDLIST 4 /* No longer used: old list encoding. */
#define OBJ_ENCODING_ZIPLIST 5 /* Encoded as ziplist */
#define OBJ_ENCODING_INTSET 6 /* Encoded as intset */
#define OBJ_ENCODING_SKIPLIST 7 /* Encoded as skiplist */
#define OBJ_ENCODING_EMBSTR 8 /* Embedded sds string encoding */
#define OBJ_ENCODING_QUICKLIST 9 /* Encoded as linked list of listpacks */
#define OBJ_ENCODING_STREAM 10 /* Encoded as a radix tree of listpacks */
#define OBJ_ENCODING_LISTPACK 11 /* Encoded as a listpack */
可通过命令object encoding key来查看数据值使用的编码格式
Redis string数据类型-编码格式
redis中string数据类型的值会采用三种编码格式来进行存储
- int格式,当数据值为数字且长度小于20时,就会采用int格式进行存储,此时不需要用到sds结构体,直接将redisObject中ptr字段的值改为该数字(注:当数值为0-10000时,key会指向预先创建好的redisObject,而不需要新建redisObject)
len = sdslen(s); if (len <= 20 && string2l(s,len,&value)) { //长度小于20 if ((server.maxmemory == 0 || !(server.maxmemory_policy & MAXMEMORY_FLAG_NO_SHARED_INTEGERS)) && value >= 0 && value < OBJ_SHARED_INTEGERS) { //当数值为0-10000时会使用预先创建好的共享redisObject对象 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); } } }
- embstr格式,当数据值为数字且长度大于20时,或者数据值为字符串且长度不大于44时,就会采用embstr格式进行存储,此时就需要使用sds结构体(注:该种编码格式是将sds字符串与其对应的redisObject对象分配在同一块连续的内存空间,彷佛sds字符串嵌入在redisObject中一样)
//此是object.c第119行的代码 #define OBJ_ENCODING_EMBSTR_SIZE_LIMIT 44 robj *createStringObject(const char *ptr, size_t len) { if (len <= OBJ_ENCODING_EMBSTR_SIZE_LIMIT) //如果长度小于44则使用embstr编码格式 return createEmbeddedStringObject(ptr,len); else return createRawStringObject(ptr,len); //否则使用raw编码格式 }
//该是object.c第85行的代码 //是创建embstr编码格式字符串的具体代码 robj *createEmbeddedStringObject(const char *ptr, size_t len) { 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; //将sds字符串结构体跟其对应的redisObject放在一起 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; }
类似于这种结构:
- raw格式,当字符串长度大于44时,将会使用raw编码格式,SDS字符串结构体的内存跟其对应的redisObject对象的内存也不一定是连续的了
//该是object.c第78行代码 //是创建raw编码格式字符串的方法 robj *createRawStringObject(const char *ptr, size_t len) { return createObject(OBJ_STRING, sdsnewlen(ptr,len)); }
//该是object.c第42行代码 //为公用的创建对象的代码 robj *createObject(int type, void *ptr) { robj *o = zmalloc(sizeof(*o)); o->type = type; o->encoding = OBJ_ENCODING_RAW; o->ptr = ptr; //这里跟创建embstr时不一样,是直接指向传入的指针 o->refcount = 1; /* Set the LRU to the current lruclock (minutes resolution), or * alternatively the LFU counter. */ if (server.maxmemory_policy & MAXMEMORY_FLAG_LFU) { o->lru = (LFUGetTimeInMinutes()<<8) | LFU_INIT_VAL; } else { o->lru = LRU_CLOCK(); } return o; }