解密redisObject属性和五大数据类型编码:让你彻底了解redis

本文涉及的产品
云数据库 Redis 版,社区版 2GB
推荐场景:
搭建游戏排行榜
简介: 解密redisObject属性和五大数据类型编码:让你彻底了解redis



🎉 redisObject属性

Redis的一个键值对,有两个对象,一个是键对象,一个是值对象,键总是一个字符串对象,而值可以是字符串、列表、集合等对象,Redis中的值对象都是由 redisObject 结构来表示:

typedef struct redisObject{
     //表示类型:string,list,hash,set,zset
     unsigned type:4;
     //编码:比如字符串的编码有int编码,embstr编码,raw编码
     unsigned encoding:4;
     //指向底层数据结构的指针,prt是个指针变量,存放地址,指向数据存储的位置
     void *ptr;
     //引用计数,类似java里的引用计数
     int refcount;
     //记录最后一次被程序访问的时间
     unsigned lru:22;
}robj
📝 type属性

redisObject结构的第一个属性是类型(type),它用来表示当前值对象的数据类型,包括字符串(string)、列表(list)、哈希表(hash)、集合(set)和有序集合(zset)等等。这个属性是一个4位的无符号整数,所以最多可以表示16种不同类型的数据。

举个例子,如果我们向Redis中插入了一个字符串,那么这个值对象的类型就是string。当我们要从Redis中获取这个值时,Redis会根据这个类型来确定如何返回这个值。

redisObject 对象的type属性记录了对象的类型(string,list,hash,set,zset),可以通过type key命令来判断对象类型,从而区分redis中key-value的类型

127.0.0.1:6379> set testString testValue
OK
127.0.0.1:6379> lpush testList testValue1 testValue2 testValue3
(integer) 3
127.0.0.1:6379> hmset testhash 1:testvalue 2:testvalue2
OK
127.0.0.1:6379> sadd testset testvalue
(integer) 1
127.0.0.1:6379> zadd testzset 1 testvalue
(integer) 1
127.0.0.1:6379> type testString
string
127.0.0.1:6379> type testList
list
127.0.0.1:6379> type testhash
hash
127.0.0.1:6379> type testset
set
127.0.0.1:6379> type testzset
zset
📝 prt和encoding属性

edisObject结构的第三个属性是指针(ptr),它用来指向值对象的底层数据结构。这个指针可以指向不同的数据结构,比如字符串对象的底层数据结构是一个字符数组,而列表对象的底层数据结构是一个双向链表。因此,ptr属性对于值对象的类型和编码来说是非常重要的。

举个例子,如果我们要向Redis中插入一个字符串“hello world”,那么Redis会先将这个字符串转换成一个字符串对象,然后在redisObject结构的ptr属性中存储这个字符串的地址。

redisObject结构的第二个属性是编码(encoding),它用来表示当前值对象的内部编码方式,比如字符串的编码方式可以是int编码、embstr编码或者raw编码。这个属性也是一个4位的无符号整数,所以最多可以表示16种不同的编码方式。

不同的编码方式有不同的优缺点,比如raw编码可以节省空间,但是在进行字符串拼接等操作时会比较慢;而int编码虽然比较快,但是只能存储小整数。

📝 refcount 属性

Redis的redisObject结构和引用计数机制,以及内存管理和内存共享的特性,都为Redis的性能提升做出了重要贡献。Redis在通过C语言的高效运行,以及Redis的配置文件中的清除策略和内存共享特性,都为Redis的应用提供了更优秀的性能表现和更好的用户体验。

在redisObject结构中,有一个重要的属性就是引用计数(refcount)。它用于跟踪值对象的使用情况,每当这个值对象被引用一次,引用计数就会增加1。当这个值对象不再被使用时,引用计数就会减少1。引用计数的作用非常重要,它可以避免值对象被误删除或者过早删除。举个例子,假设我们有两个变量指向同一个值对象,如果这个值对象的引用计数为0时就会被删除,那么就有可能会误删除这个值对象。但是如果引用计数正确地跟踪了使用情况,就可以避免这种情况的发生。

在Redis中,内存的管理也是非常重要的。Redis采用了自己构建的内存回收机制,这也是Redis之所以能够高效运行的原因之一。创建一个新对象,redisObject对象中的refcount属性就会加1,对象被一个新程序使用,调用incrRefCount函数进行加1,如果有对象不再被应用程序使用了,那么它就会调用decrRefCount函数进行减1,当对象的引用计数值为0的时候,那么这个对象所占用的内存就会被释放。

Redis还通过在配置文件中修改相关的配置,来达到解决循环引用的问题。当内存使用达到最大值时,Redis使用的清楚策略可以通过在配置文件中修改maxmemory-policy来实现。目前,Redis提供了以下的清除策略:

  1. volatile-lru:删除已有的过期时间的key。
  2. allkeys-lru:删除所有的key。
  3. volatile-random:已有过期时间的key随机删除。
  4. allkeys-random:随机删除key。
  5. volatile-ttl:删除即将过期的key。
  6. noeviction:不删除任何key,只是返回一个写错误,这个是默认选项。对于整数值的字符串对象(例如:1、2、3等),可以实现内存共享。

内存共享是Redis一个非常重要的特性。它可以帮助系统节省内存空间,在Redis使用过程中非常有优势。它能够实现的原理就是当有两个键所引用的值相同的时候,Redis会自动把这个值当作一个对象处理,共享这个对象的引用计数。这样就可以实现一种共享内存空间的效果。但是需要注意的是,内存共享只适用于整数值的字符串。对于其他类型的字符串,内存共享会增加判断的复杂度,导致性能下降。因此,在使用内存共享时需要注意它的局限性。

