本文已参与「新人创作礼」活动,一起开启掘金创作之路。
高级数据结构
基础对象 object
定义
#define LRU_BITS 24
typedef struct redisObject {
// 存储对象类型
unsigned type:4;
// 记录底层数据结构
unsigned encoding:4;
// 可能是lru/lfu
// 记录对象最后访问时间/低8位记录对象访问频率,高16位记录对象访问时间
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;
补充
- 位域
采用了c的位域来节省内存,简便操作
在移植的情况下需要适当的字节填充
- 时间计算
获取时间的函数属于系统调用,比较耗费资源
redis采用缓存的方式在定时任务中定期更新。
int serverCron(struct aeEventLoop *eventLoop, long long id, void *clientData) {
//....
/* Update the time cache. */
updateCachedTime();
//....
}
更新的时候如果在lruclock中使用了atomicGet可能是因为别的线程也会用到该时间,例如集群状态
字符串 string
定义
分了三种类型
-
Int
- 小于20字节(一个传给长整型范围内)且不是浮点数
- 利于节省空间(比如123456等,如果是int需要4字节,但是是string需要6字节)
-
Embstr
- 长度小于等于44字节的字符串,包括浮点数
- 主要是适应jemalloc分配64k的arean
- 如果修改了会被转为raw。且不会回退
-
Raw
- 长度大于44字节的字符串,包括浮点数
补充
tryObjectEncoding
| 类型 | 特点 | 优点 | 缺点 |
|---|---|---|---|
| embstr | 1.只分配一次内存空间,因此robj和sds是连续的;2.只读;3.Embstr字符串需要修改时,会转成raw,之后一直为raw | 1.创建和删除只需要一次; 2.寻找速度快 | 1.重分配涉及到robj和sds整个对象,因此embstr是只读的 |
| raw | 1.robj和sds非连续; 2.可修改 |
- append会修改字符串底层类型
- 字符串长度不能大于512M
static int checkStringLength(client *c, long long size) {
if (size > 512*1024*1024) {
addReplyError(c,"string exceeds maximum allowed size (512MB)");
return C_ERR;
}
return C_OK;
}
应用场景
- 缓存功能:mysql存储,redis做缓存
- 计数器:如点赞次数,视频播放次数
- 限流:见基于redis的分布式限流
列表 list
只有quicklist一种类型,是一个双向链表
定义
/* quicklistNode is a 32 byte struct describing a ziplist for a quicklist.
* We use bit fields keep the quicklistNode at 32 bytes.
* count: 16 bits, max 65536 (max zl bytes is 65k, so max count actually < 32k).
* encoding: 2 bits, RAW=1, LZF=2.
* container: 2 bits, NONE=1, ZIPLIST=2.
* recompress: 1 bit, bool, true if node is temporarry decompressed for usage.
* attempted_compress: 1 bit, boolean, used for verifying during testing.
* extra: 10 bits, free for future use; pads out the remainder of 32 bits */
typedef struct quicklistNode {
//双向链表的前节点
struct quicklistNode *prev;
//双向链表的后节点
struct quicklistNode *next;
///不设置压缩数据参数recompress时指向一个ziplist结构
//设置压缩数据参数recompress指向quicklistLZF结构
unsigned char *zl;
//压缩列表ziplist的总长度
unsigned int sz; /* ziplist size in bytes */
//每个ziplist中entry总个数
unsigned int count : 16; /* count of items in ziplist */
//表示是否采用了LZF压缩算法压缩quicklist节点,1表示压缩过,2表示没压缩,占2 bits长度
unsigned int encoding : 2; /* RAW==1 or LZF==2 */
//表示是否启用ziplist来进行存储
unsigned int container : 2; /* NONE==1 or ZIPLIST==2 */
//记录该节点之前是否被压缩过
unsigned int recompress : 1; /* was this node previous compressed? */
//测试是使用
unsigned int attempted_compress : 1; /* node can't compress; too small */
//额外扩展位,占10bits长度
unsigned int extra : 10; /* more bits to steal for future usage */
} quicklistNode;
/*
当指定使用lzf压缩算法压缩ziplist的entry节点时,
quicklistNode结构的zl成员指向quicklistLZF结构
quicklistLZF is a 4+N byte struct holding 'sz' followed by 'compressed'.
* 'sz' is byte length of 'compressed' field.
* 'compressed' is LZF data with total (compressed) length 'sz'
* NOTE: uncompressed length is stored in quicklistNode->sz.
* When quicklistNode->zl is compressed, node->zl points to a quicklistLZF */
typedef struct quicklistLZF {
////表示被LZF算法压缩后的ziplist的大小
unsigned int sz; /* LZF size in bytes*/
//压缩之后的数据,柔性数组
char compressed[];
} quicklistLZF;
/* quicklist is a 40 byte struct (on 64-bit systems) describing a quicklist.
* 'count' is the number of total entries.
* 'len' is the number of quicklist nodes.
* 'compress' is: -1 if compression disabled, otherwise it's the number
* of quicklistNodes to leave uncompressed at ends of quicklist.
* 'fill' is the user-requested (or default) fill factor. */
typedef struct quicklist {
//链表表头
quicklistNode *head;
//链表表尾
quicklistNode *tail;
//所有quicklistnode节点中所有的entry个数
unsigned long count; /* total count of all entries in all ziplists */
//quicklistnode节点个数,也就是quicklist的长度
unsigned long len; /* number of quicklistNodes */
//单个节点的填充因子,也就是ziplist的大小
int fill : 16; /* fill factor for individual nodes */
//保存压缩程度值,配置文件设定,占16bits,0表示不压缩
unsigned int compress : 16; /* depth of end nodes not to compress;0=off */
} quicklist;
//quicklist中quicklistNode的entry结构
typedef struct quicklistEntry {
//指向所属的quicklist指针
const quicklist *quicklist;
//指向所属的quicklistNode节点的指针
quicklistNode *node;
//指向当前ziplist结构的指针
unsigned char *zi;
//指向当前ziplist结构的字符串value成员
unsigned char *value;
//指向当前ziplist结构的整型value成员
long long longval;
//当前ziplist结构的字节数
unsigned int sz;
//保存相对ziplist的偏移量
int offset;
} quicklistEntry;
优点
- 权衡了数组和双向链表。解决了双向链表对内存不友好造成大量内存碎片,数组插入删除复杂度高的问题。综合来说类似于c++的deque
分析
当list的len=1的时候会退化为ziplist,如果找不到一块较大的连续内存会oom,当ziplist=1的时候会退化为双向链表,对内存不友好。折合来选择靠字段fill
- fill
16位,用来存放list-max-ziplist-size参数的值,默认为-2
如果为-2代表ziplist容量为8k
如果是正值,表示每个quicklist上ziplist的entry个数
//在原来节点的基础上又需要新添加一个
REDIS_STATIC int _quicklistNodeAllowInsert(const quicklistNode *node,
const int fill, const size_t sz) {
//...
//这里这里,count表示当前entry的总个数
else if ((int)node->count < fill)
return 1;
//....
}
- compress
16位,表示节点压缩深度设置,存放list-compress-depth,默认为0
当数据很多的时候最容易被访问的是两端数据。list提供了选项将中间的数据节点(node级别)进行压缩来节省内存空间
- 0 -- 都不压缩
- 1 -- 表示quicklist两端各有一个节点不压缩,中间节点压缩
- 2 -- 两端各两个节点不压缩,中间节点压缩
- 3 -- 两端各三个节点不压缩,中间节点压缩
...
- recompress
对接点暂时解压,在后面某个时刻再将其压缩,减少压缩解压的次数
_quicklistNodeAllowInsert(quicklist->head, quicklist->fill, sz)
....
特性
-
阻塞和非阻塞
- 阻塞
当给定的key不存在时,BLPOP或BRPOP命令会被阻塞连接,当另一个client对这些key执行push会解除调用BLPOP和BRPOP的阻塞
就是判断超时或有数据,否则不返回
应用场景
-
常见数据结构
- lpush+lpop=Stack(栈)
- lpush+rpop=Queue(队列)
- lpush+ltrim=Capped Collection(有限集合)
- lpush+brpop=Message Queue(消息队列)
-
文章列表
- 组合的方式:文章用hash存储,文章列表用list存储
哈希 hash
权衡了连续内存和不连续内存的利弊,使用ziplist和dict两种数据结构存储
当ziplist的entry个数大于512,或value字节数超过64都会转为dict
- ziplist转为dict的不可逆的
- 尽量使用ziplist,且长度尽量控制在1000内。长列表存取数据时间复杂度O(N)会导致cpu消耗严重
- 两个参数可以在配置文件中修改
- 当ziplist作为底层对象,保存不是顺序保存的,时间复杂度为O(N)
应用场景
set user:1:name tom
set user:1:age 23
set user:1:city beijing
set user:1 serialize(userInfo)
hmset user:1 name tom age 23 city beijing
-
存储对象
-
原生字符串
- 优点:直观,每个属性可以独立更新
- 缺点:存储大量的键,内存消耗大,用户信息内聚性差,生产较少使用
-
存储序列化后的字符串(结合pb使用,使用广泛)
- 优点:简化编程,提高内存使用率
- 缺点:序列化和反序列化有一定开销
-
哈希
- 优点:简化编程,每个用户的属性用一堆field-value,但是只用一个key存储
- 缺点:要控制在ziplist和hashtable两种编码的转换,hashtable消耗内存更多。
-
集合 set
也使用两种方式存储
intset当entry个数超过512或集合对象中存在非整数值,会转换为dict
robj *setTypeCreate(sds value){
if (isSdsRepresentableAsLongLong(value, NULL) == C_OK)
return createIntsetObject();
return createSetObject();
}
----> string2ll int
- 转换操作不可逆
- set不允许重复
- set支持交并补,社交场景,标签用的多
- 参数set-max-intset-entries可以在配置文件中修改
- dict作为底层对象,value为NULL
- intset查找时间复杂度为O(logN)
应用场景
sadd user:1:tags tag1 tag2 tag5
sadd user:2:tags tag2 tag3 tag5
...
sadd user:k:tags tag1 tag2 tag4
---------
sadd tag1:users user:1 user:3
sadd tag2:users user:1 user:2 user:3
...
sadd tagk:users user:1 user:2
...
---------
sinter user:1:tags user:2:tags
---------
spop/srandmember
-
标签,主要用于社交,尽量保证在一个事务内完成
- 给用户添加标签
- 给标签标记用户
- 计算用户共同感兴趣的标签
-
抽奖随机数
有序集合 zset
比较复杂,单独定义了一个结构体
也有两个底层:ziplist和skiplist
typedef struct zset {
dict *dict;//字段
zskiplist *zsl;//线段跳表
} zset;
ziplist当entry个数超过238,或value字节数大于64会转换为skiplist
一个插入排序
如果大于score会插入前面,如果相等,再比较元素。如果再大,就在前面插入
/* Insert (element,score) pair in ziplist. This function assumes the element is
* not yet present in the list. */
unsigned char *zzlInsert(unsigned char *zl, sds ele, double score) {
unsigned char *eptr = ziplistIndex(zl,0), *sptr;
double s;
// 排序的过程
while (eptr != NULL) {
sptr = ziplistNext(zl,eptr);
serverAssert(sptr != NULL);
s = zzlGetScore(sptr);
if (s > score) {
/* First element with score larger than score for element to be
* inserted. This means we should take its spot in the list to
* maintain ordering. */
// 元素,score
zl = zzlInsertAt(zl,eptr,ele,score);
break;
} else if (s == score) {
/* Ensure lexicographical ordering for elements. */
if (zzlCompareElements(eptr,(unsigned char*)ele,sdslen(ele)) > 0) {
zl = zzlInsertAt(zl,eptr,ele,score);
break;
}
}
/* Move to next element. */
eptr = ziplistNext(zl,sptr);
}
/* Push on tail of list when it was not yet inserted. */
if (eptr == NULL)
zl = zzlInsertAt(zl,NULL,ele,score);
return zl;
}
转换
为什么skiplist基础上要创建dict
加快速度
需要获取某个元素的score的时候,skiplist查询的时间复杂度为O(lgN),dict时间复杂度为O(1)
当底层为ziplist的时候时间复杂度是O(N)
int zsetScore(robj *zobj, sds member, double *score) {
if (!zobj || !member) return C_ERR;
if (zobj->encoding == OBJ_ENCODING_ZIPLIST) {
if (zzlFind(zobj->ptr, member, score) == NULL) return C_ERR;
} else if (zobj->encoding == OBJ_ENCODING_SKIPLIST) {
zset *zs = zobj->ptr;
dictEntry *de = dictFind(zs->dict, member);
if (de == NULL) return C_ERR;
*score = *(double*)dictGetVal(de);
} else {
serverPanic("Unknown sorted set encoding");
}
return C_OK;
}
//重点的zzlFind
unsigned char *zzlFind(unsigned char *zl, sds ele, double *score) {
unsigned char *eptr = ziplistIndex(zl,0), *sptr;
// 只是一个while循环遍历,时间复杂度为O(n)
while (eptr != NULL) {
sptr = ziplistNext(zl,eptr);
serverAssert(sptr != NULL);
if (ziplistCompare(eptr,(unsigned char*)ele,sdslen(ele))) {
/* Matching element, pull out score. */
if (score != NULL) *score = zzlGetScore(sptr);
return eptr;
}
/* Move to next element. */
eptr = ziplistNext(zl,sptr);
}
return NULL;
}
- skiplist和dict的共享元素和score的(指针复制)
- 转换操作是不可逆的
- 两个参数可以在配置文件中修改
- zset不允许重复
应用场景
zadd user:ranking:2016_03_15 3 mike
zincrby user:ranking:2016_03_15 1 mike
zrem user:ranking:2016_03_15 mike
zrevrange user:ranking:2016_03_15 0 9
hgetall user:info:tom
zscore user:ranking:2016_03_15 mike
zrank user:ranking:2016_03_15 mike
-
优先队列
- 支持排序
-
排行榜系统:视频网站上需要对用户上传的视频进行排行榜排序,排行榜是多维的,按照播放量,时间,点赞数。。。
- 添加用户赞数
- 有人点赞
- 取消点赞
- 展示获赞数前十的用户
- 展示用户信息以及分数和排名
| 数据类型 | 适用场景 | 备注 |
|---|---|---|
| 字符串(string) | 缓存;计算器 | 简单型的。如set stunum studentInfo。 计数器如限流 |
| 列表(list) | lpush+lpop=Stack(栈) lpush+rpop=Queue(队列) lpush+ltrim=Capped Collection(有限集合) lpush+brpop=Message Queue(消息队列) | 如阻塞队列,关注列表 |
| 哈希(hash) | 对象属性(尤其不定长的) | 如缓存studentInfo,hmset stunum stunum 1 stuname dinghaha age 18 |
| 集合(set) | 适用社交场景 | 赞/踩、粉丝、共同好友/喜好、推送 |
| 有序集合(set) | 排行榜;优先队列;缓存相关的元数据(比如按照排序的挑战) |
参考:
《Redis设计与实现》
硬核课堂