(SUB)redis数据结构和数据类型及应用

数据结构

简单动态字符串(SDS)

我们都知道Redis是用C写的,但Redis中并没有直接使用C语言传统的字符串(以空字符结尾的字符数组),而是自己构建了一种名为简单动态字符串(simple dynamic string,简称SDS)的结构,并用作Redis的默认字符串。在Redis中,C字符串只会用作字符串字面量,一些不会对字符串发生修改的地方,比如说打印日志等等。

Redis中SDS的定义:

typedef char *sds;      

struct __attribute__ ((__packed__)) sdshdr5 {     // 对应的字符串长度小于 1<<5
    unsigned char flags; /* 3 lsb of type, and 5 msb of string length */
    char buf[];
};
struct __attribute__ ((__packed__)) sdshdr8 {     // 对应的字符串长度小于 1<<8
    uint8_t len; /* used */                       //目前字符串的长度
    uint8_t alloc;                                //已经分配的总长度
    unsigned char flags;                          //flag用3bit来标明类型
    char buf[];                                   //柔性数组,以'\0'结尾
};
struct __attribute__ ((__packed__)) sdshdr16 {    // 对应的字符串长度小于 1<<16
    uint16_t len; /* used */
    uint16_t alloc; /* excluding the header and null terminator */
    unsigned char flags; /* 3 lsb of type, 5 unused bits */
    char buf[];
};
struct __attribute__ ((__packed__)) sdshdr32 {    // 对应的字符串长度小于 1<<32
    uint32_t len; /* used */
    uint32_t alloc; /* excluding the header and null terminator */
    unsigned char flags; /* 3 lsb of type, 5 unused bits */
    char buf[];
};
struct __attribute__ ((__packed__)) sdshdr64 {    // 对应的字符串长度小于 1<<64
    uint64_t len; /* used */
    uint64_t alloc; /* excluding the header and null terminator */
    unsigned char flags; /* 3 lsb of type, 5 unused bits */
    char buf[];
};

SDS和C字符串的区别

  • C字符串中使用n+1个字符数组表示n个长度的字符串,并且数组的最后一个总是‘\0’,表示该字符串结尾了。
  • C字符串获取长度时,需要遍历一遍字符串,时间复杂度为O(N),而SDS获取字符串长度时,只需要从SDS结构中取出len字段便可以,时间复杂度为O(1)
  • C字符串为其增加一段字符串时,如果没有为其分配足够的空间,则会造成缓冲区溢出;而使用我们的SDS时,则SDS会自动检查是否容量足够,不够的话就扩容,所以使用我们的SDS时不需要手动扩容,也就不会发生缓冲区溢出
  • 减少字符串带来的内存重分配次数,在C中,每次添加字符串都需要对数组扩容,删除字符串也需要内存重新分配。而SDS通过提前分配和惰性释放可以很好的改善内存重分配次数
  • 提前分配:当SDS剩余空间不足时,Redis不但会给它分配足够的空间,还会给它分配多余的空间,如果下次增加字符串时,则可以使用这部分空余的空间,减少内存重分配
  • 惰性释放:在删除一些字符时,Redis并不会立即释放空间,这样的话可以为将来的增加操作减少内存重分配的次数;于此同时,在Redis真正需要空间时,Redis也会释放掉这部分空间,不会内存泄露
  • 二进制安全:C字符串中不能包含一些空字符,否则可能会被认为是字符串结尾导致字符串截断,所以不能保存一些图片视频的二进制数据。而SDS并不是通过空字符来判断结束的,不会对内容进行任何处理,可以保存二进制数据
  • 兼容部分C字符串函数:Redis也会在结尾加上多余的一个‘\0’,使得某些情况可以使用C字符串的函数,减少自己实现重复功能

链表

typedef struct listNode {
    struct listNode *prev;
    struct listNode *next;
    void *value;
} listNode;

typedef struct list {
	//表头结点
    listNode *head;
    //表尾结点
    listNode *tail;
    //结点复制函数
    void *(*dup)(void *ptr);
    //结点释放函数
    void (*free)(void *ptr);
    //结点对比函数,通过这三个函数可以为结点值设置不同的函数,从而包含各种不同类型的值,实现多态
    int (*match)(void *ptr, void *key);
    //结点的数量
    unsigned long len;
} list;

字典(hashtable)

Redis的字典底层使用哈希表来实现,一个哈希表中有多个哈希表结点,一个哈希表结点保存了字典中的一个键值对

/* 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结构
    dictEntry **table;
    //哈希表大小
    unsigned long size;
    //哈希表大小掩码,用于计算索引值,总是等于size-1
    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;


typedef struct dict {
	//类型特定函数
    dictType *type;
    //私有数据
    void *privdata;
    //两个哈希表,用于rehash
    dictht ht[2];
    //当rehash不进行时,值为-1
    long rehashidx; /* rehashing not in progress if rehashidx == -1 */
    int16_t pauserehash; /* If >0 rehashing is paused (<0 indicates coding error) */
} dict;