📝 lru 属性

Redis在存储大量数据时,它的性能可能会下降。为了解决这个问题,Redis引入了LRU淘汰策略。

在Redis中,数据通过键值对的形式进行存储,因此对于每个键值对,都有一个对应的数据结构来表示它。Redis数据结构之一就是redisObject,这个结构中包含了所有数据类型共有的属性,例如类型标识、引用计数和值指针等等。除此之外,redisObject结构还有一个LRU属性,用于记录对象最后一次被命令程序访问的时间。

LRU(Least Recently Used)算法是一种经典的缓存淘汰算法,它的工作原理是淘汰最近最少使用的缓存对象,以便为新的缓存对象空出位置。当Redis内存占用超过了一定的阈值时,就会进行一些内存回收操作,比如删除最近未使用的值对象。因此,LRU算法是非常有用的,可以帮助Redis定期清理一些过期的值对象,从而避免内存占用过高。

具体来说,Redis将所有的键值对按照最近访问的时间排序,把最近最少使用的那些键值对淘汰掉。为了实现这个功能,Redis为每个键值对分配了一个22位的无符号整数来记录从Redis启动开始的秒数。这个整数就是LRU属性,可以用来快速比较数据对象的最后访问时间,以便实现LRU淘汰策略。

在Redis配置文件中,有三个与LRU算法相关的配置:最大内存配置maxmemory、触发数据淘汰后的淘汰策略maxmemory_policy,以及随机采样的精度maxmemory_samples。这些配置可以帮助Redis自动执行LRU淘汰策略,并避免内存占用过高的问题。

当有条件符合配置文件中三个配置的时候,继续往Redis中加key时,会触发执行 lru 策略,进行内存清除。这个时候,Redis会根据LRU属性来判断哪些键值对最近最少使用,然后将这些键值对从内存中删除。具体来说,Redis会将最近最少使用的键值对转移到链表头部,而最长时间未使用的键值对则位于链表的尾部。当链表满的时候,链表尾部的数据会被丢弃,以便给新的键值对提供空间。

在Redis配置文件中,淘汰策略(maxmemory_policy)有六个选项:Noeviction、allkeys-lru、volatile-lru、allkeys-random、key volatile-random和volatile-ttl。这些选项对应了不同的LRU淘汰策略。例如,Noeviction选项表示当缓存里的数据超过maxmemory值时,如果客户端正在执行命令,会让内存分配,给客户端返回错误响应;而allkeys-lru选项表示所有的key都用LRU进行淘汰。

举例说明:

A数据每10s访问一次,B数据每5s访问一次,C数据每50s访问一次,|代表计算空闲时间的截止点。

预测被访问的概率是B > A > C。

过期key的删除策略有两种:

惰性删除:每次获取键时,都检查键是否过期,过期的话,就删除该键;未过期,就返回该键。

定期删除:每隔一段时间,进行一次检查,删除里面的过期键。

📝 encoding属性

数据结构由 encoding 属性,也就是编码,由它来决定,可以通过object encoding key命令查看一个值对象的编码。

127.0.0.1:6379> object encoding testString
"embstr"
127.0.0.1:6379> object encoding testList
"quicklist"
127.0.0.1:6379> object encoding testhash
"ziplist"
127.0.0.1:6379> object encoding testset
"hashtable"
127.0.0.1:6379> object encoding testzset
"ziplist"
🔥 String类型编码

Redis中,String类型的数据可以有三种底层编码方式:int、raw和embstr。其中,int编码用于存储整数值,raw编码用于存储长字符串,embstr编码用于存储短字符串。每种编码的内存分配方式都不同,因此也有各自的优缺点。

int编码是用来存储整数值的。当存储的值是整数时,它会采用int编码,这样可以减少内存开销。但是,当存储的值不再是整数时,或者值的大小超过了long的范围,就会自动转化成raw编码。例如,当存储的值是1、2、3等整数时,它会采用int编码。但是,当存储的值是"a"、“b”、"c"等字符,或者值的大小超过了long的范围时,它就会自动转化成raw编码。

raw编码是用来存储长字符串的。它可以分配两次内存空间,一次是为redisObject分配内存空间,另一次是为sds分配内存空间。这两个内存空间是不连续的,因此它的缺点是内存使用效率相对较低。但是,raw编码也有优点,它的优点是可以存储超过44字节的数据,在Redis3.2版本之前,可用来存储超过39字节的数据。

embstr编码是用来存储短字符串的。它只分配一次内存空间,redisObject和sds是连续的内存,因此查询效率会快很多。但是,embstr编码也有它的缺点,当字符串增加的时候,它长度会增加,这个时候又需要重新分配内存,导致的结果就是整个redisObject和sds都需要重新分配空间,这样是会影响性能的。因此,Redis用embstr实现一次分配而后,只允许读,如果修改数据,那么它就会转成raw编码。

🔖版本区别

embstr编码版本之间的区别:在redis3.2版本之前,用来存储39字节以内的数据,在这之后用来存储44字节以内的数据。

raw编码版本之间的区别:和embstr相反,redis3.2版本之前,可用来存储超过39字节的数据,3.2版本之后,它可以存储超过44字节的数据。

🔖为什么在redis3.2版本之前,embstr编码只能存储39字节以内的数据?

