Redis五大基础数据类型
redis 基础数据类型有5种: string、set、zset、list和hash。
String数据类型
string类型是redis中最常见的数据类型,其内部存在三种编码:
EMBSTR
、RAW
和INT
编码。
RAW编码
- 基本编码方式是
RAW
,基于简单动态字符串(SDS)实现,存储上限为512mb。
下方是sds其中一种数据结构。详情可去看笔者的另一篇文章:Redis数据结构系列一:动态字符串SDS
在上图中的数据,总字节数len=50,可用flags=sdshdr8编码格式保存。struct __attribute__ ((__packed__)) sdshdr8 { uint8_t len; /* used:已保存的字符串字节数,不包括结束符。表示字节数最大长度8个bit,也就是2^8 -1 */ uint8_t alloc; /* buf申请的总字节数,不包括结束符*/ unsigned char flags; /* 不同SDS 头类型(sdshdr5|sdshdr8|sdshdr16|sdshdr32|sdshdr64), 用来控制SDS 头的大小*/ char buf[]; };
EMBSTR编码
-
如果存储的SDS长度小于44字节,则会采用
EMBSTR
编码,此时RedisObject header与SDS是一段连续空间。申请内存时只需要调用一次内存分配函数,效率更高。为什么RedisObject header和sds会变成连续空间?
-
由于上图中的数据,总字节数len=44,可用flags=sdshdr8编码格式保存。
-
sds 占用的总字节数totalSdsBytes = len:1 + alloc:1 + flags:1 + 44 + 1(结束标识) =
48byte
; -
RedisObject header固定为16byte, RedisObject header + totalSdsBytes =
16 + 48 = 64byte
,正好是8k,即一个Redis内存页,没有内存碎片。同时只需申请一次内存,减少用户态和内核态的交互,提交效率。若总字节数len>44byte,会导致整个sds>48byte,就会转成RAW
编码。
-
INT编码
-
如果string类型保存的是
LONG_MAX(2^32)
范围内的数字,即采用INT编码
。 -
若采用的是
INT 编码
,则使用*ptr
指针存储二进制数据(*ptr占用8byte,64bit,足以存储long_max的2^32大小的数据),去除sds结构,更加节省内存空间。其数据结构如下:
redis实践如下:
String总结:
-
使用string类型存储数据时,尽量不超过44byte,即内部采用embstr编码;
-
使用string类型存储数据时,尽量将数据转为数字,即内部采用int编码;
List数据类型
List类型可以从首、尾操作列表中的元素
有以下类型满足List数据类型的特征:
-
LinkedList:普通链表,可以从双端访问,但内存占用较高、内存碎片较多,不是连续内存
-
ZipList:压缩链表,可以从双端访问,连续内存,内存占用低,但是存储上限低
-
QuickList:等于LinkedList+QuickList,可以从双端访问,内存占用低,包含多个ZipList,存储上限高
所以:Redis 3.2版本之后,都采用QuickList作为List数据类型的底层结构。 结构如下:
List源码分析
由lpushxCommand
命令,进入查看发现所有的操作命令底层都是调用的pushGenericCommand
void lpushxCommand(client *c) {
pushGenericCommand(c,LIST_HEAD,1);
}
进入pushGenericCommand命令:
/**
* client *c: 客户端连接redis服务器时,会将客户端的信息及输入的命令都封装成client对象
* where:插入的位置,head|tail
* xx:当xx=true时,代表key存在时,才push;当xx=false时,不存在则直接return。 默认为false
*/
void pushGenericCommand(client *c, int where, int xx) {
int j;
/**
* lpush key1 v1 v2 v3;
* argv[0] = lpush
* argv[1] = key1
* argv[2] = v1
* ...
*/
// 1.判断输入的参数个数是否超出上限
for (j = 2; j < c->argc; j++) {
// 因为redis所有的键和值都会封装成RedisObject,其真实数据实际上是*ptr指向的地址,所以执行sdslen(c->argv[j]->ptr)获取数据长度
if (sdslen(c->argv[j]->ptr) > LIST_MAX_ITEM_SIZE) {
addReplyError(c, "Element too large");
return;
}
}
// 2.根据client中的db和key查询是否存在的RedisObject(robj是RedisObject的缩写)
robj *lobj = lookupKeyWrite(c->db, c->argv[1]);
// 3.判断lobj类型是否为LIST类型,若不是则直接返回
if (checkType(c,lobj,OBJ_LIST)) return;
if (!lobj) {
// 4.1如果lobj不存在,且xx=true,代表key不存在则直接return,不执行push
if (xx) {
addReply(c, shared.czero);
return;
}
// 4.2如果lobj不存在,且xx=false,代表需要创建新的List RedisObject
// 在此处可见实际上创建的是 Quicklist
lobj = createQuicklistObject();
// 4.2.1 设置lobj中quicklist的属性
// list_max_ziplist_size: 限制ziplist的最大长度
// list_compress_depth: qucikList 头尾节点不压缩数量
quicklistSetOptions(lobj->ptr, server.list_max_ziplist_size,
server.list_compress_depth);
// 4.3 将创建的robj赋值给key
dbAdd(c->db,c->argv[1],lobj);
}
for (j = 2; j < c->argc; j++) {
// 5.依次将数据保存进list orbj
listTypePush(lobj,c->argv[j],where);
server.dirty++;
}
... // 略
}
上述代码都已添加注释,可看出List数据类型最终创建的还是quicklist结构。
Set数据类型
set数据类型的特点:
-
不保证数据有序
-
保证元素唯一(可以判断元素是否存在。添加
SADD
、判断是否存在SISMEMBER
和差集SINTER
命令中**都需要判断元素是否存在 **) -
便于求交集、并集和差集
由于底层数据结构中快速查找和唯一仅有skipList和dict数据结构,而skipList中是由数值score来做排序,而set中不一定全是数值,所以只能选用dict数据结构。
Set编码
-
为了查询效率和唯一性,set采用HT编码(Dict)。dict的key用来存储元素,value统一为null。
-
若set保存的全是数值类型,且不超过
set-max-intset-entries
时,Set会采用IntSet编码,节省内存。
robj *setTypeCreate(sds value) {
// 若存储的数据都是整数,创建Inset编码的robj
if (isSdsRepresentableAsLongLong(value,NULL) == C_OK)
return createIntsetObject();
// 若存储的数据不全是整数,创建hash-table(dict类型)编码的robj
return createSetObject();
}
robj *createIntsetObject(void) {
intset *is = intsetNew();
robj *o = createObject(OBJ_SET,is);
o->encoding = OBJ_ENCODING_INTSET;
return o;
}
// 默认创建OBJ_ENCODING_HT编码的set,即dict类型
robj *createSetObject(void) {
dict *d = dictCreate(&setDictType,NULL);
robj *o = createObject(OBJ_SET,d);
o->encoding = OBJ_ENCODING_HT;
return o;
}
/** 向set添加元素,若元素存在直接返回;若不存在则在相应编码的结构中添加*/
int setTypeAdd(robj *subject, sds value) {
long long llval;
// 若是dict类型
if (subject->encoding == OBJ_ENCODING_HT) {
dict *ht = subject->ptr;
// 直接添加元素到新的dictEntry中
dictEntry *de = dictAddRaw(ht,value,NULL);
if (de) {
dictSetKey(ht,de,sdsdup(value));
dictSetVal(ht,de,NULL);
return 1;
}
} else if (subject->encoding == OBJ_ENCODING_INTSET) {
// 若是intset类型
// 先判断value是否是整数
if (isSdsRepresentableAsLongLong(value,&llval) == C_OK) {
uint8_t success = 0;
// 若是整数,使用intsetAdd添加到set
subject->ptr = intsetAdd(subject->ptr,llval,&success);
if (success) {
// 若添加成功,最后还需要判断是否大于set_max_intset_entries
size_t max_entries = server.set_max_intset_entries;
if (max_entries >= 1<<30) max_entries = 1<<30;
// 若intset的entry数量>set_max_intset_entries,则转换编码为HT
if (intsetLen(subject->ptr) > max_entries)
setTypeConvert(subject,OBJ_ENCODING_HT);
return 1;
}
} else {
// 若不是整数,则转换编码为HT
setTypeConvert(subject,OBJ_ENCODING_HT);
// 使用dictAdd添加元素到set
serverAssert(dictAdd(subject->ptr,sdsdup(value),NULL) == DICT_OK);
return 1;
}
} else {
serverPanic("Unknown set encoding");
}
return 0;
}
-
Set中保存的**{5, 10, 20}**,结构如下:
-
Set中保存的**{5, 10, 20, m1}**,结构如下:
ZSet数据类型
ZSet数据类型的特点:zset也就是sortedSet,其中每一个元素都需要指定一个member和score:
-
可以根据score排序
-
member必须唯一
-
可以根据member查询分数
ZSet底层数据结构必须满足:键值存储、键必须唯一、可排序和能根据key找到value这几个需求。(此时·key=member,score=value·
)
-
SkipList:可排序,可同时存储score和element值(member)。(1.不能做到key唯一;2.不能做到通过key查找value)
-
HT(Dict):可以存储键值,可以根据key找到value。(1.不能通过score快速定位range;2.不能排序)
由于无法使用一种数据结构完成zset类型的封装,所以redis采用dict +skiplist 类型一起作为zset的底层数据结构。
其结构代码如下:
typedef struct zset {
dict *dict;
zskiplist *zsl;
} zset;
robj *createZsetObject(void) {
zset *zs = zmalloc(sizeof(*zs));// 初始化zset内存
robj *o;
zs->dict = dictCreate(&zsetDictType,NULL);//创建dict
zs->zsl = zslCreate();// 创建skiplist
o = createObject(OBJ_ZSET,zs);// 创建zset的robj
o->encoding = OBJ_ENCODING_SKIPLIST;// 可看出,zset虽然时候dict+skiplist组成,但使用的编码是skipList类型
return o;
}
上述的结构图如下:
当元素数量不多时,HT和SkipList的优势不明显,且更耗内存。因此Redis还会采用ZIpList结构作为zset的第二种底层结构。不过需要同时满足以下两个条件:
-
元素数量<zset_max_ziplist_entries,默认值128
-
每个元素都<zset_max_ziplist_value字节,默认值64
/** t_zset.c zaddGenericCommand */
// zadd添加元素时,现根据key查找zset对应的robj,没有则创建
zobj = lookupKeyWrite(c->db,key);
if (checkType(c,zobj,OBJ_ZSET)) goto cleanup;
if (zobj == NULL) {
if (xx) goto reply_to_client; /* No key + XX option: nothing to do. */
// 若不存在,且zset_max_ziplist_entries==0,则代表使用HT+SkipList
// 若不存在,且zset_max_ziplist_value(字节大小)<value大小,则代表使用HT+SkipList
if (server.zset_max_ziplist_entries == 0 ||
server.zset_max_ziplist_value < sdslen(c->argv[scoreidx+1]->ptr)){
zobj = createZsetObject();
} else {
zobj = createZsetZiplistObject();
}
dbAdd(c->db,key,zobj);
}
ZipList本身没有排序功能,而且没有键值对的概念,因此需要有zset通过逻辑编码实现相应功能。
-
ZipList是连续内存,因此score和element是紧挨在一起的两个entry,element在前,score在后
-
Score越小越接近队首,score越大越接近队尾,按照score值升序排列
内存结构图如下:
Hash数据类型
Hash数据类型的特点:——和zset数据类型相似
-
都是兼职存储
-
都需求根据键查询值
-
都需求键唯一
-
不同点:hash无序;zset要求有序
综上:Redis将使用类似Zset的底层结构作为hash的结构。Zset使用HT+SkipList / ZipList
底层数据结构,hash无需排序,所以无需使用SkipList,则最终为HT / ZipList
底层数据结构。
-
Hash底层默认使用的ZipList编码,用以节省内存。ZipList中相邻的两个entry分别存储key和value
-
当数据量较大时,hash结构会转为HT编码,也就是Dict。触发条件两个如下:
-
ZipList中元素超过hash-max-ziplist-entries(默认512)
-
ZipList中任一entry大小超过hash-max-ziplist-value(默认64byte)
-
ZipList编码 内存结构如下:
HT编码 内存结构如下:
通过hsetCommand # hashTypeLookupWriteOrCreate
逻辑中,可看出hash 默认创建的是ZIPLIST编码。
void hsetCommand(client *c) {
int i, created = 0;
robj *o;
/**
* hmset key filed value ...
* hmset user name jack age 10
* argv[0] = hset
* argv[1] = user
* argv[2] = name
* argv[3] = jack
* ...
*/
// 通过key查找对应的hash robj,若不存在则创建
if ((o = hashTypeLookupWriteOrCreate(c,c->argv[1])) == NULL) return;
// 判断是否要转换类型,由ziplist -> ht
hashTypeTryConversion(o,c->argv,2,c->argc-1);
for (i = 2; i < c->argc; i += 2)
//将file 和value 依次添加到dict
created += !hashTypeSet(o,c->argv[i]->ptr,c->argv[i+1]->ptr,HASH_SET_COPY);
}
robj *hashTypeLookupWriteOrCreate(client *c, robj *key) {
robj *o = lookupKeyWrite(c->db,key);
if (checkType(c,o,OBJ_HASH)) return NULL;
if (o == NULL) {
// 若robj为null,则创建
o = createHashObject();
dbAdd(c->db,key,o);
}
return o;
}
//默认创建的是ZIPLIST编码,那么插入逻辑中将会涉及到由ZIPLIST编码-> 转为 HT编码的过程
robj *createHashObject(void) {
unsigned char *zl = ziplistNew();
robj *o = createObject(OBJ_HASH, zl);
o->encoding = OBJ_ENCODING_ZIPLIST;
return o;
}
再看尝试转化HT编码的逻辑,通过下述代码可明显看出只判断的ZIPLIST的value大小
,对于长度的判断
并没有体现。
void hashTypeTryConversion(robj *o, robj **argv, int start, int end) {
int i; size_t sum = 0;
// 由于默认是ZIPLIST编码,若目前不是ZIPLIST编码,则代表已经是HT编码
if (o->encoding != OBJ_ENCODING_ZIPLIST) return;
for (i = start; i <= end; i++) {
// 判断是否是key和value是不是sds,redis中数字和字符串都为sds
if (!sdsEncodedObject(argv[i])) continue;
size_t len = sdslen(argv[i]->ptr);
// 判断key和value的字节长度是否>hash_max_ziplist_value(默认64byte)
// 若大于,则需要转为HT编码
if (len > server.hash_max_ziplist_value) {
hashTypeConvert(o, OBJ_ENCODING_HT);
return;
}
sum += len;
}
// 判断所有的key和value的总字节数,总字节数>1G时,则需要转为HT编码
// Don't let ziplists grow over 1GB in any case
if (!ziplistSafeToAdd(o->ptr, sum)) hashTypeConvert(o, OBJ_ENCODING_HT);
}
紧着看hashTypeSet
方法,该方法是具体插入hash数据的方法。
int hashTypeSet(robj *o, sds field, sds value, int flags) {
int update = 0;// 0:新增;1:更新
// 判断是否是ZIPLIST编码
if (o->encoding == OBJ_ENCODING_ZIPLIST) {
unsigned char *zl, *fptr, *vptr;
zl = o->ptr;
// 若是ZIPLIST编码,则查找头节点
fptr = ziplistIndex(zl, ZIPLIST_HEAD);
// 若头节点为空,则直接新增
if (fptr != NULL) {
//若头节点不为空,通过field查找entry位置
fptr = ziplistFind(zl, fptr, (unsigned char*)field, sdslen(field), 1);
if (fptr != NULL) { // 若查询到对应的entry
// 计算得出filed对应value的位置
vptr = ziplistNext(zl, fptr);
serverAssert(vptr != NULL);
// 将update=1,代表更新
update = 1;
// 更新filed对应的value
zl = ziplistReplace(zl, vptr, (unsigned char*)value,
sdslen(value));
}
}
// update=0,代表新增,依次新增filed 和 value
if (!update) {
/* Push new field/value pair onto the tail of the ziplist */
zl = ziplistPush(zl, (unsigned char*)field, sdslen(field),
ZIPLIST_TAIL);
zl = ziplistPush(zl, (unsigned char*)value, sdslen(value),
ZIPLIST_TAIL);
}
o->ptr = zl;
// 判断ziplist中entry的长度是否>hash_max_ziplist_entries(默认512)
// 若大于,则需要转为HT编码
if (hashTypeLength(o) > server.hash_max_ziplist_entries)
hashTypeConvert(o, OBJ_ENCODING_HT);
} else if (o->encoding == OBJ_ENCODING_HT) {
// ht编码直接插入或覆盖
...
} else {
serverPanic("Unknown hash encoding");
}
...
}
最终发现,需转换编码逻辑中对长度的判断
再具体插入的时候处理。仔细一想,就应该放在这个位置,因为hash是保证filed唯一
,所以hset
数据时可能时插入或覆盖,覆盖的话就没有长度的增加了。