type和privdata是针对不同类型的键值对,为多态而创建的
ht[2]两个哈希表是为了rehash而设计的,一般只使用ht[0]这个哈希表,ht[1]只会在对ht[0]进行rehash的时候使用
rehashidx它记录了目前rehash的进度,为-1时则说明不进行rehash
在这里插入图片描述

哈希算法

当插入一个键值对时,需要用哈希算法算出对应的索引值,并把它插入其中(具体就不再多说什么,不了解的可以去查看数据结构这门课程)

Redis计算哈希值和索引的方法:

#使用字典设置的哈希函数,计算键key 的哈希值 
hash = dict->type->hashFunction(key); 
#使用哈希表的sizemask 属性和哈希值,计算出索引值 
#根据情况不同,
ht[x] 可以是ht[0] 或者ht[1] index = hash & dict->ht[x].sizemask;

rehash

Redis使用MurmurHash2算法来计算键的哈希值,这种算法最大的优点就是当输入有规律的数时,也能平均散列到数组中

随着操作的进行,哈希表的键值对会逐渐增多或者减少,为了让哈希表的负载因子保持在一个合理的范围区间,就必须对哈希表进行扩展或者收缩。而扩展或者收缩,我们需要执行rehash(重新散列)来完成一次操作
rehash执行步骤如下:

为字典的ht[1]分配空间,大小取决于ht[0]和所执行的扩展或者收缩操作。
将ht[0]的所有键值对rehash到ht[1]上(rehash指的是重新计算哈希值和索引,重新散列到ht[1]这个哈希表中)
移动完成后,释放ht[0]的空间,将ht[1]改为ht[0],并为ht[1]重新创建一个空的哈希表,为下一次rehash准备

渐进式rehash

在渐进式rehash期间,字典进行的删除,更新,查找会在两张哈希表上进行。比如查找,redis会先在ht[0]查找,找不到才会到ht[1]上面查找

而字典进行的插入操作,则只会在ht[1]表里执行。这样的话,ht[0]表里的数量只减 不增,也减少了重复插入的操作

如果数据量比较多时,一次性移动我们的hash表,那么时间会比较久,就有可能造成redis服务停止。所以执行rehash时,并不是一次完成的,而是渐进式完成的
渐进式rehash步骤如下:

  1. 为ht[1]分配空间,让字典同时持有ht[0]和ht[1]两个哈希表
  2. 在字典中维持一个rehashidx,也就是上面的字典结构的属性,将其值设置为0,表示rehash工作开始
  3. 在rehash期间,程序除了执行指定的操作外,还会将索引为rehashidx的数据移动到ht[1]相当于将ht[0]里的数据删除,在ht[1]里面增加,当rehashidx这个索引的数据全部移动完成时,则将rehashidx值加1,直到全部完成
  4. 如果有增删改查操作时,如果index大于rehashindex,访问ht[0],否则访问ht[1]。
  5. 完成后,将rehashidx的值表示为-1,并将ht[1]设置为ht[0].

扩容的条件:

负载因子是大于1的时候。
fork为了避免写时复制,负载因子大于时候5的时候才进行扩容。
每次redis在进行正删改查的时候(hset、hdel 等操作指令时)进行rehash一次。
在没有fork操作的时候,定时的进行的rehash 1ms 步长是100次。

缩容的条件

负载因子小于0.1的时候进行缩容。当字典使用容量不足总空间的 10% 时就会触发缩容,Redis 在进行缩容时也会把 rehashindex 设置为 0,表示之后需要进行 rehash 操作。

解决键冲突

Redis使用链地址法解决键冲突,相同的哈希值会对应一个链表结构,每次有哈希冲突时,就把新的元素插入到链表的尾部

skiplist跳表

/* ZSETs use a specialized version of Skiplists */
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;

backward回退指针:
每个节点只有一个回退指针,所以每次只能回退到前一个节点,而不能跳来跳去;回退指针用于从后向前遍历
score分值:是一个double类型的浮点数,跳表中所有元素都是按分值来排序的。
ele成员对象:是一个sds的字符串对象。成员对象必须是唯一的,而分值可以是相同的。分值相同时,按成员变量的字典序排序
在这里插入图片描述
表头节点虽然都有对应的属性,但是我们是不会用到的,只是作为一个索引使后面插入,删除时更加方便

随即造层(指定高度为3)

整数集合intset

typedef struct intset {
	//编码方式
    uint32_t encoding;
    //集合数量
    uint32_t length;
    //保存元素的数组
    int8_t contents[];
} intset;

encoding编码方式表示数组用什么类型来保存
length集合数量保存了集合中的数量
contents[]集合数组虽然是类型是int8_t,但是实际取决于encoding编码方式。数组内是大小有序的,从小到大,不含重复值