首先,我们需要了解embstr是由redisObject和sdshdr两个结构体组成的一块连续内存区域。redisObject是redis对象,用于记录对象的类型、编码方式、LRU信息以及引用计数器等,而sdshdr则是简单动态字符串的结构体,里面包含了字符串的长度、空余空间大小以及数据缓冲区。

redisObject占据16个字节,其中4个字节用于记录类型,4个字节记录编码方式,24个字节用于记录LRU信息,4个字节用于记录引用计数器。而8个字节则用于记录具体的内容地址,这里假设指针是64位,需要8个字节。

struct RedisObject {
    int4 type; // 4bits,不同的redis对象会有不同的数据类型(string、list、hash等),type记录类型,会用到4bits。
    int4 encoding; // 4bits,存储编码形式,用4bits。
    int24 lru; // 24bits,用24bits记录对象的LRU信息
    int32 refcount; // 4bytes = 32bits,引用计数器,用到32bits
    void *ptr; // 8bytes,64-bit system,指针指向对象的具体内容,需要64bits
}

sdshdr则占据48个字节,其中4个字节用于记录字符串的长度,4个字节记录空余空间大小,而剩余的数据缓冲区则为字符串的实际内容。如果假设数据缓冲区中存储的是39个字节的数据,那么sdshdr的大小为8+39+1=48个字节。

