MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

Redis对象类型与编码的动态调整

2021-09-257.3k 阅读

Redis对象类型基础

Redis是一个开源的、基于内存的数据结构存储系统,常被用作数据库、缓存和消息中间件。它支持多种数据结构,每种数据结构都以对象的形式存在于Redis中。Redis中主要的对象类型包括字符串(string)、哈希(hash)、列表(list)、集合(set)和有序集合(zset)。

字符串(string)

字符串类型是Redis中最基本的数据类型,它可以存储任何形式的字符串,包括二进制数据。例如,我们可以存储一个简单的文本字符串:

127.0.0.1:6379> SET mykey "Hello, Redis!"
OK

在这个例子中,我们使用SET命令将键mykey的值设置为字符串Hello, Redis!。字符串类型的对象在Redis内部可以有不同的编码方式,这取决于存储的数据内容。

哈希(hash)

哈希类型用于存储字段和值的映射,类似于编程语言中的字典。例如,我们可以存储一个用户信息的哈希:

127.0.0.1:6379> HMSET user:1 name "John" age 30 city "New York"
OK

这里我们使用HMSET命令创建了一个键为user:1的哈希对象,其中包含nameagecity三个字段及其对应的值。

列表(list)

列表类型是一个有序的字符串元素集合,可以通过索引访问元素,支持在列表两端进行插入和删除操作。例如,我们可以创建一个任务列表:

127.0.0.1:6379> RPUSH tasks "task1" "task2" "task3"
(integer) 3

上述命令使用RPUSH将三个任务添加到名为tasks的列表中。

集合(set)

集合类型是一个无序的字符串元素集合,集合中的元素是唯一的,不允许重复。例如,我们可以创建一个包含不同颜色的集合:

127.0.0.1:6379> SADD colors "red" "green" "blue"
(integer) 3

这里通过SADD命令向名为colors的集合中添加了三个颜色元素。

有序集合(zset)

有序集合与集合类似,但每个元素都关联一个分数(score),根据分数对元素进行排序。例如,我们可以创建一个排行榜:

127.0.0.1:6379> ZADD leaderboard 100 "player1" 200 "player2" 150 "player3"
(integer) 3

此命令使用ZADD向名为leaderboard的有序集合中添加了三个玩家及其对应的分数。

Redis对象编码

Redis为了高效地存储不同类型的数据,针对每种对象类型都设计了多种编码方式。编码方式的选择会影响内存的使用和操作的性能。

字符串对象编码

字符串对象有三种编码方式:intembstrraw

  • int编码:当字符串对象保存的是整数值,并且这个整数值可以用long类型来表示时,Redis会使用int编码。例如:
127.0.0.1:6379> SET num 1234567890
OK
127.0.0.1:6379> OBJECT ENCODING num
"int"
  • embstr编码:当字符串对象保存的是长度小于等于39字节的字符串时,Redis会使用embstr编码。embstr编码将对象头和字符串内容连续存储在一块内存中,减少内存碎片,提高内存利用率。例如:
127.0.0.1:6379> SET short_str "This is a short string"
OK
127.0.0.1:6379> OBJECT ENCODING short_str
"embstr"
  • raw编码:当字符串对象保存的字符串长度大于39字节,或者字符串对象在创建时无法直接使用embstr编码(例如使用APPEND命令追加字符串导致长度超过39字节),则会使用raw编码。raw编码下,对象头和字符串内容是分开存储的。例如:
127.0.0.1:6379> SET long_str "This is a very long string that exceeds 39 bytes. This is a very long string that exceeds 39 bytes. This is a very long string that exceeds 39 bytes."
OK
127.0.0.1:6379> OBJECT ENCODING long_str
"raw"

哈希对象编码

哈希对象有两种编码方式:ziplisthashtable

  • ziplist编码:当哈希对象的所有键值对的键和值的长度都比较小时,并且哈希对象包含的键值对数量也比较少(默认小于等于512个),Redis会使用ziplist编码。ziplist是一种紧凑的、连续的内存结构,它可以高效地存储小的键值对。例如:
