Redis面试系列:基础类型编码方式和应用场景(一)

前言

面试中肯定会被问到一个问题:Redis有哪些基础类型,它们的应用场景是什么?或直接问:你实际项目中用到了哪些?

下面会结合一些核心源码来讲解,还会讲一些细节点。

(温馨提示:源码看不懂的可以先跳过,有兴趣的可以研究。不对读一读源码有助于加深理解和记忆,)

一、基础类型

核心的5个基础类型:string、list、hash、set、zset
扩展:HyperLogLog、Geo、Pub/Sub、BloomFilter

对象类型(type):字符串对象、列表对象、哈希对象、集合对象、有序集合对象

存储形式(encoding) :int、raw、embstr、ziplist、quicklist、hashtable、intset、skiplist

在这里插入图片描述

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 */

当哈希,列表,仅由整数组成的集合和有序集合在小于给定元素数且最大元素大小时,以一种非常节省内存的方式进行编码,该方式使用的内存减少多达10倍(其中5节省的时间(平均节省)。所以Redis每一种对象类型都有2种的编码存储方式。

1. String 字符串对象

在字符串长度小于44字节时,使用 embstr 形式存储;当长度超过 44字节时,使用 raw 形式存储。
int 编码用来保存的是可以用 long 类型表示的整数值。

SDS结构体:

#define SDS_TYPE_5  0
#define SDS_TYPE_8  1
#define SDS_TYPE_16 2
#define SDS_TYPE_32 3
#define SDS_TYPE_64 4

struct __attribute__ ((__packed__)) sdshdr5 {
    unsigned char flags; /* flags共8位,低三位保存类型标志,高5位保存字符串长度,小于32(2^5-1)*/
    char buf[]; /* 保存具体的字符串 */
};

struct __attribute__ ((__packed__)) sdshdr{8,16,32,64} { 
    uint8_t len; /* 字符串长度,buf已用的长度*/
    uint8_t alloc; /* 为buf分配的总长度,alloc-len就是sds结构体剩余的空间 */
    unsigned char flags; /* 低三位保存类型标志*/
    char buf[]; /* 保存具体的字符串 */
};

buf被定义为字节数组,用来存放数据,len代表字符串长度(获取字符串长度时间复杂度O(1) )。这样可以存放任何二进制的数据和文本数据,包括’\0’ (二进制安全)。

embstr和raw都是用sds来保存数据,两者的区别:embstr使用只分配一次内存空间(redisObject和sds是连续的),而raw需要分配两次内存空间(分别为redisObject和sds分配空间,redisObject和sds一般是不连续的)

二进制安全:指的是只关心二进制化的字符串,不关心具体格式。只会严格的按照二进制的数据存取,不会妄图以某种特殊格式解析数据。比如遇到’\0’ 字符不会停止解析。而对于C语言字符串来说,strlen是判断遇到’\0’之前的字符数量。

编码的转换

  • 当 int 编码保存的值不再是整数,或大小超过了long的范围时,自动转化为raw。
  • 对于 embstr 编码,在对embstr对象进行修改时,都会先转化为raw再进行修改。因此,只要是修改embstr对象,修改后的对象一定是raw的,无论是否达到了44个字节。
//创建字符串源码:
#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;
}
robj *createObject(int type, void *ptr) {
  robj *o = zmalloc(sizeof(*o));
  o->type = type;
  o->encoding = OBJ_ENCODING_RAW;
  o->ptr = ptr;
  o->refcount = 1;
  /* Set the LRU to the current lruclock (minutes resolution), or
   * alternatively the LFU counter. */
  if (server.maxmemory_policy & MAXMEMORY_FLAG_LFU) {
    o->lru = (LFUGetTimeInMinutes()<<8) | LFU_INIT_VAL;
  } else {
    o->lru = LRU_CLOCK();
  }
  return o;
}

SDS空间分配策略

  • 空间预分配:在字符串长度小于1MB之前,扩容空间采用加倍策略。当大于等于1MB之后,每次扩容只会分配1MB大小的空间。
  • 惰性空间释放:惰性空间释放用于字符串缩短的操作。当字符串缩短是,程序并不是立即使用内存重分配来回收缩短出来的字节,而是使用free属性记录起来,并等待将来使用。