数组升级

当对数组中添加新元素时,如果新添加的元素类型大于原来的数组的类型,则需要对数组进行升级。
升级步骤如下:

  • 按新的类型扩展原有数组大小,并为新元素分配空间
  • 将原来的数组元素转换为新的元素类型,并且保存有序
  • 将新添加的元素加入数组

不能降级

压缩列表(ziplist)

下图是压缩列表的各个组成部分:
在这里插入图片描述
各个字段含义如下:
在这里插入图片描述
entry:

在这里插入图片描述

previous_entry_length:表示前一个节点的长度,用于从尾向前遍历;如果前一个节点长度小于254个字节,那么就用1字节来保存,如果大于等于254字节,就用5字节来保存。
encoding:记录了content保存的数据类型和长度
content:保存节点的值,类型和长度由encoding类型决定

MySQL:“为什么说 ziplist 省内存?”

  1. 与 linkedlist 相比,少了 prev、next 指针。
  2. 通过 encoding 字段针对不同编码来细化存储,尽可能做到按需分配,当 entry 存储的是 int 类型时,encoding 和 entry-data 会合并到 encoding ,省掉了 entry-data 字段。
  3. 每个 entry-data 占据内存大小不一样,为了解决遍历问题,增加了 prevlen 记录上一个 entry 长度。遍历数据时间复杂度是 O(1),但是数据量很小的情况下影响不大。

连锁更新

那我们考虑一种情况,如果每一个节点的长度都在靠近254字节。那么新插入了一个大于等于254字节的节点,那么下一个节点的头部就必须是5个字节了(本来previous_entry_length只用一个字节),所以程序将对压缩列表执行空间重分配操作,将该节点扩容至5字节。但是该节点现在扩容后又大于254字节了,所以后面的节点又要接着扩容。当然,删除一个节点也可能造成这种情况:比如说第一个节点长度大于等于254,第二个节点长度小于254,那么后面一个节点previous_entry_length保存的就是1字节,那我们把第二个节点删除,现在就又变成刚才的情况!Redis把这种特殊情况造成的连续更新叫做连锁更新。因此:从 Redis7.0 后, hash 默认使用 listpack 结构

quicklist(linkedlist)

插入元素:

【1】判断head节点ziplist是否已满,_quicklistNodeAllowInsert函数中根据quicklist.fill属性判断节点是否已满。
【2】head节点未满,直接调用ziplistPush函数,插入元素到ziplist中。
【3】更新quicklistNode.sz属性。
【4】head节点已满,创建一个新节点,将元素插入新节点的ziplist中,再将该节点头插入quicklist中。

typedef struct quicklist { // src/quicklist.h
    quicklistNode *head;
    quicklistNode *tail;
    unsigned long count;        /* ziplist 的个数 */
    unsigned long len;          /* quicklist 的节点数 */
    unsigned int compress : 16; /* LZF 压缩算法深度 */
    //...
} quicklist;

typedef struct quicklistNode {
    struct quicklistNode *prev;
    struct quicklistNode *next;
    unsigned char *zl;           /* 对应的 ziplist */
    unsigned int sz;             /* ziplist 字节数 */
    unsigned int count : 16;     /* ziplist 个数 */
    unsigned int encoding : 2;   /* RAW==1 or LZF==2 */
    unsigned int container : 2;  /* NONE==1 or ZIPLIST==2 */
    unsigned int recompress : 1; /* 该节点先前是否被压缩 */
    unsigned int attempted_compress : 1; /* 节点太小无法压缩 */
    //...
} quicklistNode;

typedef struct quicklistLZF {
    unsigned int sz; 
    char compressed[];
} quicklistLZF;

quicklist 添加操作对应函数是 quicklistPush,源码如下:

void quicklistPush(quicklist *quicklist, void *value, const size_t sz,
                   int where) {
    if (where == QUICKLIST_HEAD) {
        // 在列表头部添加元素
        quicklistPushHead(quicklist, value, sz);
    } else if (where == QUICKLIST_TAIL) {
        // 在列表尾部添加元素
        quicklistPushTail(quicklist, value, sz);
    }
}


int quicklistPushHead(quicklist *quicklist, void *value, size_t sz) {
    quicklistNode *orig_head = quicklist->head;
    if (likely(
            _quicklistNodeAllowInsert(quicklist->head, quicklist->fill, sz))) {
        // 在头部节点插入元素
        quicklist->head->zl =
            ziplistPush(quicklist->head->zl, value, sz, ZIPLIST_HEAD);
        quicklistNodeUpdateSz(quicklist->head);
    } else {
        // 头部节点不能继续插入,需要新建 quicklistNode、ziplist 进行插入
        quicklistNode *node = quicklistCreateNode();
        node->zl = ziplistPush(ziplistNew(), value, sz, ZIPLIST_HEAD);
        quicklistNodeUpdateSz(node);
        // 将新建的 quicklistNode 插入到 quicklist 结构中
        _quicklistInsertNodeBefore(quicklist, quicklist->head, node);
    }
    quicklist->count++;
    quicklist->head->count++;
    return (orig_head != quicklist->head);
}