127.0.0.1:6379> HMSET small_hash key1 "value1" key2 "value2"
OK
127.0.0.1:6379> OBJECT ENCODING small_hash
"ziplist"
  • hashtable编码:当哈希对象的键值对数量超过512个,或者有至少一个键或值的长度超过64字节时,Redis会将哈希对象的编码转换为hashtablehashtable是一种基于哈希表的数据结构,适合存储大量的键值对,查询效率高。例如:
127.0.0.1:6379> HMSET large_hash key1 "value1" key2 "value2" ... (more than 512 key - value pairs)
OK
127.0.0.1:6379> OBJECT ENCODING large_hash
"hashtable"

列表对象编码

列表对象有两种编码方式:ziplistlinkedlist

  • ziplist编码:当列表对象保存的所有字符串元素的长度都比较小,并且列表对象包含的元素数量也比较少(默认小于等于512个)时,Redis会使用ziplist编码。例如:
127.0.0.1:6379> RPUSH small_list "a" "b" "c"
(integer) 3
127.0.0.1:6379> OBJECT ENCODING small_list
"ziplist"
  • linkedlist编码:当列表对象保存的元素数量超过512个,或者有至少一个元素的长度超过64字节时,Redis会将列表对象的编码转换为linkedlistlinkedlist是一种双向链表结构,适合存储大量的元素,但内存开销相对较大。例如:
127.0.0.1:6379> RPUSH large_list "a" "b" "c" ... (more than 512 elements)
(integer) 513
127.0.0.1:6379> OBJECT ENCODING large_list
"linkedlist"

集合对象编码

集合对象有两种编码方式:intsethashtable

  • intset编码:当集合对象保存的所有元素都是整数值,并且集合对象包含的元素数量比较少(默认小于等于512个)时,Redis会使用intset编码。intset是一种紧凑的整数集合结构,用于高效存储整数。例如:
127.0.0.1:6379> SADD int_set 1 2 3
(integer) 3
127.0.0.1:6379> OBJECT ENCODING int_set
"intset"
  • hashtable编码:当集合对象包含的元素数量超过512个,或者有至少一个元素不是整数值时,Redis会将集合对象的编码转换为hashtable。例如:
127.0.0.1:6379> SADD mixed_set 1 "a" 2
(integer) 3
127.0.0.1:6379> OBJECT ENCODING mixed_set
"hashtable"

有序集合对象编码

有序集合对象有两种编码方式:ziplistskiplist

  • ziplist编码:当有序集合对象保存的元素数量比较少(默认小于等于128个),并且所有成员的长度都比较小(默认小于等于64字节)时,Redis会使用ziplist编码。在ziplist中,元素按照分数从小到大排列。例如:
127.0.0.1:6379> ZADD small_zset 10 "member1" 20 "member2"
(integer) 2
127.0.0.1:6379> OBJECT ENCODING small_zset
"ziplist"
  • skiplist编码:当有序集合对象保存的元素数量超过128个,或者有至少一个成员的长度超过64字节时,Redis会将有序集合对象的编码转换为skiplistskiplist是一种可以快速查找元素的有序数据结构,结合了哈希表和链表的优点,查询效率较高。例如:
127.0.0.1:6379> ZADD large_zset 10 "member1" 20 "member2" ... (more than 128 members)
(integer) 129
127.0.0.1:6379> OBJECT ENCODING large_zset
"skiplist"

Redis对象编码的动态调整

Redis会根据对象的实际情况,在运行时动态地调整对象的编码方式,以优化内存使用和操作性能。

字符串对象编码的动态调整

  • embstrraw:如前文所述,当使用embstr编码的字符串对象通过APPEND命令追加字符串,导致字符串长度超过39字节时,Redis会将编码从embstr转换为raw。例如:
127.0.0.1:6379> SET short_str "This is a short string"
OK
127.0.0.1:6379> OBJECT ENCODING short_str
"embstr"
127.0.0.1:6379> APPEND short_str " that becomes long"
(integer) 33
127.0.0.1:6379> OBJECT ENCODING short_str
"raw"
  • intraw:当使用int编码的字符串对象执行一些操作,使得其值不再是可以用long类型表示的整数时,Redis会将编码转换为raw。例如:
127.0.0.1:6379> SET num 1234567890
OK
127.0.0.1:6379> OBJECT ENCODING num
"int"
127.0.0.1:6379> APPEND num "a"
(integer) 11
127.0.0.1:6379> OBJECT ENCODING num
"raw"

哈希对象编码的动态调整

  • ziplisthashtable:当使用ziplist编码的哈希对象新增键值对,导致键值对数量超过512个,或者有至少一个键或值的长度超过64字节时,Redis会将编码从ziplist转换为hashtable。例如:
127.0.0.1:6379> HMSET small_hash key1 "value1" key2 "value2"
OK
127.0.0.1:6379> OBJECT ENCODING small_hash
"ziplist"
127.0.0.1:6379> HMSET small_hash key3 "a very long value that exceeds 64 bytes a very long value that exceeds 64 bytes a very long value that exceeds 64 bytes a very long value that exceeds 64 bytes a very long value that exceeds 64 bytes a very long value that exceeds 64 bytes"
OK
127.0.0.1:6379> OBJECT ENCODING small_hash
"hashtable"

列表对象编码的动态调整

  • ziplistlinkedlist:当使用ziplist编码的列表对象新增元素,导致元素数量超过512个,或者有至少一个元素的长度超过64字节时,Redis会将编码从ziplist转换为linkedlist。例如:
127.0.0.1:6379> RPUSH small_list "a" "b" "c"
(integer) 3
127.0.0.1:6379> OBJECT ENCODING small_list
"ziplist"
127.0.0.1:6379> RPUSH small_list "a very long element that exceeds 64 bytes a very long element that exceeds 64 bytes a very long element that exceeds 64 bytes a very long element that exceeds 64 bytes a very long element that exceeds 64 bytes a very long element that exceeds 64 bytes"
(integer) 4
127.0.0.1:6379> OBJECT ENCODING small_list
"linkedlist"

集合对象编码的动态调整

  • intsethashtable:当使用intset编码的集合对象新增元素,导致元素数量超过512个,或者有至少一个元素不是整数值时,Redis会将编码从intset转换为hashtable。例如:
127.0.0.1:6379> SADD int_set 1 2 3
(integer) 3
127.0.0.1:6379> OBJECT ENCODING int_set
"intset"
127.0.0.1:6379> SADD int_set "a"
(integer) 1
127.0.0.1:6379> OBJECT ENCODING int_set
"hashtable"

有序集合对象编码的动态调整

  • ziplistskiplist:当使用ziplist编码的有序集合对象新增成员,导致成员数量超过128个,或者有至少一个成员的长度超过64字节时,Redis会将编码从ziplist转换为skiplist。例如:
127.0.0.1:6379> ZADD small_zset 10 "member1" 20 "member2"
(integer) 2
127.0.0.1:6379> OBJECT ENCODING small_zset
"ziplist"
127.0.0.1:6379> ZADD small_zset 30 "a very long member that exceeds 64 bytes a very long member that exceeds 64 bytes a very long member that exceeds 64 bytes a very long member that exceeds 64 bytes a very long member that exceeds 64 bytes a very long member that exceeds 64 bytes"
(integer) 1
127.0.0.1:6379> OBJECT ENCODING small_zset
"skiplist"

动态调整的影响与应用场景

Redis对象编码的动态调整机制使得Redis在不同的数据规模和操作场景下都能保持较好的性能和内存利用率。

