欢迎大家关注 github.com/hsfxuebao ,希望对大家有所帮助,要是觉得可以的话麻烦给点一下Star哈
前面几篇文章,我们陆续介绍了Redis6用到的所有主要数据结构:简单动态字符串(SDS)、整数集合、字典、压缩列表、双端链表、快速列表、跳表。redis并没有直接使用前面的数据结构来实现键值对的数据库,而是基于数据结构创建了一个对象系统,每种对象都用到前面至少一种数据结构。如果对这几种编码类型不是很了解,请查看redis6系列文章。
1. 对象的类型和编码
在 Redis 中,这五种类型的对象都是用 redisObject 结构来表示的:
#define LRU_BITS 24
typedef struct redisObject {
unsigned type:4;
unsigned encoding:4;
//记录对象最后一次被程序访问时间,用于计算空转时长(当前时间-lru)
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). */
//引用计数,初始为1,用于内存回收
int refcount;
//指向数据的指针
void *ptr;
} robj;
1.1 类型(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. */
#define OBJ_MODULE 5 /* Module object. */
#define OBJ_STREAM 6 /* Stream object. */
注意:随着 Redis 的发展,Redis 对象又增加了 MODULE 和 STREAM两种类型,这两种类型不常用,这里不做介绍。
1.2 编码(encoding)
上一节中的对象类型对应十一种编码方式(即实际的数据结构类型):
#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 ziplists */
#define OBJ_ENCODING_STREAM 10 /* Encoded as a radix tree of listpacks */
1.3 对象类型和编码的对应关系
| 对象 | 对象type属性 | type命令输出 | 编码encoding | 对应的数据结构 | object encoding命令输出 |
|---|---|---|---|---|---|
| 字符串对象 | OBJ_STRING | "string" | OBJ_ENCODING_INT | 整数值 | "int" |
| 字符串对象 | OBJ_STRING | "string" | OBJ_ENCODING_EMBSTR | embstr 编码的简单动态字符串 | "embstr" |
| 字符串对象 | OBJ_STRING | "string" | OBJ_ENCODING_RAW | 简单动态字符串 | "raw" |
| 列表对象 | OBJ_LIST | "list" | OBJ_ENCODING_QUICKLIST | 快速列表 | "quicklist" |
| 哈希对象 | OBJ_HASH | "hash" | OBJ_ENCODING_ZIPLIST | 压缩列表 | "ziplist" |
| 哈希对象 | OBJ_HASH | "hash" | OBJ_ENCODING_HT | 字典 | "hashtable" |
| 集合对象 | OBJ_SET | "set" | OBJ_ENCODING_INTSET | 整数集合 | "intset" |
| 集合对象 | OBJ_SET | "set" | OBJ_ENCODING_HT | 字典 | "hashtable" |
| 有序集合对象 | OBJ_ZSET | "zset" | OBJ_ENCODING_ZIPLIST | 压缩列表 | "ziplist" |
| 有序集合对象 | OBJ_ZSET | "zset" | OBJ_ENCODING_SKIPLIST | 跳表 | "skiplist" |
1.4 从set hello word 说起
以set hello word为例,因为Redis是KV键值对的数据库, 每个键值对都会有一个dictEntry(源码位置:dict.h) ,源码如下:
typedef struct dictEntry {
void *key;
union {
void *val;
uint64_t u64;
int64_t s64;
double d;
} v;
struct dictEntry *next;
} dictEntry;
- 里面指向了
key和value的指针,next指向下一个dictEntry。 key是字符串,但是Redis没有直接使用C的字符数组,而是存储在redis自定义的SDS中。value既不是直接作为字符串存储,也不是直接存储在SDS中,而是存储在redisObject中。 实际上五种常用的数据类型的任何一种,都是通过redisObject来存储的。
1.5 使用对象优点
为了便于操作,Redis采用redisObjec结构来统一五种不同的数据类型,这样所有的数据类型就都可以以相同的形式在函数间传递而不用使用特定的类型结构。同时,为了识别不同的数据类型,redisObjec中定义了type和encoding字段对不同的数据类型加以区别。简单地说, redisObjec就是string、hash、list、set、zset的父类 ,可以在函数间传递时隐藏具体的类型信息,所以作者抽象了redisObjec结构来到达同样的目的。使用对象的优点:
-
在执行命令之前,根据对象类型判断一个对象是否可以执行给定的命令
-
针对不同厂家,Wie对象设置多种不同的数据结构实现,从而优化效率
-
实现了基于引用计数的内存回收机制,不再使用的对象,内存会自动释放
-
引用计数实现对象共享机制,多个数据库共享同一个对象以节约内存
-
对象带有时间时间积累信息,用于计算空转时间
2. 字符串对象(String)
2.1 常用命令
- 最常用
- set key value
- get key
- 同时设置/获取多个键值
- mset key value [key value ...]
- mget key [key ...]
- 数值增减
- 递增数字 incr key
- 增加指定的整数 incrby key increment
- 递减数字 decr key
- 减少指定整数 decrby key increment
- 获取字符串长度
- strlen key
- 分布式锁(后面文章详细讲解)
- setnx key value
- set key value [EX seconds] [PX milliseconds] [NX|XX]
set key value 命令是怎么执行的?源码t_string.c如下:
/* SET key value [NX] [XX] [KEEPTTL] [GET] [EX <seconds>] [PX <milliseconds>]
* [EXAT <seconds-timestamp>][PXAT <milliseconds-timestamp>] */
void setCommand(client *c) {
robj *expire = NULL;
int unit = UNIT_SECONDS;
int flags = OBJ_NO_FLAGS;
if (parseExtendedStringArgumentsOrReply(c,&flags,&unit,&expire,COMMAND_SET) != C_OK) {
return;
}
c->argv[2] = tryObjectEncoding(c->argv[2]);
setGenericCommand(c,flags,c->argv[1],c->argv[2],expire,unit,NULL,NULL);
}
2.2 编码结构
字符串对象有三种编码结构,int、embstr、raw,接下来我们分别进行解释。
2.2.1 OBJ_ENCODING_INT编码
保存了long型(长整型)的64位(8个字节)有符号整数:
- long 数据类型是64位、有符号的以二进制补码表示的整数
- 最小值是
**-9223372036854775808**(-2^64) - 最大值是
**9223372036854775807**(2^64 -1) - 这种类型主要是使用在需要比较大整数的系统上
- 默认值是0L
注意:只有整数才会使用int,如果是浮点数,Redis内部其实先将浮点数转化成字符串值,然后在保存。
案例:set k1 123
当字符串键值的内容可以用一个 64 位有符号整形来表示时, Redis 会将键值转化为 long 型来进行存储,此时即对应 OBJ_ENCODING_INT 编码类型。内部的内存结构表示如下 :
Redis 启动时会预先建立 10000 个分别存储 0~9999 的 redisObject 变量作为共享对象,这就意味着如果 set 字符串的键值在 0~10000 之间的话,则可以 直接指向共享对象 而不需要再建立新对象,此时键值不占空间! 对应的源码在 object.c,常量值定义在server.c中:
robj *tryObjectEncoding(robj *o) {
...
len = sdslen(s);
// 字符串长度小于等于20且字符串转long型成功
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. */
// 配置maxmemory且值在10000以内,直接使用共享对象值
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);
}
}
}
2.2.2 OBJ_ENCODING_EMBSTR编码
embstr:embedded string,表示嵌入式的String。表示embstr格式的SDS(Simple Dynamic String),保存长度小于44字节的字符串。对应的源码在 object.c,常量值定义在server.c中:
#define OBJ_ENCODING_EMBSTR_SIZE_LIMIT 44
robj *createStringObject(const char *ptr, size_t len) {
if (len <= OBJ_ENCODING_EMBSTR_SIZE_LIMIT)
return createEmbeddedStringObject(ptr,len);
else
return createRawStringObject(ptr,len);
}
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;
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;
}
对于长度小于 44的字符串,Redis 对键值采用OBJ_ENCODING_EMBSTR 方式,EMBSTR 顾名思义即:embedded string,表示嵌入式的String。从内存结构上来讲 即字符串 sds结构体与其对应的 redisObject 对象分配在同一块连续的内存空间,字符串sds嵌入在redisObject对象之中一样。对应的结构如下:
2.2.3 OBJ_ENCODING_RAW编码
保存长度大于44字节的字符串。对应的源码在 object.c,常量值定义在server.c中:
#define OBJ_ENCODING_EMBSTR_SIZE_LIMIT 44
robj *createStringObject(const char *ptr, size_t len) {
if (len <= OBJ_ENCODING_EMBSTR_SIZE_LIMIT)
return createEmbeddedStringObject(ptr,len);
else
return createRawStringObject(ptr,len);
}
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;
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;
}
当字符串的键值为长度大于44的超长字符串时,Redis 则会将键值的内部编码方式改为OBJ_ENCODING_RAW格式,这与OBJ_ENCODING_EMBSTR编码方式的不同之处在于,此时动态字符串sds的内存 与其依赖的redisObject的内存不再连续了,对应的结构图:
特殊案例:
127.0.0.1:6379> SET str "1234567890"
OK
127.0.0.1:6379> STRLEN str
(integer) 10
127.0.0.1:6379> OBJECT ENCODING str
"embstr"
127.0.0.1:6379> APPEND str _
(integer) 11
127.0.0.1:6379> OBJECT ENCODING str
"raw"
注意:对于embstr ,由于其实现是只读的,因此对 embstr 对象进行修改时,都会先转化成 raw 再进行修改。因此,只要是修改 embstr 对象,修改后的对象一定是 raw 的,无论是否达到44字节。
编码转换逻辑图如下:
2.2.4 三种编码结构总结
- 只有整数才会使用
int,如果是浮点数, Redis 内部其实先将浮点数转化为字符串值,然后再保存。 embstr与raw类型底层的数据结构其实都是SDS(简单动态字符串 ,Redis 内部定义 sdshdr 一种结构)。 这三者的区别如下表格: |编码类型|content| |--|--| |int|Long类型整数时,RedisObject中的ptr指针直接赋值为整数数据,不再额外的指针再指向整数了,节省了指针的空间开销。| |embstr|当保存的是字符串数据且字符串小于等于44字节时,embstr类型将会调用内存分配函数,只分配一块连续的内存空间,空间中依次包含redisObject与sdshdr两个数据结构,让元数据、指针和SDS是一块连续的内存区域,这样就可以避免内存碎片| |raw|当字符串大于44字节时,SDS的数据量变多变大了,SDS和RedisObject布局分家各自过,会给SDS分配多的空间并用指针指向SDS结构,raw类型将会调用两次内存分配函数,分配两块内存空间,一块用于包含redisObject结构,而另一块用于包含sdshdr结构|
三者的结构图如下:
总结:Redis内部会根据用户给的不同键值而使用不同的编码格式,自适应地选择比较优化的内部编码结构,而这一切对用户完全透明!
2.3 应用场景
- 比如 点赞一篇文章,点一下加一次 incr
3. 哈希对象(hash)
3.1 常用命令
- 一次设置一个字段值 hset key field value
- 一次获取一个字段值 hget key field
- 一次设置多个字段值 hmset key field1 value1 [field value ...]
- 一次获取多个字段值 hmget key field [field ...]
- 获取所有字段值 hgetall key
- 获取某个key内的全部数量 hlen key
- 删除一个key hdel key
hset命令时怎么执行的?源码t_hash.c:
3.2 编码结构
哈希对象的编码可以是ziplist 和 hashtable 。配置参数如下:
hash_max_ziplist_value使用压缩列表保存时哈希集合中单个元素的最大长度,默认64bytehash_max_ziplist_entries使用压缩列表保存时哈希集合中的最大元素个数,默认512个
只有当哈希对象保存的键值对数量小于512个, 且所有键值对的键和值的字符串长度都小于等于64byte(一个英文字母一个字节)时用ziplist,否则使用hashtable。 ziplist升级到hashtable可以,反过来降级不可以。
编码转换流程图:
3.3 应用场景
购物车早期设计:
新增商品 → hset shopcar:uid1024 334488 1
新增商品 → hset shopcar:uid1024 334477 1
增加商品数量 → hincrby shopcar:uid1024 334477 1
商品总数 → hlen shopcar:uid1024
全部选择 → hgetall shopcar:uid1024
4. 列表
一个双端链表的结构, 容量是2的32次方减1个元素,大概40多亿,主要功能有push/pop等,一般用在栈、队列、消息队列等场景。
4.1 常用命令
- 向列表左边加元素 lpush key value [value ...]
- 向列表右边添加元素 rpush key value [value ...]
- 查看列表 lrange key start end
- 获取列表中元素的个数 llen key
4.2 编码结构
使用quicklist来存储,quicklist 存储了一个双向链表,每一个节点都是ziplist。是ziplist和linkedlist的结合体。
4.3 应用场景
- 微信公众号订阅消息
- 大V作者张老师和CSDN发布了文章分别是 11 和 22
- 王五关注了他们两个,只要他们发布了新文章,就会安装进我的List
- lpush likearticle:王五id 11 22
- 查看张三自己的号订阅的全部文章,类似分页,下面0~10就是一次显示10条
- lrange likearticle:王五id 0 9
- 商品评论列表
需求1:用户针对某一商品发布评论,一个商品会被不同的用户评论,保存商品评论时,要按时间顺序排序; 需求2:用户在前端查询改商品的评论时,需要按照时间顺序降序-
使用list存储商品评论信息,key是该商品的id,value是商品评论信息商品编号为1001的商品评论key【items:comment:1001】
-
lpush items:comment:1001 {"id":1001,"name":"huawei","date":1600484283054,"content":"lasjfdljsa;fdlkajsd;lfjsa;ljf;lasjf;lasjfdlsad"}
-
5. 集合对象
5.1 常用命令
- 添加元素 sadd key member [member ...]
- 删除元素 srem key member [member ...]
- 遍历集合中的所有元素 smembers key
- 判断元素是否在集合中 sismember key member
- 判断集合中元素总数 scard key
- 从集合中随机弹出一个元素,元素不删除 srandmember key [数字]
- 从集合中随机弹出一个元素,出一个删除一个 spop key [数字]
- 集合的差集运算 sdiff key [key ...]
- 集合的交集运算 sinter key [key ...]
- 集合的并集运算 sunion key [key ...]
5.2 编码结构
Redis用intset或hashtable存储集合对象。
set-max-intset-entries集合中的元素个数,默认512 如果元素都是整数类型并且与元素个数不超过set-max-intset-entries的值,就用intset存储;否则就使用hashtable存储。源码t_set.c如下:
void saddCommand(client *c) {
robj *set;
int j, added = 0;
set = lookupKeyWrite(c->db,c->argv[1]);
if (checkType(c,set,OBJ_SET)) return;
if (set == NULL) {
set = setTypeCreate(c->argv[2]->ptr);
dbAdd(c->db,c->argv[1],set);
}
for (j = 2; j < c->argc; j++) {
if (setTypeAdd(set,c->argv[j]->ptr)) added++;
}
if (added) {
signalModifiedKey(c,c->db,c->argv[1]);
notifyKeyspaceEvent(NOTIFY_SET,"sadd",c->argv[1],c->db->id);
}
server.dirty += added;
addReplyLongLong(c,added);
}
/* Add the specified value into a set.
*
* If the value was already member of the set, nothing is done and 0 is
* returned, otherwise the new element is added and 1 is returned. */
int setTypeAdd(robj *subject, sds value) {
long long llval;
if (subject->encoding == OBJ_ENCODING_HT) {
dict *ht = subject->ptr;
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) {
if (isSdsRepresentableAsLongLong(value,&llval) == C_OK) {
uint8_t success = 0;
subject->ptr = intsetAdd(subject->ptr,llval,&success);
if (success) {
/* Convert to regular set when the intset contains
* too many entries. */
size_t max_entries = server.set_max_intset_entries;
/* limit to 1G entries due to intset internals. */
if (max_entries >= 1<<30) max_entries = 1<<30;
if (intsetLen(subject->ptr) > max_entries)
setTypeConvert(subject,OBJ_ENCODING_HT);
return 1;
}
} else {
/* Failed to get integer from object, convert to regular set. */
setTypeConvert(subject,OBJ_ENCODING_HT);
/* The set *was* an intset and this value is not integer
* encodable, so dictAdd should always work. */
serverAssert(dictAdd(subject->ptr,sdsdup(value),NULL) == DICT_OK);
return 1;
}
} else {
serverPanic("Unknown set encoding");
}
return 0;
}
5.3 应用场景
- 微信抽奖小程序
- 用户ID,立即参与按钮 :sadd key 用户ID
- 显示已经有多少人参与了 :scard key
- 抽奖(从set中任意选取N个中奖人)
- SRANDMEMBER key 2 随机抽奖2个人,元素
不删除 - SPOP key 3 随机抽奖3个人,元素 会删除
- SRANDMEMBER key 2 随机抽奖2个人,元素
- 微信朋友圈点赞
- 新增点赞 : sadd pub:msgID 点赞用户ID1 点赞用户ID2
- 取消点赞 : srem pub:msgID 点赞用户ID
- 展现所有点赞过的用户 : SMEMBERS pub:msgID
- 点赞用户数统计,就是常见的点赞红色数字:scard pub:msgID
- 判断某个朋友是否对楼主点赞过:SISMEMBER pub:msgID 用户ID
- 微博好友关注社交关系
- 共同关注的人:sinter key1 key2
- 好友推荐 : sdiff key1 key2
6. 有序集合对象
6.1 常用命令
- 添加元素 zadd key score member [score member ...]
- 返回索引之间的所有与元素 zrange key start stop [WITHSCORES]
- 获取元素的分数 zscore key member
- 删除元素 zrem key member [member ...]
- 获取指定分数范围的元素 zrangebyscore key min max [WITHSCORES] [LIMIT offset count]
- 增加某个元素的分数 zincrby key increment member
- 获取集合中元素的数量 zcard key
- 获得指定分数范围内的数量 zcount key min max
- 按照排名范围删除元素 zremrangebyrank key start stop
- 获取元素的排名 zrank key member(从小到大) zrevrank key member(从大到小)
6.2 编码结构
有序集合对应可以使用ziplist和skiplist结构进行存储。参数如下:
zset_max_ziplist_entries:有序集合中元素个数,默认128zset_max_ziplist_value: 有序集合中元素长度,默认64byte
当有序集合中包含的元素数量不超过server.zset_max_ziplist_entries 的值(默认值为 128), 且有序集合中新添加元素的 member 的长度不大于 server.zset_max_ziplist_value 的值(默认值为 64)时, 使用ziplist 存储,否则采用skiplist存储。源码t_zset.c 如下:
6.3 应用场景
- 根据商品销售对商品进行排序显示
思路:定义商品销售排行榜(sorted set集合),key为goods:sellsort,分数为商品销售数量。- 商品编号1001的销量是9,商品编号1002的销量是15
zadd goods:sellsort 9 1001 15 1002 - 有一个客户又买了2件商品1001,商品编号1001销量加2
zincrby goods:sellsort 2 1001 - 求商品销量前10名
ZRANGE goods:sellsort 0 10 withscores
- 商品编号1001的销量是9,商品编号1002的销量是15
- 微博热搜
- 点击新闻
ZINCRBY hotvcr:20200919 15 新闻1 2 新闻2 - 展示当日排行前10条
ZREVRANGE hotvcr:20200919 0 9 withscores
- 点击新闻
7. 总结
7.1 五种数据类型对应的底层编码结构
| 对象 | 条件 | 编码结构 |
|---|---|---|
| 字符串对象 | 8个字节的长整型 | int |
| 字符串对象 | 小于等于44个字节的字符串 | embstr |
| 字符串对象 | 大于44个字节的字符串 | raw |
| 哈希对象 | 当哈希类型元素个数小于hash-max-ziplist-entries 配置(默认512个)、同时所有值都小于hash-max-ziplist-value配置(默认64 字节)时 | ziplist |
| 哈希对象 | 无法满足使用ziplist条件 | hashtable |
| 列表对象 | 只有一种编码类型 | quicklist |
| 集合对象 | 当集合中的元素都是整数且元素个数小于set-max- intset-entries配置(默认512个)时 | intset |
| 集合对象 | 无法满足使用intset条件 | hashtable |
| 有序集合对象 | 当有序集合的元素个数小于zset-max-ziplist- entries配置(默认128个),同时每个元素的值都小于zset-max-ziplist-value配 置(默认64字节)时 | ziplist |
| 有序集合对象 | 无法满足使用ziplist条件 | skiplist |