参数说明:

entry:quicklistEntry结构,quicklistEntry.node指定元素插入的quicklistNode节点,quicklistEntry.offset指定插入ziplist的索引位置。
after:是否在quicklistEntry.offset之后插入。
【1】根据参数设置以下标志。

full:待插入节点ziplist是否已满。
at_tail:是否ziplist尾插。
at_head:是否ziplist头插。
full_next:后驱节点是否已满。
full_prev:前驱节点是否已满。
REDIS_STATIC void _quicklistInsert(quicklist *quicklist, quicklistEntry *entry,
                                   void *value, const size_t sz, int after) {
    int full = 0, at_tail = 0, at_head = 0, full_next = 0, full_prev = 0;
    int fill = quicklist->fill;
    quicklistNode *node = entry->node;
    quicklistNode *new_node = NULL;
    ...
    // [1]
    if (!_quicklistNodeAllowInsert(node, fill, sz)) {
        full = 1;
    }

    if (after && (entry->offset == node->count)) {
        at_tail = 1;
        if (!_quicklistNodeAllowInsert(node->next, fill, sz)) {
            full_next = 1;
        }
    }

    if (!after && (entry->offset == 0)) {
        at_head = 1;
        if (!_quicklistNodeAllowInsert(node->prev, fill, sz)) {
            full_prev = 1;
        }
    }
    // [2]
    ...
}


quicklist 元素删除分为两种情况:单一元素删除和区间元素删除,它们都位于 src/quicklist.c 文件中。

1、单一元素删除

单一元素的删除函数是 quicklistDelEntry,源码如下:

void quicklistDelEntry(quicklistIter *iter, quicklistEntry *entry) {
    quicklistNode *prev = entry->node->prev;
    quicklistNode *next = entry->node->next;
    // 删除指定位置的元素
    int deleted_node = quicklistDelIndex((quicklist *)entry->quicklist,
                                         entry->node, &entry->zi);
    //...
}

可以看出 quicklistDelEntry 函数的底层,依赖 quicklistDelIndex 函数进行元素删除。

2、区间元素删除

区间元素删除的函数是 quicklistDelRange,源码如下:

// start 表示开始删除的下标,count 表示要删除的个数
int quicklistDelRange(quicklist *quicklist, const long start,
                      const long count) {
    if (count <= 0)
        return 0;
    unsigned long extent = count; 
    if (start >= 0 && extent > (quicklist->count - start)) {
        // 删除的元素个数大于已有元素
        extent = quicklist->count - start;
    } else if (start < 0 && extent > (unsigned long)(-start)) {
        // 删除指定的元素个数
        extent = -start; /* c.f. LREM -29 29; just delete until end. */
    }
    //...
    // extent 为剩余需要删除的元素个数,
    while (extent) {
        // 保存下个 quicklistNode,因为本节点可能会被删除
        quicklistNode *next = node->next;
        unsigned long del;
        int delete_entire_node = 0;
        if (entry.offset == 0 && extent >= node->count) {
            // 删除整个 quicklistNode
            delete_entire_node = 1;
            del = node->count;
        } else if (entry.offset >= 0 && extent >= node->count) {
           // 删除本节点的所有元素
            del = node->count - entry.offset;
        } else if (entry.offset < 0) {
            // entry.offset<0 表示从后向前,相反则表示从前向后剩余的元素个数
            del = -entry.offset;
            if (del > extent)
                del = extent;
        } else {
            // 删除本节点部分元素
            del = extent;
        }
        D("[%ld]: asking to del: %ld because offset: %d; (ENTIRE NODE: %d), "
          "node count: %u",
          extent, del, entry.offset, delete_entire_node, node->count);
        if (delete_entire_node) {
            __quicklistDelNode(quicklist, node);
        } else {
            quicklistDecompressNodeForUse(node);
            node->zl = ziplistDeleteRange(node->zl, entry.offset, del);
            quicklistNodeUpdateSz(node);
            node->count -= del;
            quicklist->count -= del;
            quicklistDeleteIfEmpty(quicklist, node);
            if (node)
                quicklistRecompressOnly(quicklist, node);
        }
        // 剩余待删除元素的个数
        extent -= del;
        // 下个 quicklistNode
        node = next;
        // 从下个 quicklistNode 起始位置开始删除
        entry.offset = 0;
    }
    return 1;
}

从上面代码可以看出,quicklist 在区间删除时,会先找到 start 所在的 quicklistNode,计算删除的元素是否小于要删除的 count,如果不满足删除的个数,则会移动至下一个 quicklistNode 继续删除,依次循环直到删除完成为止。