struct sdshdr {
    unsigned int len;//4个字节
    unsigned int free;//4个字节
    char buf[];//假设buf里面是39个字节
};
if (ptr) {
        memcpy(sh->buf,ptr,len);
        sh->buf[len] = '\0';//一个字节

根据以上结构体的组合,我们可以计算得出,一个embstr最大占据64个字节:redisObject占据了16个字节,而sdshdr占据了48个字节(其中4个字节用于记录长度,4个字节记录空余空间大小,而剩余的数据缓冲区中存储了39个字节数据)。于是16+48(4+4+1+39)=64。

接下来,让我们来探讨为什么embstr的最小大小为33字节。同样的情况下,假设sdshdr中只有8个字节的数据缓冲区。此时sdshdr的大小为4+4+8+1=17个字节,而redisObject的大小仍然为16个字节。那么,一个embstr的大小为16+17(4+4+1+8)=33个字节。

从上面我们可以得知redisObject占16个字节,现在buf中取8字节。

struct sdshdr {
    unsigned int len;//4个字节
    unsigned int free;//4个字节
    char buf[];//假设buf里面是8个字节
};
if (ptr) {
        memcpy(sh->buf,ptr,len);
        sh->buf[len] = '\0';//一个字节

sdshdr的大小为4+4+8+1=17

计算得出:16+17(4+4+1+8)=33

8,16,32都比33字节小,所以最小分配64字节。

在字符数大于8时,会分配64字节的内存。而在字符数小于39时,也会分配64字节的内存。而为什么默认是39字节,则是因为在大多数场景中,字符串长度都在6到40个字符之间,因此redis官方默认都使用39个字节进行分配。这样分配能够避免占用过多的内存空间,同时也能保证字符串的长度不会超过64字节,从而限制内存的占用。

通过对比:

16+17(4+4+1+8)=33

16+48(4+4+1+39)=64

当字符数大于8时,会分配64字节。当字符数小于39时,会分配64字节。这个默认39就是这样来的。

🔖为什么在redis3.2版本之后,embstr编码界值由39字节会变成44字节?

每个sds都有一个sdshdr,里面的len和free记录了这个sds的长度和空闲空间。

struct sdshdr {
    unsigned int len;
    unsigned int free;

而这个len和free用的是unsigned int,它可以表示很大的范围,但是对于短的sds来说,这种方式就会浪费内存。因为unsigned int占用了8个字节的空间,短的sds可能只用了很少的空间,却占用了很多的内存。

Redis 3.2版本中,采用了不同的方式来存储sds。具体来说,采用了sdshdr8,sdshdr16,sdshdr32,和sdshdr64,分别用于不同长度的sds。sdshdr8用于短字符串的embstr,采用最小的存储方式,可以只用3个字节来存储。其他的sds则采用sdshdr16,sdshdr32和sdshdr64来存储,分别针对不同长度的字符串。

struct __attribute__ ((__packed__)) sdshdr8 {
    uint8_t len; /* used */
    uint8_t alloc; /* excluding the header and null terminator */
    char flags; /* 2 lsb of type, and 6 msb of refcount */
    char buf[];
};
struct __attribute__ ((__packed__)) sdshdr16 {
    uint16_t len; /* used */
    uint16_t alloc; /* excluding the header and null terminator */
    char flags; /* 2 lsb of type, and 6 msb of refcount */
    char buf[];
};
struct __attribute__ ((__packed__)) sdshdr32 {
    uint32_t len; /* used */
    uint32_t alloc; /* excluding the header and null terminator */
    char flags; /* 2 lsb of type, and 6 msb of refcount */
    char buf[];
};
struct __attribute__ ((__packed__)) sdshdr64 {
    uint64_t len; /* used */
    uint64_t alloc; /* excluding the header and null terminator */
    char flags; /* 2 lsb of type, and 6 msb of refcount */

在Redis 3.2版本中,为了测试这种优化方式是否可行,开发人员进行了一些测试。测试结果表明,在内存使用方面,sdshdr16,sdshdr32和sdshdr64比unsigned int更加节约内存。而且,这种优化方式并不会影响Redis的性能,反而可以提高Redis的性能。

但是,这种优化方式也有一定的缺点。首先,由于优化之后,每种sds都需要额外的sdshdr占用空间,所以总体内存使用量可能会略微增加。其次,由于使用了不同的sds存储方式,会使得一些sds的大小不再是2的整数次幂,这可能会影响内存的管理和调用。因此,在实际应用中,需要根据实际情况来选择采用哪种sds存储方式,以达到最佳的性能和内存使用效果。

因此,Redis 3.2版本中,将embstr编码界值由39字节变成44字节,是为了更好地利用优化后的sds存储方式,以达到更高的性能和更加节约的内存使用。这种优化方式可能会增加一些内存使用的开销,但是通过测试可以证明,它能够在不影响Redis性能的情况下,提高Redis的内存使用效率,从而更好地支持大规模分布式系统的应用。

sizes = sdscatprintf(sizes,"sdshdr:%d", (int)sizeof(struct sdshdr));
改成了
sizes = sdscatprintf(sizes,"sdshdr8:%d", (int)sizeof(struct sdshdr8));
sizes = sdscatprintf(sizes,"sdshdr16:%d", (int)sizeof(struct sdshdr16));
sizes = sdscatprintf(sizes,"sdshdr32:%d", (int)sizeof(struct sdshdr32));
sizes = sdscatprintf(sizes,"sdshdr64:%d", (int)sizeof(struct sdshdr64));

unsigned int占四个字节

uint8_t 占1个字节

Char 占一个字节

我们通过计算可以得出为什么优化之后会多出5个字节了,短字符串的embstr用最小的sdshdr8。

sdsdr8 = uint8_t _ 2 + char = 1_2+1 = 3

sdshdr = unsigned int _ 2 = 4 _ 2 = 8

这么一算是不是少了五个字节了,所以3.2版本更新之后,由于优化小sds的内存使用,使得原本39个字节可以多使用5个字节,这就变成了44字节了。

🔖Redis字符串最大长度是多少?

512M,查看源码可知。

static int checkStringLength(redisClient *c, long long size) {
    if (size > 512*1024*1024) {
        addReplyError(c,"string exceeds maximum allowed size (512MB)");
        return REDIS_ERR;
    }
    return REDIS_OK;
}
🔖优化手段

在Redis中,String是最常用的数据类型之一。在处理大型String类型数据时,如何优化String类型编码成为Redis调优的关键。下面是一些优化手段:

  1. 小字符串优化(Small String Optimization,SSO):当一个字符串长度小于等于44字节时,可以通过使用embstr编码来节省内存空间。Embstr编码将字符串和一个RedisObject对象存储在一起,使用一个header来描述这个RedisObject对象的类型、长度等信息。该优化方法可以减少内存碎片,并且减少因为小字符串造成的内存浪费。
  2. 短字符串编码(Short String Encoding,SSE):当一个字符串长度大于44字节、小于等于64字节时,可以通过使用raw编码来节省内存空间。Raw编码将字符串直接存储在RedisObject对象中,并且不会额外分配空间。该优化方法可以减少性能损失,因为在使用时无需解码。
  3. 压缩列表编码(Compressed List Encoding,ZL):当一个字符串对象既可以被当做字符串也可以被当做列表来使用时,可以使用压缩列表编码来节省内存空间。压缩列表编码使用一些特殊技巧来减少列表的内存使用,并且可以存储更多的元素。
  4. 整数编码(Integer Encoding):当一个字符串对象可以被解释为整数时,可以使用整数编码来节省内存空间。整数编码使用不同的格式来存储整数,例如int16_t、int32_t以及int64_t格式,以适应不同的数字大小。
  5. 对齐优化:在使用String类型时,为了提高内存读写性能,可以在数据结构中使用CPU字长对齐的技术。例如,对于64位CPU来说,使用8字节对齐可以提高内存访问效率。
🔥 List集合对象编码

列表类型是Redis中最基本的数据类型之一,它可以被用来实现栈、队列、阻塞队列等数据结构。它的底层数据结构是链表,实现这一数据类型的时候可以使用两种编码:ziplist(压缩列表)和linkedlist(双端链表)。

在Redis中,当列表保存的元素个数小于512个,每个元素的长度小于64字节的时候,触发机制会使用ziplist(压缩列表)编码,否则使用linkedlist(双端链表)编码。这一触发机制可以通过在redis的配置文件中修改配置参数进行调整:

list-max-ziplist-entries 512
list-max-ziplist-value 64

通过修改这两个配置参数,我们可以控制列表类型使用哪种编码。比如,我们想让列表保存的元素个数小于1024个并且每个元素长度小于128字节时使用ziplist(压缩列表)编码,否则使用linkedlist(双端链表)编码,我们可以将配置修改如下:

list-max-ziplist-entries 1024
list-max-ziplist-value 128

ziplist压缩列表是由一些连续的内存块组成的,有顺序的存储结构,是一种专门节约内存而开发的顺序型数据结构。在物理内存固定不变的情况下,随着内存慢慢增加会出现内存不够用的情况,这种情况可以通过调整配置文件中的二个参数,让list类型的对象尽可能的用压缩列表编码,从而达到节约内存的效果,但是也要均衡一下编码和解码对性能的影响。如果有一个几十万的列表长度进行列表压缩的话,在查询和插入的时候,进行编解码会对性能造成特别大的损耗。

如果有不可避免的长列表的存储的话,需要在代码层面配合降低Redis存储的内存。一种方法是,在存储Redis的Key的时候,在保证唯一性和可读性的基础上,尽量简化Redis的Key。这样可以有效节约Redis的存储空间。另一种方法是对长列表进行拆分,将一个包含大量元素的列表拆分成多个包含较少元素的列表,每个列表的元素个数都不超过配置文件里面的每个元素大小。这样可以保证列表使用压缩列表编码,达到节约内存的目的。

除了上面的优化方法,还可以将缓存的数据打包成二进制位和字节进行存储,比如用户的位置信息。以上海市黄浦区为例,可以将上海市和黄浦区的信息分别存储到数组或者列表里面。然后只需要存储一个包含上海市的索引0和一个包含黄浦区的索引1的01信息到Redis中,当从缓存中获取到这个01信息时,再根据数组或者列表取出真正的上海市和黄浦区信息。

最后,Redis的新版本对列表类型的数据结构进行了改造,使用quicklist代替了原有的数据结构。quicklist是ziplist和linkedlist的混合体,它将每个ziplist连接起来,并对ziplist进行了LZF算法压缩,每个ziplist默认长度为8KB。这种改造大大提高了Redis的性能和空间利用率。

在 Redis 中,List 类型是一个双向链表,双向链表的每个节点都包含一个字符串对象,这个字符串对象就是 List 中的一个元素。对于 List 类型的编码优化,可以从以下几个方面入手。

🔖1. 压缩列表编码

在 Redis 中,List 类型还可以使用压缩列表(ziplist)来保存数据。压缩列表是在内存中实现的一个紧凑的数据结构,它可以带来较小的空间占用和高效的随机访问性能。

当 List 中的元素比较少或元素较小(小于 64 字节)时,Redis 会使用压缩列表编码。如果元素个数较多或元素较大,则会使用双向链表编码。

🔖2. 合并短链表

Redis 对于 List 类型的操作都是基于双向链表实现的。在执行 List 元素的添加、删除、插入等操作时,如果链表较短(长度小于 list-max-ziplist-entries 配置项),则 Redis 会将链表转换为压缩列表,从而提升性能。但是,如果链表很长,转换成压缩列表可能会导致性能下降。

因此,在对 List 进行修改时,应该尽量将多个连续的 List 元素一次性添加或删除,以减少链表长度,提高性能。

🔖3. 使用管道

在批量添加 List 元素时,可以使用 Redis 的管道特性,通过一次请求添加多个元素,从而减少网络通信开销。

例如,假设要向 List 中添加 100 个元素,可以使用以下命令:

pipe = redis.pipeline()
for i in range(100):
    pipe.rpush("mylist", str(i))
pipe.execute()

这样可以将 100 个元素一次性批量添加到 List 中,而不是使用 100 次 rpush 命令,从而减少网络通信的开销。

🔖4. 避免频繁的操作

List 是 Redis 中常用的数据结构之一,但是如果使用不当,也会带来性能问题。

例如,如果在 List 中频繁添加、删除、插入元素,会导致链表长度不断变化,从而降低性能。因此,在使用 List 时,应该尽量减少频繁的操作,合并操作,避免浪费资源。

🔥 Hash对象编码

哈希类型是一种数据类型,在计算机编程中被广泛使用。相比于字符串类型,在消耗内存和CPU方面更小。其中,哈希的编码方式有两种:ziplist编码和hashtable编码。

当元素的数量小于512个且每个元素的长度小于64字节时,我们可以使用ziplist编码。这个编码方式相当于一种压缩列表。压缩列表可以将多个元素压缩成一个,从而节省内存空间。例如,我们有一个列表,其中有3个元素:{“apple”, “banana”, “cherry”}。使用ziplist编码后,我们可以将它们压缩成一个二进制字符串,例如"applebananaorange"。这样,我们就可以节省内存,同时还可以通过一些算法快速地查找和访问每个元素。

但是当元素数量超过512个或者每个元素长度大于64字节时,ziplist编码就不再适用了。这时候,我们可以使用hashtable编码。这个编码方式类似于一个字典,其中的键是字符串对象,值是null。当我们需要添加或查找元素时,我们可以通过字典的键值对快速地获取元素。例如,我们有一个hashtable,其中有三个元素:{“apple”: null, “banana”: null, “cherry”: null}。当我们需要添加一个新的元素"date"时,我们只需要将它当做键添加进字典即可。这个过程非常快速,可以在O(1)的时间复杂度内完成。

那么,如何选择ziplist或者hashtable编码呢?这个取决于元素数量和每个元素的长度。如果元素数量小于512个且每个元素长度小于64字节,就使用ziplist编码。否则,使用hashtable编码。我们可以通过修改set-max-intset-entries参数来改变元素数量的阈值。例如,我们可以将这个参数设置为1024,这样就可以将元素数量阈值提高到1024个。

ziplist是一种紧凑但有限的编码方式,适用于小型Hash对象,而hashtable则适用于大型Hash对象。

下面是Redis调优之Hash对象编码优化手段:

  1. 确定Hash对象的大小:在选择Hash对象的编码方式时,我们需要确定这个Hash对象的大小。如果Hash对象的元素数量较少,则可以使用ziplist编码;如果Hash对象的元素数量超过一定限制,则必须使用hashtable编码。
  2. 配置哈希对象的压缩参数:对于使用ziplist编码的Hash对象,可以通过修改配置文件中的hash-max-ziplist-entries和hash-max-ziplist-value参数来进行压缩。这些参数定义了ziplist编码的Hash对象中元素的最大数量和最大长度。
  3. 合并Hash对象:对于使用hashtable编码的Hash对象,如果元素数目很少,可以通过使用命令HSET和HDEL来删除或添加元素,从而把Hash对象压缩为ziplist编码。如果元素数目增加,可以使用命令HINCRBY或者HMSET来合并多个小的Hash对象为一个大的Hash对象。
  4. 使用适当的Hash函数:对于使用hashtable编码的Hash对象,需要使用一个合适的Hash函数来分散元素的位置。如果Hash函数不好,会导致多个元素存储在哈希表的同一位置,从而影响性能。Redis中默认使用的Hash函数是MurmurHash2,这是一种高效的Hash函数。
  5. 把Hash对象拆分为多个Hash对象:对于超大型Hash对象,可以考虑将其拆分为多个小型Hash对象,从而减少单个Hash对象的大小。这样做可以提高性能并减轻Redis的内存压力。
🔥 Set集合对象编码

Set类型是 Redis 数据类型中的一种,它可以实现很多有趣的功能,例如抽奖小程序、点赞、收藏、加标签、关注模型等等。同时,Set类型还有两种编码方式,一种是 intset 编码,另一种是 hashtable 编码。

首先,我们来看看 intset 编码。intset 编码使用整数集合作为底层实现,可以用来保存元素数量不超过512个的整型数据。整数集合中的元素是唯一的,不会存在重复元素,这也是 Set 类型的一个特点。

举个例子,比如我们要设计一个点赞功能,我们可以使用 Set 类型来保存被点赞的用户 ID。如果我们使用 intset 编码,那么就可以在 Set 中保存最多512个用户 ID。

另一种编码方式是 hashtable 编码。hashtable 编码可以类比 HashMap 的实现,HashTable 类中存储的实际数据是 Entry 对象,数据结构与 HashMap 是相同的。hashtable 编码可以用来存储数量超过512个的元素。

假设我们要设计一个收藏功能,我们可以使用 Set 类型来保存用户收藏的文章 ID。如果用户收藏的文章数量超过了512个,那么就会自动使用 hashtable 编码来保存数据。

除了以上提到的功能,Set 类型还可以用来实现抽奖小程序。比如我们要设计一个抽奖功能,我们可以使用 Set 类型来保存参与抽奖的用户 ID。在抽奖的时候,我们可以随机从 Set 中选择一个用户来获得奖品。

同时,Set 类型也可以用来加标签和关注模型。比如我们要设计一个加标签的功能,我们可以使用 Set 类型来保存所有打上该标签的对象。在关注模型中,我们可以使用 Set 类型来保存用户关注的对象 ID。

不过需要注意的是,Set 类型中的所有元素都必须是整型数据。如果要保存其他类型的数据,可以考虑使用其他的 Redis 数据类型,例如 String 类型、List 类型、Hash 类型等等。

在 Redis 中,Set 集合对象是通过哈希表实现的,该哈希表的元素都是指向集合中元素的指针,这些指针实际上都是 Redis 对象。因此,Set 集合的编码优化主要围绕以下两个方面进行:

🔖1. 对象的大小优化

因为 Set 集合中的元素指针都是 Redis 对象,因此,如果元素被编码为 Intset(整数集合)或者 ZipList(压缩列表),就可以避免对每个元素都创建一个 Redis 对象。这些编码方式相比于 HashTable(哈希表),可以更有效地使用内存,从而减少内存开销。

🔖2. 哈希表大小优化

Set 集合对象的哈希表大小对于集合的性能和内存占用都会产生很大的影响。因此,在需要存储大量元素的集合中,应该采取一些措施来优化哈希表的大小。以下是几种哈希表大小优化的手段:

  • rehashing:将哈希表大小扩大到两倍,从而减少哈希冲突,提高查询效率;
  • shrink:在哈希表元素数量不断减少的情况下,缩小哈希表的大小,从而减少内存占用;
  • ziplist memory reuse:通过重复利用 Ziplist 中空闲的内存空间,避免频繁地重新分配内存,从而提高性能和内存效率;

这里简单介绍一下如何使用以上方法进行 Set 集合对象编码优化:

🔖1. 对象的大小优化

一般情况下,Redis 会自动选择最优的编码方式来存储 Set 集合中的元素。但是,如果我们知道元素是整数或者用字符串表示的数字,我们可以手动将元素编码为 Intset 或者 ZipList,以减少内存开销。

例如,以下命令可以将 Set 集合 myset 中的所有元素都转换为整数集合:

127.0.0.1:6379> SMEMBERS myset
1) "foo"
2) "bar"
3) "baz"
127.0.0.1:6379> SINTERSTORE myset myset
(integer) 3
127.0.0.1:6379> OBJECT ENCODING myset
"hashtable"
127.0.0.1:6379> SPOP myset
"foo"
127.0.0.1:6379> SPOP myset
"bar"
127.0.0.1:6379> SPOP myset
"baz"
127.0.0.1:6379> SADD myset 1 2 3
(integer) 3
127.0.0.1:6379> SMEMBERS myset
1) "1"
2) "2"
3) "3"
127.0.0.1:6379> OBJECT ENCODING myset
"intset"

