当前位置:网站首页>从底层看 Redis 的五种数据类型
从底层看 Redis 的五种数据类型
2022-08-04 08:02:00 【星辰与晨曦】
上一篇:===》 初始Redis以及简单了解Redis当中的数据类型
String
String是 Redis 中最为常见的数据存储类型
其基本编码方式是RAW,基于简单动态字符串(SDS)实现,存储上限为512mb。
如果存储的SDS长度小于44字节,则会采用EMBSTR编码,此时object head与SDS是一段连续空间。申请内存时只需要调用一次内存分配函数,效率更高。
如果存储的字符串是整数值,并且大小在LONG_MAX范围内,则会采用INT编码:直接将数据保存在RedisObject的ptr指针位置(刚好8字节),不再需要SDS了。
底层逻辑图:
在Redis命令窗口中对其进行验证
List
就和 Java 中的集合一样的。
当然在 Redsis 中是要做一些大量优化的。
我们已知:
- LinkedList :普通链表,可以从双端访问,内存占用较高,内存碎片较多
- ZipList :压缩列表,可以从双端访问,内存占用低,存储上限。
所以就在这结合这两个特点形成了QuickList。
QuickList:LinkedList + ZipList,可以从双端访问,内存占用较低,包含多个ZipList,存储上限高。
Redis的List结构类似一个双端链表,可以从首、尾操作列表中的元素:
- 在3.2版本之前,Redis 采用 ZipList 和 LinkedList 来实现List,当元素数量小于512并且元素大小小于64字节时采用ZipList编码,超过则采用LinkedList编码。
- 在3.2版本之后,Redis统一采用 QuickList 来实现List:
它的底层如下:
Set
Set是Redis中的单列集合,满足下列特点:
- 不保证有序性
- 保证元素唯一(可以判断元素是否存在)
Set是Redis中的集合,不一定确保元素有序,可以满足元素唯一、查询效率要求极高。
为了查询效率和唯一性,SET 采用 HT 编码(Dict)。Dict 中的 key 用来存储元素,value 统一为null。
当存储的所有数据都是整数,并且元素数量不超过 set-max-intset-entries 时,Set 会采用 IntSet 编码,以节省内存空间。
源码如下:
robj *setTypeCreate(sds value) {
// 判断value是否是数值类型 long long
if (isSdsRepresentableAsLongLong(value,NULL) == C_OK)
// 如果是数值类型,则采用IntSet编码
return createIntsetObject();
// 否则采用默认编码,也就是HT
return createSetObject();
}
robj *createIntsetObject(void) {
// 初始化INTSET并申请内存空间
intset *is = intsetNew();
// 创建RedisObject
robj *o = createObject(OBJ_SET,is);
// 指定编码为INTSET
o->encoding = OBJ_ENCODING_INTSET;
return o;
}
robj *createSetObject(void) {
// 初始化Dict类型,并申请内存
dict *d = dictCreate(&setDictType,NULL);
// 创建RedisObject
robj *o = createObject(OBJ_SET,d);
// 设置encoding为HT
o->encoding = OBJ_ENCODING_HT;
}
在Set中由intset编码方式向ht编码转变的判断语句:
它的底层如下:(底层从intset型转换为ht的过程)
ZSet
ZSet 也就是 SortedSet,其中每一个元素都需要指定一个 score 值和 member 值:
● 可以根据score值排序
● member必须唯一
● 可以根据member查询分数
因此,zset底层数据结构必须满足键值存储、键必须唯一、可排序这几个需求。
- SkipList:可以排序,并且可以同时存储score和ele值(member)
- HT(Dict):可以键值存储,并且可以根据 key 找 value。
所以在ZSet 当中就是将这两者结合而出的(全都要)。
它的底层如下:
可以明显的看到这就是一个用空间换时间的做法,在底层是将数据保存了两份的。
当元素数量不多时,HT和SkipList的优势不明显,而且更耗内存。因此zset还会采用ZipList结构来节省内存,不过需要同时满足两个条件:
元素数量小于zset_max_ziplist_entries,默认值128
每个元素都小于zset_max_ziplist_value字节,默认值64
所以在这看看它的底层源码:
// zadd添加元素时,先根据key找到zset,不存在则创建新的zset
zobj = lookupKeyWrite(c->db,key);
if (checkType(c,zobj,OBJ_ZSET)) goto cleanup;
// 判断是否存在
if (zobj == NULL) {
// zset不存在
if (server.zset_max_ziplist_entries == 0 ||
server.zset_max_ziplist_value < sdslen(c->argv[scoreidx+1]->ptr))
{
// zset_max_ziplist_entries设置为0就是禁用了ZipList,
// 或者value大小超过了zset_max_ziplist_value,采用HT + SkipList
zobj = createZsetObject();
} else {
// 否则,采用 ZipList
zobj = createZsetZiplistObject();
}
dbAdd(c->db,key,zobj);
}
//采用HT + SkipList
robj *createZsetObject(void) {
// 申请内存
zset *zs = zmalloc(sizeof(*zs));
robj *o;
// 创建Dict
zs->dict = dictCreate(&zsetDictType,NULL);
// 创建SkipList
zs->zsl = zslCreate();
o = createObject(OBJ_ZSET,zs);
o->encoding = OBJ_ENCODING_SKIPLIST;
return o;
}
//采用 ZipList
robj *createZsetZiplistObject(void) {
// 创建ZipList
unsigned char *zl = ziplistNew();
robj *o = createObject(OBJ_ZSET,zl);
o->encoding = OBJ_ENCODING_ZIPLIST;
return o;
}
每次在新增元素时的判断。
int zsetAdd(robj *zobj, double score, sds ele, int in_flags, int *out_flags, double *newscore) {
/* 判断编码方式*/
if (zobj->encoding == OBJ_ENCODING_ZIPLIST) {
// 是ZipList编码
unsigned char *eptr;
// 判断当前元素是否已经存在,已经存在则更新score即可 if ((eptr = zzlFind(zobj->ptr,ele,&curscore)) != NULL) {
//...略
return 1;
} else if (!xx) {
// 元素不存在,需要新增,则判断ziplist长度有没有超、元素的大小有没有超
if (zzlLength(zobj->ptr)+1 > server.zset_max_ziplist_entries
|| sdslen(ele) > server.zset_max_ziplist_value
|| !ziplistSafeToAdd(zobj->ptr, sdslen(ele)))
{
// 如果超出,则需要转为SkipList编码
zsetConvert(zobj,OBJ_ENCODING_SKIPLIST);
} else {
zobj->ptr = zzlInsert(zobj->ptr,ele,score);
if (newscore) *newscore = score;
*out_flags |= ZADD_OUT_ADDED;
return 1;
}
} else {
*out_flags |= ZADD_OUT_NOP;
return 1;
}
} // 本身就是SKIPLIST编码,无需转换
if (zobj->encoding == OBJ_ENCODING_SKIPLIST) {
// ...略
} else {
serverPanic("Unknown sorted set encoding");
}
return 0; /* Never reached. */
}
注意:
ziplist 本身是没有排序功能,而且没有键值对的概念,因此需要有zset通过编码实现:
ZipList是连续内存,因此score和element是紧挨在一起的两个entry, element 在前,score 在后
score越小越接近队首,score越大越接近队尾,按照score值升序排列。

