基本数据类型与存储原理

String字符串

        redis 是KV 的数据库,是通过hashtable 实现的。每个键值对都会有一个dictEntry,里面指向了key 和value 的指针。next 指向下一个dictEntry。set hello word 为例

typedef struct dictEntry {
    void *key; /* key 关键字定义*/

    union {
        void *val; uint64_t u64; /* value 定义*/
        int64_t s64; double d;
    } v;

    struct dictEntry *next; /* 指向下一个键值对节点*/
} dictEntry;

        

            key 是字符串,是存储在自定义的SDS中。value 不是直接存储在SDS 中,而是存储在redisObject 中。实际上五种常用的数据类型的任何一种,都是通过redisObject 来存储的。

redisObject

typedef struct redisObject {
    unsigned type:4; /* 对象的类型,包括:OBJ_STRING、OBJ_LIST、OBJ_HASH、OBJ_SET、OBJ_ZSET */
    unsigned encoding:4; /* 具体的数据结构*/
    unsigned lru:LRU_BITS; /* 24 位,对象最后一次被命令程序访问的时间,与内存回收有关*/
    int refcount; /* 引用计数。当refcount 为0 的时候,表示该对象已经不被任何对象引用,则可以进行垃圾回收了*/
    void *ptr; /* 指向对象实际的数据结构*/
} robj;

内部编码

   

  字符串类型的内部编码有三种:
  1、int存储8 个字节的长整型(long,2^63-1)。
  2、embstr, 代表embstr 格式的SDS(Simple Dynamic String 简单动态字符串),存储小于44 个字节的字符串。
  3、raw,存储大于44 个字节的字符串
/* object.c */
#define OBJ_ENCODING_EMBSTR_SIZE_LIMIT 44

问题:embstr 和raw 的区别?

  embstr 的使用只分配一次内存空间(因为RedisObject 和SDS 是连续的),而raw需要分配两次内存空间(分别为RedisObject 和SDS 分配空间)。
  因此与raw 相比,embstr 的好处在于创建时少分配一次空间,删除时少释放一次空间,以及对象的所有数据连在一起,寻找方便。
  而embstr 的坏处也很明显,如果字符串的长度增加需要重新分配内存时,整个RedisObject 和SDS 都需要重新分配空间,因此Redis 中的embstr 实现为只读。

Hash实现原理

  内层的哈希底层可以使用两种数据结构实现:

  ziplist:OBJ_ENCODING_ZIPLIST(压缩列表)
  hashtable:OBJ_ENCODING_HT(哈希表)

ziplist 压缩列表

  ziplist 是一个经过特殊编码的双向链表,不存储指向上一个链表节点和指向下一个链表节点的指针,而是存储上一个节点长度和当前节点长度,通过牺牲部分读写性能,来换取高效的内存空间利用率,是一种时间换空间的思想。只用在字段个数少,字段值小的场景里面。ziplist 的内部结构

  

typedef struct zlentry {
    unsigned int prevrawlensize; /* 上一个链表节点占用的长度*/
    unsigned int prevrawlen; /* 存储上一个链表节点的长度数值所需要的字节数*/
    unsigned int lensize; /* 存储当前链表节点长度数值所需要的字节数*/
    unsigned int len; /* 当前链表节点占用的长度*/
    unsigned int headersize; /* 当前链表节点的头部大小(prevrawlensize + lensize),即非数据域的大小*/
    unsigned char encoding; /* 编码方式*/
    unsigned char *p; /* 压缩链表以字符串的形式保存,该指针指向当前节点起始位置*/
} zlentry;

  编码encoding(ziplist.c 源码第204 行)

#define ZIP_STR_06B (0 << 6) //长度小于等于63 字节
#define ZIP_STR_14B (1 << 6) //长度小于等于16383 字节
#define ZIP_STR_32B (2 << 6) //长度小于等于4294967295 字节

  

什么时候使用ziplist 存储?

  当hash 对象同时满足以下两个条件的时候,使用ziplist 编码:
  1)所有的键值对的健和值的字符串长度都小于等于64byte(一个英文字母一个字节);
  2)哈希对象保存的键值对数量小于512 个。