可以看到,由于元素都是整数,将 myset 编码为 Intset,内存占用从 488 字节减少到 24 字节,大大优化了内存占用。

🔖2. 哈希表大小优化

Set 集合对象的哈希表大小对于集合的性能和内存占用都会产生很大的影响,因此,在需要存储大量元素的集合中,应该采取一些措施来优化哈希表的大小。以下是几种哈希表大小优化的手段:

  • rehashing

rehashing 是一种动态扩展哈希表大小的方法。当哈希表的元素数量超过了负载因子(0.75)和哈希表大小的乘积时,Redis 就会自动扩展哈希表大小。这种方法能够减少哈希冲突,提高查询效率。

例如,以下命令可以手动触发 rehashing:

127.0.0.1:6379> CONFIG SET hash-max-zipmap-entries 64
OK
127.0.0.1:6379> SADD myset 1 2 3 4 5 6 7 8 9 10
(integer) 10
127.0.0.1:6379> OBJECT ENCODING myset
"ziplist"
127.0.0.1:6379> SADD myset 11 12 13 14 15 16 17 18 19 20
(integer) 10
127.0.0.1:6379> OBJECT ENCODING myset
"hashtable"

可以看到,当 Set 集合 myset 中元素数量达到 11 个时,Redis 自动将 myset 的编码方式从 Ziplist 切换为 Hashtable,从而扩大了哈希表的大小。

  • shrink