// SDS_MAX_PREALLOC == 1MB,如果修改后的长度小于1M,则分配的空间是原来的2倍,否则增加1MB的空间
if (newlen < SDS_MAX_PREALLOC)
    newlen *= 2;
else
    newlen += SDS_MAX_PREALLOC;
2. List 列表对象

List的编码方式有两种:ziplist、quicklist(开始的时候用的是LINKEDLIST,但是最新版的Redis已经不用了。/*No longer used: old list encoding*/)。

Redis 的 List 内部是通过 quicklist 实现的:quicklist 是一个双向链表,quicklist 的每个节点都是一个 ziplist。

list-max-ziplist-size 就是用于配置 quicklist 中的每个节点的 ziplist 的大小。 当这个值配置为正数时表示 quicklist 每个节点的 ziplist 所包含的元素个数是一个确定的数量。当 list-max-ziplist-size 为负数时表示限制每个 ziplist 的大小
具体有以下含义
list-max-ziplist-size = -5:最大 64 kb //正常环境不推荐
list-max-ziplist-size = -4:最大 32 kb
list-max-ziplist-size = -3:最大 16 kb
list-max-ziplist-size = -2:最大 8 kb //默认配置
list-max-ziplist-size = -1:最大 4kb

3. Hash 哈希对象

哈希对象的键是一个字符串类型,值是一个键值对集合。
哈希对象的编码可以是 ziplist 或者 hashtable。

redis.conf的配置:
hash-max-ziplist-entries 512
hash-max-ziplist-value 64

编码转换

和List列表对象使用 ziplist 编码一样,当同时满足下面两个条件时,使用ziplist(压缩列表)编码:

  1. 哈希对象保存的数量小于512个
  2. 保存键(field)或值(value)的字符串对象长度小于64字节

不能满足这两个条件的时候转成使用 hashtable 编码。

4. Set 集合对象

集合对象的编码可以是 intset 或者 hashtable。
集合中的元素是无序的,集合中的元素不能有重复。

编码转换

redis.conf的配置:
set-max-intset-entries 512

当集合同时满足以下两个条件时,使用 intset 编码:

  1. 集合对象中所有元素都是整数
  2. 集合对象所有元素数量不超过512

不能满足这两个条件的就使用 hashtable 编码。

5. ZSet 有序集合对象

有序集合为每个元素设置一个分数(score)作为排序依据。
有序集合的编码可以是 ziplist 或者 skiplist。

redis.conf的配置:
zset-max-ziplist-entries 128
zset-max-ziplist-value 64

编码转换

当有序集合对象同时满足以下两个条件时,对象使用 ziplist 编码:

  1. 保存的元素数量小于128;
  2. 保存的所有元素长度都小于64字节。

不能满足上面两个条件的使用 skiplist 编码。

可以用OBJECT ENCODING key 命令来查看使用编码是什么
使用哪种编码从用户和API的角度来看,这是完全透明的。

上面可以了解字符串是怎么实现,每个数据类型有几种编码,简单的了解即可。
学习的过程中,最好把自己的总结用xmind画出来,方便复习和理解。

真正在面试被问的最多的还有2个问题:rehash和跳跃表的实现

二、应用场景

string :

  • 字符串
  • 数值: 点赞、统计、限流
  • bitmap: 统计任意用户在任意时间窗口内的登录天数、登录用户送礼物(2亿用户,礼品数要备用多少?即统计近期或环比往期的活跃用户)、【数据分析 常驻内存 临时导入Redis 统计报表】

list

  • 栈、队列
  • 评论列表

set

  • 点赞、好友等相互关系的存储

zset

  • 分页
  • 排行榜

hash

  • 商品详情页的聚合数据
  • 用户的聚合值:粉丝、点赞…

总结

基础类型还是比较简单的,花点时间记忆一下就好。
应用场景根据实际中用到的说一说就好。

猜你喜欢

转载自blog.csdn.net/Oooo_mumuxi/article/details/105857232
今日推荐