/* src/redis.conf 配置*/
hash-max-ziplist-value 64 // ziplist 中最大能存放的值长度
hash-max-ziplist-entries 512 // ziplist 中最多能存放的entry 节点数量

  一个哈希对象超过配置的阈值(键和值的长度有>64byte,键值对个数>512 个)时,会转换成哈希表(hashtable)

/* 源码位置:t_hash.c ,当达字段个数超过阈值,使用HT 作为编码*/
if (hashTypeLength(o) > server.hash_max_ziplist_entries)
    hashTypeConvert(o, OBJ_ENCODING_HT);

/*源码位置: t_hash.c,当字段值长度过大,转为HT */
for (i = start; i <= end; i++) {
    if (sdsEncodedObject(argv[i]) && sdslen(argv[i]->ptr) > server.hash_max_ziplist_value){
        hashTypeConvert(o, OBJ_ENCODING_HT);
        break;
    }
}

hashtable(dict)

  hashtable 被称为字典(dictionary),它是一个数组+链表的结构。Redis 的KV 结构是通过一个dictEntry 来实现的。Redis 又对dictEntry 进行了多层的封装。

typedef struct dictEntry {
    void *key; /* key 关键字定义*/
    union {
        void *val; uint64_t u64; /* value 定义*/
        int64_t s64; double d;
    } v;

    struct dictEntry *next; /* 指向下一个键值对节点*/
} dictEntry;

  dictEntry 放到了dictht(hashtable 里面):

/* This is our hash table structure. Every dictionary has two of this as we
* implement incremental rehashing, for the old to the new table. */
typedef struct dictht {
    dictEntry **table; /* 哈希表数组*/
    unsigned long size; /* 哈希表大小*/
    unsigned long sizemask; /* 掩码大小,用于计算索引值。总是等于size-1 */
    unsigned long used; /* 已有节点数*/
} dictht;

  ht 放到了dict 里面:

typedef struct dict {
    dictType *type; /* 字典类型*/
    void *privdata; /* 私有数据*/
    dictht ht[2]; /* 一个字典有两个哈希表*/
    long rehashidx; /* rehash 索引*/
    unsigned long iterators; /* 当前正在使用的迭代器数量*/
} dict;

  最底层到最高层dictEntry——dictht——dict——OBJ_ENCODING_HT,哈希的存储结构

 

  注意:dictht 后面是NULL 说明第二个ht 还没用到。dictEntry*后面是NULL 说明没有hash 到这个地址。dictEntry 后面是NULL 说明没有发生哈希冲突

为什么要定义两个哈希表呢?ht[2]
  redis 的hash 默认使用的是ht[0],ht[1]不会初始化和分配空间。
  哈希表dictht 是用链地址法来解决碰撞问题的。在这种情况下,哈希表的性能取决于它的大小(size 属性)和它所保存的节点的数量(used 属性)之间的比率:

  • 比率在1:1 时(一个哈希表ht 只存储一个节点entry),哈希表的性能最好;
  • 如果节点数量比哈希表的大小要大很多的话(这个比例用ratio 表示,5 表示平均一个ht 存储5 个entry),那么哈希表就会退化成多个链表,哈希表本身的性能优势就不再存在。
    在这种情况下需要扩容。Redis 里面的这种操作叫做rehash。
    rehash 的步骤:
    1、为字符ht[1]哈希表分配空间,这个哈希表的空间大小取决于要执行的操作,以及ht[0]当前包含的键值对的数量。
    扩展:ht[1]的大小为第一个大于等于ht[0].used*2。
    2、将所有的ht[0]上的节点rehash 到ht[1]上,重新计算hash 值和索引,然后放入指定的位置。
    3、当ht[0]全部迁移到了ht[1]之后,释放ht[0]的空间,将ht[1]设置为ht[0]表,并创建新的ht[1],为下次rehash 做准备。

 什么时候触发扩容?
  负载因子(源码位置:dict.c):

static int dict_can_resize = 1;
static unsigned int dict_force_resize_ratio = 5;

  ratio = used / size,已使用节点与字典大小的比例

  dict_can_resize 为1 并且dict_force_resize_ratio 已使用节点数和字典大小之间的比率超过1:5,触发扩容

购物车

  key:用户id;field:商品id;value:商品数量。
  +1:hincr。-1:hdecr。删除:hdel。全选:hgetall。商品数:hlen。