性能影响

  • 读操作:一般来说,intziplistintset等紧凑编码方式在读取少量数据时性能较好,因为它们的内存结构紧凑,减少了内存访问的开销。而hashtablelinkedlistskiplist等编码方式在处理大量数据时,由于其数据结构的特性,查询效率也能得到保证。例如,对于一个小的哈希对象使用ziplist编码,读取其中一个字段的值时,直接在紧凑的ziplist结构中查找,速度较快;而对于一个大的哈希对象使用hashtable编码,通过哈希算法可以快速定位到所需的键值对。
  • 写操作:紧凑编码方式在写入少量数据时,由于内存结构简单,写入速度也较快。但当数据量增加,导致编码转换时,会有一定的性能开销。例如,当一个使用ziplist编码的列表对象不断添加元素,超过512个后转换为linkedlist编码,这个转换过程需要重新分配内存、复制数据等操作,会影响写操作的性能。

内存影响

紧凑编码方式如embstrziplistintset等,通过减少内存碎片和提高内存利用率,在存储小数据量时能有效节省内存。而当数据量增大,转换为rawhashtablelinkedlist等编码方式后,虽然可能会增加内存占用,但也能更好地适应大数据量的存储需求。例如,一个使用ziplist编码的哈希对象,由于其紧凑的内存结构,在存储少量键值对时占用内存较少;但当键值对数量增多,转换为hashtable编码后,虽然hashtable本身有一定的内存开销,但能更高效地存储大量键值对。

应用场景

  • 缓存场景:对于缓存数据,通常数据量较小且读写频繁。此时,Redis的动态编码调整机制能充分发挥作用,在数据量较小时使用紧凑编码方式节省内存,在数据量增大时自动转换编码以保证性能。例如,缓存用户的基本信息(如姓名、年龄等),一开始可能数据量小,使用ziplist编码的哈希对象存储,随着用户信息的丰富和缓存数据量的增加,自动转换为hashtable编码。
  • 排行榜场景:在排行榜应用中,有序集合是常用的数据结构。一开始用户数量较少时,使用ziplist编码的有序集合可以高效存储和查询;随着用户数量增多,超过128个时,自动转换为skiplist编码,能保证在大数据量下依然有较好的查询性能,以实时展示排行榜信息。

总结

Redis对象类型与编码的动态调整是Redis高效运行的关键机制之一。通过根据数据的实际情况动态选择合适的编码方式,Redis在内存使用和操作性能之间实现了良好的平衡。开发人员在使用Redis时,了解这一机制有助于优化应用程序的性能和内存使用。在设计数据结构和操作时,应充分考虑数据的规模和操作特点,以利用Redis的动态编码调整优势,提高应用程序的整体性能。同时,通过OBJECT ENCODING命令可以随时查看对象的当前编码方式,便于调试和性能优化。在实际应用中,还可以根据业务需求和数据特点,合理设置Redis的配置参数,进一步优化Redis的性能和内存使用。例如,通过调整哈希对象、列表对象等转换编码的阈值参数,可以更精确地控制编码转换的时机,以适应不同的应用场景。总之,深入理解Redis对象类型与编码的动态调整机制,对于开发高性能、高效内存利用的Redis应用至关重要。

附录:Redis对象结构源码分析(简要)

Redis的对象结构在源码中定义在redis.h文件中。以字符串对象为例,robj结构体是Redis对象的通用结构:

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字段表示对象类型(如字符串、哈希等),encoding字段表示对象的编码方式。对于字符串对象,当编码为int时,ptr直接指向一个long类型的整数;当编码为embstrraw时,ptr指向一个sds(简单动态字符串)结构。sds结构定义如下:

struct sdshdr {
    int len;
    int free;
    char buf[];
};

len字段表示字符串的长度,free字段表示未使用的字节数,buf数组存储实际的字符串内容。通过这些结构的组合,Redis实现了字符串对象的不同编码方式及其动态调整。哈希、列表、集合、有序集合等对象类型也都有类似的结构设计和编码转换机制,通过对源码的研究可以更深入地理解Redis对象类型与编码动态调整的本质。

希望以上内容能帮助你深入理解Redis对象类型与编码的动态调整机制。在实际开发中,结合具体业务场景合理利用这一特性,能让Redis更好地为应用程序服务。