Hash
Hash结构与Redis中的Zset非常类似:
● 都是键值存储;
● 都需求根据键获取值;
● 键必须是唯一的。
区别如下:
- zset的键是member,值是score;hash的键和值都是任意值
- zset要根据score排序;hash则无需排序
因此,Hash 底层采用的编码与 Zset 也基本一致,只需要把排序有关的 SkipList 去掉即可
- Hash结构默认采用ZipList编码,用以节省内存。 ZipList中相邻的两个entry 分别保存 field 和 value
- 当数据量较大时,Hash结构会转为HT编码,也就是Dict,触发条件有两个:
ZipList中的元素数量超过了hash-max-ziplist-entries(默认512)
ZipList中的任意entry大小超过了hash-max-ziplist-value(默认64字节)
底层结构:
看一下底层源码:
void hsetCommand(client *c) {
// hset user1 name Jack age 21
int i, created = 0;
robj *o; // 略 ...
// 判断hash的key是否存在,不存在则创建一个新的,默认采用ZipList编码
if ((o = hashTypeLookupWriteOrCreate(c,c->argv[1])) == NULL) return;
// 判断是否需要把ZipList转为Dict
hashTypeTryConversion(o,c->argv,2,c->argc-1);
// 循环遍历每一对field和value,并执行hset命令
for (i = 2; i < c->argc; i += 2)
created += !hashTypeSet(o,c->argv[i]->ptr,c->argv[i+1]->ptr,HASH_SET_COPY); // 略 ...
}
// 判断hash的key是否存在
robj *hashTypeLookupWriteOrCreate(client *c, robj *key) {
// 查找key
robj *o = lookupKeyWrite(c->db,key);
if (checkType(c,o,OBJ_HASH)) return NULL;
// 不存在,则创建新的
if (o == NULL) {
o = createHashObject();
dbAdd(c->db,key,o);
}
return o;
}
robj *createHashObject(void) {
// 默认采用ZipList编码,申请ZipList内存空间
unsigned char *zl = ziplistNew();
robj *o = createObject(OBJ_HASH, zl);
// 设置编码
o->encoding = OBJ_ENCODING_ZIPLIST;
return o;
}
// 判断是否需要把ZipList转为Dict
void hashTypeTryConversion(robj *o, robj **argv, int start, int end) {
int i;
size_t sum = 0;
// 本来就不是ZipList编码,什么都不用做了
if (o->encoding != OBJ_ENCODING_ZIPLIST) return;
// 依次遍历命令中的field、value参数
for (i = start; i <= end; i++) {
if (!sdsEncodedObject(argv[i]))
continue;
size_t len = sdslen(argv[i]->ptr);
// 如果field或value超过hash_max_ziplist_value,则转为HT
if (len > server.hash_max_ziplist_value) {
hashTypeConvert(o, OBJ_ENCODING_HT);
return;
}
sum += len;
}// ziplist大小超过1G,也转为HT
if (!ziplistSafeToAdd(o->ptr, sum))
hashTypeConvert(o, OBJ_ENCODING_HT);
}
// 插入元素
int hashTypeSet(robj *o, sds field, sds value, int flags) {
int update = 0;
// 判断是否为ZipList编码
if (o->encoding == OBJ_ENCODING_ZIPLIST) {
unsigned char *zl, *fptr, *vptr;
zl = o->ptr;
// 查询head指针
fptr = ziplistIndex(zl, ZIPLIST_HEAD);
if (fptr != NULL) {
// head不为空,说明ZipList不为空,开始查找key
fptr = ziplistFind(zl, fptr, (unsigned char*)field, sdslen(field), 1);
if (fptr != NULL) {
// 判断是否存在,如果已经存在则更新
update = 1;
zl = ziplistReplace(zl, vptr, (unsigned char*)value,
sdslen(value));
}
}
// 不存在,则直接push
if (!update) {
// 依次push新的field和value到ZipList的尾部
zl = ziplistPush(zl, (unsigned char*)field, sdslen(field),
ZIPLIST_TAIL);
zl = ziplistPush(zl, (unsigned char*)value, sdslen(value),
ZIPLIST_TAIL);
}
o->ptr = zl;
/* 插入了新元素,检查list长度是否超出,超出则转为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");
}
return update;
}
边栏推荐
- 解决:Hbuilder工具点击发行打包,一直报尚未完成社区身份验证,请点击链接xxxxx,项目xxx发布H5失败的错误。
- 卷积神经网络CNN
- 安装GBase 8c数据库集群时,报错误码:80000306,显示Dcs cluster not healthy。怎么处理错误呢?
- MMDetection finetune
- 【字符串】最小表示法
- 沃尔玛、阿里国际该如何做测评自养号?
- Redis分布式锁的应用
- Yolov5更换主干网络之《旷视轻量化卷积神经网络ShuffleNetv2》
- Distributed Computing Experiment 3 PRC-based Book Information Management System
- 24.循环神经网络RNN
猜你喜欢
随机推荐
QT + msvc2017编译器
解决:Hbuilder工具点击发行打包,一直报尚未完成社区身份验证,请点击链接xxxxx,项目xxx发布H5失败的错误。
经典递归回溯问题之——解数独(LeetCode 37)
怎么写专利更容易通过?
【论文笔记】—低照度图像增强—Supervised—RetinexNet—2018-BMVC
Cross-species regulatory sequence activity prediction
powershell和cmd对比
字节跳动岗位薪酬体系曝光,看完我真的酸了...
「PHP基础知识」转换数据类型
C语言指针
华为设备配置VRRP与路由联动监视上行链路
data:image/jpg; base64 format data is converted to image
金仓数据库 KDTS 迁移工具使用指南 (6. 注意事项)
inject() can only be used inside setup() or functional components.
Lightweight Backbone VGNetG Achieves "No Choice, All" Lightweight Backbone Network
第一次用postgreSQL,想装主从,用的12.7 tar.gz版本。安装好后没在 share目录下找到样例配置recovery.conf.sample,是安装方式不对,还是路径不对?
【剑指Offer】二分法例题
经典二分法查找的进阶题目——LeetCode33 搜索旋转排序数组
通过GBase 8c Platform安装数据库集群时报错
使用GBase 8c数据库的时候,遇到这种报错