List原理
    统一用quicklist 来存储。quicklist 存储了一个双向链表,每个节点都是一个ziplist。quicklist(快速列表)是ziplist 和linkedlist 的结合体。
typedef struct quicklist {
    quicklistNode *head; /* 指向双向链表的表头*/
    quicklistNode *tail; /* 指向双向链表的表尾*/
    unsigned long count; /* 所有的ziplist 中一共存了多少个元素*/
    unsigned long len; /* 双向链表的长度,node 的数量*/
    int fill : 16; /* fill factor for individual nodes */
    unsigned int compress : 16; /* 压缩深度,0:不压缩; */
} quicklist;

  quicklistNode 中的*zl 指向一个ziplist,一个ziplist 可以存放多个元素

typedef struct quicklistNode {
struct quicklistNode *prev; /* 前一个节点*/
struct quicklistNode *next; /* 后一个节点*/
unsigned char *zl; /* 指向实际的ziplist */
unsigned int sz; /* 当前ziplist 占用多少字节*/
unsigned int count : 16; /* 当前ziplist 中存储了多少个元素,占16bit(下同),最大65536 个*/
unsigned int encoding : 2; /* 是否采用了LZF 压缩算法压缩节点,1:RAW 2:LZF */
unsigned int container : 2; /* 2:ziplist,未来可能支持其他结构存储*/
unsigned int recompress : 1; /* 当前ziplist 是不是已经被解压出来作临时使用*/
unsigned int attempted_compress : 1; /* 测试用*/
unsigned int extra : 10; /* 预留给未来使用*/
} quicklistNode;

  

  

用户消息时间线timeline

  因为List 是有序的,可以用来做用户时间线

Set存储原理

  Redis 用intset 或hashtable 存储set。如果元素都是整数类型,就用inset 存储。如果不是整数类型,就用hashtable(数组+链表的存来储结构)。

  问题:KV 怎么存储set 的元素?key 就是元素的值,value 为null。
  如果元素个数超过512 个,也会用hashtable 存储。
配置文件redis.conf
set-max-intset-entries 512

抽奖

  随机获取元素 spop myset

点赞、签到、打卡

  这条微博的ID 是t1001,用户ID 是u3001。
  用like:t1001 来维护t1001 这条微博的所有点赞用户。
  点赞了这条微博:sadd like:t1001 u3001
  取消点赞:srem like:t1001 u3001
  是否点赞:sismember like:t1001 u3001
  点赞的所有用户:smembers like:t1001
  点赞数:scard like:t1001

商品标签

  sadd tags:i5001 画面清晰细腻
  sadd tags:i5001 真彩清晰显示屏
  sadd tags:i5001 流畅至极

商品筛选

  获取差集
  获取交集
  获取并集

ZSET原理

  满足以下条件时使用ziplist 编码:

  • 元素数量小于128 个
  • 所有member 的长度都小于64 字节

  在ziplist 的内部,按照score 排序递增来存储。插入的时候要移动之后的数据。

对应redis.conf 参数:
zset-max-ziplist-entries 128
zset-max-ziplist-value 64

  超过阈值之后,使用skiplist+dict 存储。

  

   所有新增加的指针连成了一个新的链表,但它包含的节点个数只有原来的一半(上图中是7, 19, 26)。在插入一个数据的时候,决定要放到那一层,取决于一个算法(在redis 中t_zset.c 有一个zslRandomLevel 这个方法)。

      查找数据的时候,先沿着这个新链表进行查找。当碰到比待查数据大的节点时,再回到原来的链表中的下一层进行查找。比如,我们想查找23,查找的路径是沿着下图中标红的指针所指向的方向进行的:
       
  • 23 首先和7 比较,再和19 比较,比它们都大,继续向后比较。
  • 但23 和26 比较的时候,比26 要小,因此回到下面的链表(原链表),与22比较。
  • 23 比22 要大,沿下面的指针继续向后和26 比较。23 比26 小,说明待查数据23 在原链表中不存在

应用场景

排行榜
id 为6001 的新闻点击数加1:zincrby hotNews:20190926 1 n6001
获取今天点击最多的15 条:zrevrange hotNews:20190926 0 15 withscores

 

 
  

  

  
  
 

 

posted on 2021-08-08 11:18  溪水静幽  阅读(303)  评论(0)    收藏  举报