quicklistDelRange 函数的返回值为 int 类型,当返回 1 时表示成功的删除了指定区间的元素,返回 0 时表示没有删除任何元素。

 quicklist插入流程表格化:

Redis核心原理与实践--列表实现原理之quicklist结构 - binecy - 博客园 (cnblogs.com)

条件条件说明处理方式
!full && after待插入节点未满,ziplist尾插再次检查ziplist插入位置是否存在后驱元素,如果不存在则调用ziplistPush函数插入元素(更快),否则调用ziplistInsert插入元素
!full && !after待插入节点未满,非ziplist尾插调用ziplistInsert函数插入元素
full && at_tail && node -> next && !full_next && after待插入节点已满,尾插,后驱节点未满将元素插入后驱节点ziplist中
full && at_head && node -> prev && !full_prev && !after待插入节点已满,ziplist头插,前驱节点未满将元素插入前驱节点ziplist中
full && ((at_tail && node -> next && full_next && after) ||(at_head && node->prev && full_prev && !after))满足以下条件:(1)待插入节点已满 (2)尾插且后驱节点已满,或者头插且前驱节点已满构建一个新节点,将元素插入新节点,并根据after参数将新节点插入quicklist中
full待插入节点已满,并且在节点ziplist中间插入将插入节点的数据拆分到两个节点中,再插入拆分后的新节点中

MySQL:“ziplist听起来很完美,为啥还搞什么 quicklist ”

既要又要还要的需求是很难实现的,ziplist 节省了内存,但是也有不足。

  • 不能保存过多的元素,否则查询性能会大大降低,O(N) 时间复杂度。
  • ziplist 存储空间是连续的,当插入新的 entry 时,内存空间不足就需要重新分配一块连续的内存空间,引发连锁更新的问题。

quicklist 是综合考虑了时间效率与空间效率引入的新型数据结构。结合了原先 linkedlist 与 ziplist 各自的优势,本质还是一个链表,只不过链表的每个节点是一个 ziplist。

数据结构定义在 quicklist.h 文件中,链表由 quicklist 结构体定义,每个节点由 quicklistNode 结构体定义(源码版本为 6.2,7.0 版本使用 listpack 取代了 ziplist)。

quicklist 是一个双向链表,所以每个 quicklistNode 都有前序指针(*prev)、后序指针(*next)。每个节点是 ziplist,所以还有一个指向 ziplist 的指针 *zl。