shrink 是一种动态缩小哈希表大小的方法。当哈希表的元素数量不断减少,但是哈希表的大小仍然很大时,Redis 就可以采用 shrink 方法减小哈希表的大小,从而减少内存占用。

例如,以下命令可以手动触发 shrink:

127.0.0.1:6379> SADD myset 1 2 3 4 5 6 7 8 9 10
(integer) 10
127.0.0.1:6379> OBJECT ENCODING myset
"hashtable"
127.0.0.1:6379> SREM myset 1 2 3 4 5 6 7 8 9 10
(integer) 10
127.0.0.1:6379> OBJECT ENCODING myset
"hashtable"
127.0.0.1:6379> SREM myset 11 12
(integer) 2
127.0.0.1:6379> OBJECT ENCODING myset
"ziplist"

可以看到,当元素数量从 10 个减少到 0 个时,myset 的编码方式依然为 Hashtable。但是,当元素数量从 2 个减少到 0 个时,myset 的编码方式就被自动切换为 Ziplist,从而减少了哈希表的大小。

  • ziplist memory reuse

ziplist memory reuse 是一种重复利用 Ziplist 中空闲的内存空间的方法,避免频繁地重新分配内存,从而提高性能和内存效率。

例如,以下命令可以手动触发 ziplist memory reuse:

