基础知识
redisObject
redis通过redisObject对象进行存储
typedef struct redisObject {
unsigned type:4; //表示对象类型
unsigned encoding:4; //表示对象的内部编码
unsigned lru:REDIS_LRU_BITS; //对象最后一次被命令程序访问的时间
int refcount; //记录的是该对象被引用的次数
void *ptr; //指针指向具体的数据
} robj;
string
主要有三种编码方式:int、embstr、raw
Int
8个字节的长整型。存储整数。
embstr
存储<=39字节的字符串。使用redisObject和sds保存数据,只分配一次内存空间(因此redisObject和sds是连续的)
raw
大于39个字节的字符串,创建时分配两次内存空间(分别为redisObject和sds分配空间)
补充:
sds
注意:以下仅是一个简单的示例,事实上sds根据长度范围定义了不同的结构
struct sdshdr {
unsigned int len; //buf中已经使用的长度
unsigned int alloc; //buf中未使用的长度
unsigned char flags; //flag用3bit来标明类型,其余5bit目前没有使用
char buf[]; //柔性数组buf
};
使用场景
string主要有以下几个典型使用场景:
- 缓存功能
- 计数
- 共享Session
- 限速
Hash
主要有两种编码方式:listpack、hashtable
listpack
/* Each entry in the listpack is either a string or an integer. */
typedef struct {
/* 当使用字符串时,它提供长度(slen) */
unsigned char *sval;
uint32_t slen;
/* 当使用整数时,“sval”为NULL,lval 保存该值。 */
long long lval;
} listpackEntry;
hashtable
一个hashtable由1个dict结构、2个dictht结构、多个dictEntry组成(7.0版本之前,7.0版本之后dicht被去除,dict直接指向dictEntry **table链表)
typedef struct dict {
dictType *type;
void *privdata;
dictht ht[2];
long rehashidx; /* rehashing not in progress if rehashidx == -1 */
unsigned long iterators; /* number of iterators currently running */
} dict;
typedef struct dictht {
dictEntry **table;
unsigned long size;
unsigned long sizemask;
unsigned long used;
} dictht;
typedef struct dictEntry {
void *key;
union {
void *val;
uint64_t u64;
int64_t s64;
double d;
} v;
struct dictEntry *next;
} dictEntry;
使用场景
hash主要有以下典型应用场景:
- 缓存用户信息
- 缓存对象
List
主要有两种编码方式:linklist、ziplist、quicklist
linklist
主要是一个双向链表
ziplist
压缩链表,主要是一块连续内存块
- 1、zlbytes:压缩列表的字节长度,占4个字节
- 2、zltail:压缩列表尾元素相对于压缩列表起始地址的偏移量,占4个字节
- 3、zllen:压缩列表的元素数目,占两个字节
- 4、entryX:压缩列表存储的若干个元素,可以为字节数组或者整数
- 5、zlend:压缩列表的结尾,占一个字节,恒为0xFF。
typedef struct zlentry {
unsigned int prevrawlensize; /* Bytes used to encode the previous entry len*/
unsigned int prevrawlen; /* Previous entry len. */
unsigned int lensize; /* Bytes used to encode this entry type/len.
For example strings have a 1, 2 or 5 bytes
header. Integers always use a single byte.*/
unsigned int len; /* Bytes used to represent the actual entry.
For strings this is just the string length
while for integers it is 1, 2, 3, 4, 8 or
0 (for 4 bit immediate) depending on the
number range. */
unsigned int headersize; /* prevrawlensize + lensize. */
unsigned char encoding; /* Set to ZIP_STR_* or ZIP_INT_* depending on
the entry encoding. However for 4 bits
immediate integers this can assume a range
of values and must be range-checked. */
unsigned char *p; /* Pointer to the very start of the entry, that
is, this points to prev-entry-len field. */
} zlentry;
quicklist
为了结合linklist和ziplist的优点,list 的底层实现使用快速列表 quicklist。
quicklist 包含多个内存不连续的节点,但每个节点本身就是一个 ziplist
typedef struct quicklistNode {
struct quicklistNode *prev; // 上一个 quicklist
struct quicklistNode *next; // 下一个 quicklist
unsigned char *zl; // 数据指针,指向 ziplist 结构,或者 quicklistLZF 结构
unsigned int sz; // ziplist 占用内存长度(未压缩)
unsigned int count : 16; // ziplist 记录数量
unsigned int encoding : 2; // 编码方式,1 表示 ziplist ,2 表示 quicklistLZF
unsigned int container : 2; //
unsigned int recompress : 1; // 临时解压,1 表示该节点临时解压用于访问
unsigned int attempted_compress : 1; // 测试字段
unsigned int extra : 10; // 预留空间
} quicklistNode;
typedef struct quicklistLZF {
unsigned int sz; // 压缩数据长度
char compressed[]; // 压缩数据
} quicklistLZF;
typedef struct quicklist {
quicklistNode *head; // 列表头部
quicklistNode *tail; // 列表尾部
unsigned long count; // 记录总数
unsigned long len; // ziplist 数量
int fill : 16; // ziplist 长度限制,每个 ziplist 节点的长度(记录数量/内存占用)不能超过这个值
unsigned int compress : 16; // 压缩深度,表示 quicklist 两端不压缩的 ziplist 节点的个数,为 0 表示所有 ziplist 节点都不压缩
} quicklist;
使用场景
list主要有以下几种使用场景:
- 消息队列
- 文章列表
Set
主要是两种编码方式:hashtable、intset
intset
typedef struct intset {
uint32_t encoding; /* 集合类型 */
uint32_t length; /* 集合长度 */
int8_t contents[]; /* 集合 */
} intset;
使用场景
set主要有如下使用场景:
- 标签(tag)
- 共同关注
ZSet
主要是两种编码方式:skiplist、ziplist
skiplist
主要是跳表
typedef struct zskiplistNode {
sds ele;
double score;
struct zskiplistNode *backward;
struct zskiplistLevel {
struct zskiplistNode *forward;
unsigned long span;
} level[];
} zskiplistNode;
typedef struct zskiplist {
struct zskiplistNode *header, *tail;
unsigned long length;
int level;
} zskiplist;
typedef struct zset {
dict *dict;
zskiplist *zsl;
} zset;
ziplist
参见List。
使用场景
ZSet主要应用场景:
- 用户点赞统计
- 用户排序
常见问题
1. 说说Redis主要数据结构和底层实现
redis主要数据结构有string(字符串)、hash(哈希)、list(列表)、set(集合)、zset(有序集合)五种。这些只是 Redis 对外的数据结构,实际上每种数据结构都有自己底层的内部编码实现,而且是多种实现, 而Redis 会在合适的场景选择合适的内部编码。
先说string,就是字符串,主要用于记录计数或一些简单的缓存对象,它主要有三种编码的实现,分别是int、embstr、raw。当记录对象为整数时,才会采用int编码;而存储<=39字节的字符串。使用emstr编码,使用redisObject和sds保存数据,此时只分配一次内存空间(因此redisObject和sds是连续的);当存储大于39个字节的字符串,使用raw,创建时分配两次内存空间(分别为redisObject和sds分配空间)。而sds则是redis自己定义的数据结构,用以存储数据,它主要由柔性数组(char[])buf、buf中已经使用的长度(int)len、buf中未使用的长度(int)alloc、buf类型(char)flags,flags一般用3bit来标明类型,其余5bit目前没有使用。
然后是hash,就是哈希,缓存复杂且有较频繁修改部分属性需求的对象。其两种编码方式:listpack、hashtable,主要使用的是hashtable,其在7.0版本之前由1个dctht结构、2个dicht、多个dictEntry组成,7.版本之后dicht被去除,dict直接指向dictEntry **table链表。
dict由字典的类型(dictType*)type、一个包含两个 dictht 结构体的数组(dictht[2])ht以用于实现哈希表的两个哈希表(ht[0] 和 ht[1])、用以表示当前是否在进行 rehash 操作(long)rehashidx 如果值为 -1则表示没有正在进行的 rehash 操作、表示当前正在运行的迭代器的数量(unsigned long)iterators,比较特殊的是通常情况下只有一个哈希表 ht[0] 有值,在扩容的时候,把ht[0]里的值rehash到ht[1],然后进行 渐进式rehash ——所谓渐进式rehash,指的是这个rehash的动作并不是一次性、集中式地完成的,而是分多次、渐进式地完成的。待搬迁结束后,h[1]就取代h[0]存储字典的元素。
dictht由一个指向 dictEntry 指针数组的指针(dictEntry **)table、table 数组的长度(unsigned long)size、哈希表大小的掩码以用于快速计算哈希值对应的数组索引(unsigned long)sizemask、哈希表中已经使用的槽位数量也就是当前哈希表中存储的元素个数(unsigned long)used组成。
dictEntry由一个指向键的指针(void )key、一个联合体,可以存储值的不同类型,包括指针、64 位无符号整数、64 位有符号整数和双精度浮点数()v、 一个指向下一个 dictEntry 结构体的指针(dictEntry)next*组成。
接着是list,主要用于消息队列、列表。主要有三种编码方式:linklist、ziplist、quicklist。着重说一下quicklist快速列表,quicklist 包含多个内存不连续的节点,这些节点以双向链表相连,且每个节点本身就是一个 ziplist
还有set,集合,主要有在标签(tag)和共同关注使用,主要是两种编码方式:hashtable、intset。
最后是ZSet,有序集合,主要在用户点赞统计和用户排序等排行榜使用,主要是两种编码方式:skiplist、ziplist。主要是skiplist跳表,实现原理如下:
- 层级结构: 跳表包含多个层级,每个层级都是一个链表,最底层是包含所有元素的完整有序链表。每个元素在每个层级中以某种概率出现,这样就可以通过跳过多个元素,加速查找。
- 索引: 在每个层级的链表中,节点包含一个指向下一个节点的指针,同时也包含一个指向同一层级中下一个元素的指针。这两种指针构成了节点在同一层级中的链表。在跳表的最顶层,有一个额外的头节点,指向最左和最右的元素。
- 查找操作: 从最顶层的头节点开始,沿着每一层的链表进行搜索,如果当前节点的下一个节点的值大于目标值,则在同一层级中继续搜索,如果小于或等于目标值,则转移到下一层级。这样反复进行,直到找到目标值或者确定目标值不在跳表中。
- 插入操作: 在执行插入操作时,首先执行查找操作找到插入位置,然后在相应层级中插入新节点,并更新索引。在每一层级中,决定是否在该层级插入新节点的概率是通过随机生成的方式确定的。
- 删除操作: 执行删除操作时,同样需要通过查找找到待删除节点,然后在每一层级中删除相应的节点,并更新索引。
作者水平有限。若有错漏还请指出,感谢!