typedef struct quicklistNode {    // 前序节点指针    struct quicklistNode *prev;    // 后序节点指针    struct quicklistNode *next;    // 指向 ziplist 的指针    unsigned char *zl;    // ziplist 字节大小    unsigned int sz;    // ziplst 元素个数    unsigned int count : 16;    // 编码格式,1 = RAW 代表未压缩原生ziplist,2=LZF 压缩存储    unsigned int encoding : 2;    // 节点持有的数据类型,默认值 = 2 表示是 ziplist    unsigned int container : 2;    // 节点持有的 ziplist 是否经过解压, 1 表示已经解压过,下一次操作需要重新压缩。    unsigned int recompress : 1;    // ziplist 数据是否可压缩,太小数据不需要压缩    unsigned int attempted_compress : 1;    // 预留字段    unsigned int extra : 10;} quicklistNode;

quicklist 作为链表,定义了 头、尾指针,用于快速定位表表头和链表尾。

typedef struct quicklist {    // 链表头指针    quicklistNode *head;    // 链表尾指针    quicklistNode *tail;    // 所有 ziplist 的总 entry 个数    unsigned long count;    // quicklistNode 个数    unsigned long len;    int fill : QL_FILL_BITS;    unsigned int compress : QL_COMP_BITS;    unsigned int bookmark_count: QL_BM_BITS;    // 柔性数组,给节点添加标签,通过名称定位节点,实现随机访问的效果    quicklistBookmark bookmarks[];} quicklist;

结构

结合 quicklist 和 quicklistNode定义,quicklist 链表结构如下图所示。

从结构上看,quicklist 就是 ziplist 的升级版,优化的关键点在于控制好每个 ziplist 的大小或者元素个数。

  • quicklistNode 的 ziplist 越小,可能会造成更多的内存碎片,极端情况下是每个 ziplist 只有一个 entry,退化成了 linkedlist
  • quicklistNode 的 ziplist 过大,极端情况下一个 quicklist 只有一个 ziplist,退化成了 ziplist。连锁更新的性能问题就会暴露无遗。

合理配置很重要,Redis 提供了 list-max-ziplist-size(default: -2),

当 list-max-ziplist-size 为负数时表示限制每个 quicklistNode 的 ziplist 的内存大小,超过这个大小就会使用 linkedlist 存储数据,每个值有以下含义:

  • -5:每个 quicklist 节点上的 ziplist 大小最大 64 kb <--- 正常环境不推荐
  • -4:每个 quicklist 节点上的 ziplist 大小最大 32 kb <--- 不推荐
  • -3:每个 quicklist 节点上的 ziplist 大小最大 16 kb <--- 可能不推荐
  • -2:每个 quicklist 节点上的 ziplist 大小最大 8 kb <--- 不错
  • -1:每个 quicklist 节点上的 ziplist 大小最大 4kb <--- 不错

默认值为 -2,也是官方最推荐的值,当然你可以根据自己的实际情况进行修改。

 listpack结构

ziplist由于存在对前一个entry大小的记录导致存在连锁更新问题,因此7.0引入listpack完全替代所有的ziplist结构(eg:quicklist=listpack + linkedlist)

listpack 也是一种紧凑型数据结构,用一块连续的内存空间来保存数据,并且使用多种编码方式来表示不同长度的数据来节省内存空间。

源码文件 listpack.h对 listpack 的解释:A lists of strings serialization format,意思是一种字符串列表的序列化格式,可以把字符串列表进行序列化存储,可以存储字符串或者整形数字。

  • tot-bytes,也就是 total bytes,占用 4 字节,记录 listpack 占用的总字节数。
  • num-elements,占用 2 字节,记录 listpack elements 元素个数。
  • elements,listpack 元素,保存数据的部分。
  • listpack-end-byte,结束标志,占用 1 字节,值固定为 255。

相比ziplist最大变化:element 不再像 ziplist 的 entry 保存前一项的长度

  • encoding-type,元素的编码类型,会不同长度的整数和字符串编码。
  • element-data,实际存放的数据。
  • element-tot-len,encoding-type + element-data 的总长度,不包含自己的长度。

每个 element 只记录自己的长度,不像 ziplist 的 entry,记录上一项的长度。当修改或者新增元素的时候,不会影响后续 element 的长度变化,解决了连锁更新的问题。

数据类型

Redis——底层数据结构原理_51CTO博客_redis底层数据结构

字符串(strings), 散列(hashes), 列表(lists), 集合(sets), 有序集合(sorted sets) 与范围查询, bitmaps, hyperloglogs 和 地理空间(geospatial)

Memcached没有Redis那么多数据类型,数据都是以字符串(可以用JSON转换)的形式表示。

Redis存在那么多数据类型的意义是什么?

如果客户端想要获得JSON中的某一小部分元素,可以只取其一小部分,直接返回给客户端。
而Memcached需要全量地返回整个JSON而不能去解析它的一部分,需要客户端自己去解析。Memcached的性能损耗会在IO以及客户端数据的解析上。

因此,重要的不是类型,重要的是Redis Server对每种数据类型都实现了自己的方法(函数)。
本质上是一种解耦,计算向数据移动。

redisObject

redis中每个对象用redisObject表示:

typedef struct redisObject {
	//类型
    unsigned type:4;
    //编码方式
    unsigned encoding:4;
    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;

type

可以使用type命令显示键对应值的数据类型
在这里插入图片描述

ptr指针

指向底层实现的数据结构

encoding

编码方式:可通过object encoding xxx来看

/* Objects encoding. Some kind of objects like Strings and Hashes can be
 * internally represented in multiple ways. The 'encoding' field of the object
 * is set to one of this fields for this object. */
#define OBJ_ENCODING_INT 1     /* Encoded as integer */
#define OBJ_ENCODING_EMBSTR 8  /* Embedded sds string encoding */
#define OBJ_ENCODING_RAW 0     /* Raw representation */
#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_QUICKLIST 9 /* Encoded as linked list of ziplists */
#define OBJ_ENCODING_STREAM 10 /* Encoded as a radix tree of listpacks */

字符串strings

编码:

#define OBJ_ENCODING_INT 1     /* Encoded as integer */
#define OBJ_ENCODING_EMBSTR 8  /* Embedded sds string encoding */
#define OBJ_ENCODING_RAW 0     /* Raw representation */

int

存储8个字节的长整型(long,2的63次方-1),那么字符串对象会将整数值保存在字符串里的ptr属性里面,将void*转换为long,并将字符串编码设置为int

embstr

在 Redis 中,如果 SDS 的存储值大于 64 字节时,Redis 的内存分配器会认为此对象为大字符串,并使用 raw 类型来存储,当数据小于 64 字节时(字符串类型),会使用 embstr 类型存储。既然内存分配器的判断标准是 64 字节,那为什么 embstr 类型和 raw 类型的存储判断值是 44 字节?

embstr格式的SDS = redisObject结构 + sdshdr结构

程序员小乐, 细说 redis 十种数据类型及底层原理

为什么时44字节

从 SDS 的源码可以看出,SDS 的存储类型一共有 5 种:SDSTYPE5、SDSTYPE8、SDSTYPE16、SDSTYPE32、SDSTYPE64,在这些类型中最小的存储类型为 SDSTYPE5,但 SDSTYPE5 类型会默认转成 SDSTYPE8,以下源码可以证明,如下图所示:

https://s2.51cto.com/images/blog/202302/09095934_63e453863947599121.png?x-oss-process=image/watermark,size_16,text_QDUxQ1RP5Y2a5a6i,color_FFFFFF,t_30,g_se,x_10,y_10,shadow_20,type_ZmFuZ3poZW5naGVpdGk=/format,webp/resize,m_fixed,w_1184

struct __attribute__ ((__packed__)) sdshdr8 {
    uint8_t len; // 1 byte
    uint8_t alloc; // 1 byte
    unsigned char flags; // 1 byte
    char buf[];
};

可以看出除了内容数组(buf)之外,其他三个属性分别占用了 1 个字节,最终分隔字符等于 64 字节,减去 redisObject 的 16 个字节,再减去 SDS 自身的 3 个字节,再减去结束符 ​​\0​​ 结束符占用 1 个字节,最终的结果是 44 字节(64-16-3-1=44),内存占用如下图所示:

https://s2.51cto.com/images/blog/202302/09095934_63e453867d91061956.png?x-oss-process=image/watermark,size_16,text_QDUxQ1RP5Y2a5a6i,color_FFFFFF,t_30,g_se,x_10,y_10,shadow_20,type_ZmFuZ3poZW5naGVpdGk=/format,webp/resize,m_fixed,w_1184

raw

存储大于44字节的字符串,需要调用两次内存分配函数

程序员小乐, 细说 redis 十种数据类型及底层原理

Redis浮点数

也是用字符串值来表示的,需要用时先把它转为浮点数,计算完后又转为字符串值保存

命令

Redis 字符串(String) | 菜鸟教程 (runoob.com)

set k1 value1 nx nx表示无值的时候才执行成功

set k1 value1 xx xx表示有值的时候才执行成功

mset k3 value3 k4 value4 含义为 more set,用来设置多个值

mget k1 k2 获取两个值

msetnx k2 c k3 d 这个命令可以保证多笔操作是原子操作,类似于一个事务中

appenk k1 "world" 在k1后面追加world

getrange k1 2 5 获取k1某两个索引之间的字符串子串(支持正负索引)
在这里插入图片描述

setrange k1 offset value从某个索引开始覆盖

strlen k1 获取字符串长度

getset k1 bashibing 取出旧值,并将旧值设置为新值(这样可以减少一次IO通信)

数值类型的加减操作

decr k1               k1-1

decrby k1 22                                k1+22

incrbyfloat k1 0.5       k1-0.5

查看key类型

type key

查看编码类型

object encoding xxx

应用

在这里插入图片描述

二进制安全

redis 是二进制安全的:并不会去破坏你的编码,也不去关心你是什么编码。底层存储的时候,是按照Byte字节存储的。我们前面看到的encoding,只是为了让加减之类的运算方法变得更快一些。
(HBASE也是二进制安全的)

在redis进程与外界交互的时候,redis存储的是字节流,而不会转换成字符流,也不会擅自按照某种数据类型存储,这样保证了数据不会被破坏,不会发生数据被截断/溢出等错误。

编码并不会影响数据的存储
因此,在多人使用redis的时候,我们一定要在用户端约定好数据的编码和解码。

在这里插入图片描述

————————————————
版权声明:本文为CSDN博主「寒泉Hq」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/sinat_42483341/article/details/106931121

bitmap

在这里插入图片描述

bitop按位操作

在这里插入图片描述

应用:

公司有用户系统,让你统计用户登录天数,且时间窗口随机。例如,A用户在某一年中登陆了几次。怎么优化?
可以使用redis实现,假设一年400天,让每一天对应一个二进制位,需要50个字节即可。
setbit Tom 1 1 表示 Tom 在第2天登录了一次(下标从0开始)
setbit Tom 364 1 表示 Tom 在第365天登录了一次
bitcount Tom -2 -1 统计 Tom 在最后16天的总登录次数
你是京东,618做活动,登录就送礼物,假设京东有2亿用户。大库应该备货多少礼物?
用户应该分为:僵尸用户、冷热用户、忠诚用户
你需要统计活跃用户,也是随机时间窗口
比如说,统计本月1号~3号范围内的活跃过的用户,需要去重
在这里插入图片描述

list

quicklist实现,有序(插入顺序)

命令

Redis 列表(List) | 菜鸟教程 (runoob.com)

应用场景

list 类型的应用场景主要是实现队列和栈,比如:

  • 消息队列,利用 lpush 和 rpop 命令实现生产者消费者模式。

lpush k1 a b c d e左边push

rpop k1 a b c d e右边pop(先进先出)

  • 最新消息,利用 lpush 和 ltrim 命令实现固定长度的时间线。

  • 历史记录,利用 lpush 和 lrange 命令实现浏览记录或者搜索记录。

  • lpush k1 a b c d e左边push
     lpop k1 a b c d e左边pop(后进先出)

散列hashes

  • hash-max-ziplist-entries :表示当 hash 中的元素数量小于或等于该值时,使用 ziplist 编码,否则使用 hashtable 编码。 ziplist 是一种压缩列表,它可以节省内存空间,但是访问速度较慢。 hashtable 是一种哈希表,它可以提高访问速度,但是占用内存空间较多。默认值为 512 。
  • hash-max-ziplist-value :表示当 hash 中的每个元素的 key 和 value 的长度都小于或等于该值时,使用 ziplist 编码,否则使用 hashtable 编码。默认值为 64 。

在这里插入图片描述

每次添加都会添加都末尾,添加时,需要遍历一遍整个压缩列表,如果有key和插入的key相同,则将它的value替换,否则就添加到末尾。

在这里插入图片描述

命令

Redis 哈希(Hash) | 菜鸟教程 (runoob.com)

使用场景

哈希字典的典型使用场景如下:

  • 商品购物车,购物车非常适合用哈希字典表示,使用人员唯一编号作为字典的 key,value 值可以存储商品的 id 和数量等信息;
  • 存储用户的属性信息,使用人员唯一编号作为字典的 key,value 值为属性字段和对应的值;
  • 收藏,详情页,点赞

集合(sets)集合

集合对象底层可以是intset或者hashtable,如果集合对象全为整数,且数量小于等于512个,则使用intset;否则使用hashtable,无序,去重

使用intset实现:
在这里插入图片描述
使用hashtable实现:集合元素为字典的key,字典的value则为null。(类似Java的Set集合,底层也是使用map实现的,不过它的value值是一个final常量,不是null)
在这里插入图片描述

命令

Redis 集合(Set) | 菜鸟教程 (runoob.com)

应用

在这里插入图片描述

有序集合对象sorted set

有序集合对象使用ziplist或者skiplist来实现,当集合数量小于128且字符长度小于等于64字节时使用ziplist;否则使用zset(hashtable + skiplist)

除了元素本身以外,需要有分值这个维度,用来排序。
如果分值相同,则按照名称字典序排列。
每个元素都有自己的正负向索引

底层使用ziplist实现:压缩列表,示意图如下:
在这里插入图片描述
在这里插入图片描述


集合内的元素按分值大小排序

底层使用skiplist实现:这种编码的有序集合对象使用zset结构作为底层实现,一个zset结构包含一个字典和一个跳跃表

跳跃表中按分值从大到小进行保存所有元素,跳跃表节点保存一个集合元素,跳跃表的ele成员对象保存了元素的成员,跳跃表的score保存了元素的分值
在这里插入图片描述

字典中也保存了集合的全部元素,字典的键是元素成员,字典的值是元素分值

注意:有序集合的每个元素成员是一个字符串,分值是double类型的浮点数。
并且虽然看起来是保存了两份,但是他们相同元素通过指针指向的都是同一个地方,所以并不会有冗余;

为什么要同时使用跳跃表和字典呢??

虽然用单独的一种都可以实现有序集合的功能,但是两个都各有利弊,使用两个的话就相当于同时保留了他们的优点,摒弃了缺点。

比如我们只使用字典来实现,那么它查找一个元素的分值复杂度为O(1),但是如果范围查找的话,就需要先排序,时间复杂度为O(nlogn),空间复杂度为O(n)

比如我们只使用跳跃表来实现,那么它可以很快的支持范围查找,但它查找一个分值的时间复杂度为O(logn)。

而范围查找和只查询分值在有序集合是很常见的,所以综合起来,同时使用两个最优。

为什么要使用跳跃表,而不是用平衡树???

跳跃表更加简单,使用范围查找比其他的平衡树效率要高;并且是容易实现容易调试的;并且跳表插入和删除只要维护节点指针即可,不需要调整树。

那为什么Java的HashMap底层不使用跳跃表呢???不是更加简单吗??
跳表是以空间换时间的数据结构,而红黑树并不需要额外空间
并且HashMap的Entry之间并没有排序关系,无法满足跳跃表的条件。
额外:TreeMap 的并发实现 ConcurrentSkipListMap 就是使用的跳表;concurrentHashMap8 之前采用链表数据结构,8 之后是红黑树。concurrentSkipListMap 跳表,是因为要实现线程安全的 TreeMapCAS 操作会很复杂,才产生的。高并发有序。
————————————————
版权声明:本文为CSDN博主「small_engineer」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/small_engineer/article/details/124623298

 命令

Redis 有序集合(sorted set) | 菜鸟教程 (runoob.com)

 ZUNIONSTORE 示例:
在这里插入图片描述

在这里插入图片描述

 应用

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值