127.0.0.1:6379> SADD myset foo bar baz
(integer) 3
127.0.0.1:6379> OBJECT ENCODING myset
"hashtable"
127.0.0.1:6379> SREM myset foo
(integer) 1
127.0.0.1:6379> OBJECT ENCODING myset
"hashtable"
127.0.0.1:6379> SREM myset bar
(integer) 1
127.0.0.1:6379> OBJECT ENCODING myset
"ziplist"

可以看到,当元素数量减少到 2 个时,myset 的编码方式从 Hashtable 切换为 Ziplist。在切换后,Ziplist 中原来的内存空间并没有被释放,而是继续被重复利用,从而避免了频繁地重新分配内存。

🔥 Zset有序集合对象编码

Redis是一个内存型数据库,其中有序集合(Sorted Set)是其中的一种数据结构,它能够按照元素的分值(score)进行排序,并且能够快速地执行范围查询,因此被广泛应用于排行榜等实时计算场景中。有序集合的编码方式有两种:ziplist和skiplist。

当有序集合的元素数量小于128,且所有元素长度小于64字节时,Redis采用ziplist编码方式。ziplist底层使用压缩列表实现,相邻的两个节点分别保存成员和分值。元素按照分值从小到大排序,小的放在靠近表头的位置,大的放在靠近表尾的位置。

当有序集合的元素数量超过了128,或者有序集合中至少有一个元素长度大于64字节时,Redis采用skiplist编码方式。skiplist包含一个字典和一个跳跃表,字典的键保存元素的值,字典的值保存元素的分值;跳跃表由zskiplistNode和skiplist两个结构,跳跃表skiplist中的object属性保存元素的成员,score 属性保存元素的分值。这两种数据结构会通过指针来共享相同元素的成员和分值,因此不会产生重复成员和分值,造成内存的浪费。

那么为什么Redis需要两种不同的编码方式呢?如果单独使用字典,虽然能直接通过字典的值查找成员的分值,但是因为字典是以无序的方式来保存集合元素,所以每次进行范围操作的时候都要进行排序;如果单独使用跳跃表来实现,虽然能执行范围操作,但是查找操作就会变慢,因此Redis采用了两种数据结构来共同实现有序集合。

除了成员和分值之外,skiplist还有一个重要的属性:层数。跳跃表基于有序链表的,在链表上建索引,每两个节点提取一个结点到上一级,每个跳跃表节点的层高都是1至32之间的随机数。这样一来,当有大量的节点需要插入或者删除时,跳跃表会根据需要自动增加或者删除索引节点,以保证性能的稳定和可靠。

在跳跃表中,插入一个新的节点时,需要从最高层开始和前一个节点的下一个节点进行比较,如果新节点应该插入到前一个节点和下一个节点之间,则在当前层插入新节点。如果当前层插入成功,则通过抛硬币的方式决定是否将新节点提取到更高一层。如果抛到正面就继续提取到更高一层,直到抛到反面为止。这样,当大量的新节点通过逐层比较,最终插入到原链表之后,上层的索引节点会慢慢变得不够用,这时就会根据需要自动增加索引节点。

总之,有序集合是Redis非常重要的数据结构之一,它能够支持按照分值进行排序和范围查询,因此具有非常广泛的应用场景。同时,Redis中的有序集合采用了两种不同的编码方式,分别是ziplist和skiplist,通过动态地调整索引节点,可以在保证查询性能的同时,降低内存的使用。

🔖zset优化方法
  1. 使用有序集合代替普通Set

普通Set只能存储成员,而有序集合可以存储成员和对应的分数。因此,使用有序集合可以在一些应用中取代普通Set,减少内存占用,提高性能。

  1. 调整zset元素数量

由于zset在元素数量小于128个时采用ziplist编码,而在大于128个时采用skiplist编码,因此,为了避免zset的编码方式频繁变化,可以根据实际情况调整zset的元素数量。

  1. 使用合适的分数

分数在zset中起到非常重要的作用,它可以用来排序和筛选元素。分数的类型可以是浮点数或整数,但一般情况下建议使用整数,因为整数比浮点数的存储和比较都要快。此外,为了避免产生过多的小数位,可以采用将分数乘以一个固定因子的方式来存储分数。

  1. 避免使用大的zset

在实际场景中,zset的元素数量可能会非常大,尤其是在涉及到排行榜、热门帖子等场景时。为了避免出现大的zset导致内存溢出,可以考虑对zset进行分片存储,或采用异步写入数据库等方式来优化处理。

  1. 避免频繁的zset操作

由于zset是一种有序数据结构,所以在使用时建议采用批量操作的方式,例如使用zadd、zrange等命令进行操作。为了避免频繁的zset操作,可以考虑将操作缓存起来,然后批量执行。

  1. 采用合适的数据结构存储zset

如果有多个zset,并且它们的元素数量不多,可以考虑采用hash方式来存储zset,而不是使用Redis自带的zset数据结构。因为zset的编码方式和其他数据结构不一样,所以在存储多个zset时,可能会产生内存碎片,从而降低Redis的性能。而hash可以通过自定义编码方式来减少内存碎片的产生。


相关实践学习
基于Redis实现在线游戏积分排行榜
本场景将介绍如何基于Redis数据库实现在线游戏中的游戏玩家积分排行榜功能。
云数据库 Redis 版使用教程
云数据库Redis版是兼容Redis协议标准的、提供持久化的内存数据库服务,基于高可靠双机热备架构及可无缝扩展的集群架构,满足高读写性能场景及容量需弹性变配的业务需求。 产品详情:https://www.aliyun.com/product/kvstore     ------------------------------------------------------------------------- 阿里云数据库体验:数据库上云实战 开发者云会免费提供一台带自建MySQL的源数据库 ECS 实例和一台目标数据库 RDS实例。跟着指引,您可以一步步实现将ECS自建数据库迁移到目标数据库RDS。 点击下方链接,领取免费ECS&RDS资源,30分钟完成数据库上云实战!https://developer.aliyun.com/adc/scenario/51eefbd1894e42f6bb9acacadd3f9121?spm=a2c6h.13788135.J_3257954370.9.4ba85f24utseFl
相关文章
|
25天前
|
存储 消息中间件 NoSQL
Redis数据类型详解:选择合适的数据结构优化你的应用
Redis数据类型详解:选择合适的数据结构优化你的应用
|
3月前
|
存储 NoSQL 算法
Redis的三种特殊数据类型
【1月更文挑战第6天】Redis的三种特殊数据类型
36 1
|
3月前
|
设计模式 NoSQL Java
常用的设计模式以及操作Redis、MySQL数据库、各种MQ、数据类型转换的方法
常用的设计模式以及操作Redis、MySQL数据库、各种MQ、数据类型转换的方法
|
3月前
|
SQL NoSQL 定位技术
Redis基本命令和常用数据类型
Redis基本命令和常用数据类型
136 0
|
4天前
|
存储 NoSQL Redis
第十八章 Redis查看配置文件和数据类型
第十八章 Redis查看配置文件和数据类型
12 0
|
26天前
|
存储 XML NoSQL
Redis支持哪些数据类型?
Redis提供五种数据类型:String(支持JSON、XML等序列化,最大512MB),Hash(键值对,适合存储对象),List(有序列表,可在两端添加元素),Set(无序唯一元素集合),以及Sorted Set(有序集合,元素带分数排序)。每种类型有特定应用场景,优化了数据操作效率。
8 0
|
1月前
|
存储 消息中间件 NoSQL
Redis 常见数据类型(对象类型)和应用案列
接下来,让我们走进 Redis 的对象世界,Redis 5.0版本就已经支持了下面的 9 种类型,分别是 :字符串对象、列表对象、哈希对象、集合对象、有序集合对象、Bitmaps 对象、HyperLogLog 对象、Geospatial 对象、Stream对象。
Redis 常见数据类型(对象类型)和应用案列
|
1月前
|
存储 NoSQL Redis
Redis新数据类型-Bitmaps
Redis新数据类型-Bitmaps
|
1月前
|
存储 NoSQL Java
【Redis】1、学习 Redis 的五大基本数据类型【String、Hash、List、Set、SortedSet】
【Redis】1、学习 Redis 的五大基本数据类型【String、Hash、List、Set、SortedSet】
54 0
|
2月前
|
NoSQL Redis
redis五大数据类型及其常用命令(详细)
redis五大数据类型及其常用命令(详细)
22 0