Skip to content

五种数据结构、内部编码

更新: 2025/2/24 字数: 0 字 时长: 0 分钟

Redis有5种数据结构分别是: string(字符串)、hash(哈希)、list(列表)、set(集合)、zset(有序集合)。

QQ截图20200517153651

字符串

字符串类型是Redis最基础的数据结构,其他几种数据结构都是在字符串类型基础上构建的。

字符串类型的值实际可以是字符串(简单的字符串、复杂的字符串(例如JSON、XML))、数字(整数、浮点数),甚至是二进制(图片、音频、视频),但是值最大不能超过512MB

QQ截图20200517154010

添加、获取、删除

这里的操作命令和前面的一模一样:

添加:添加一个键值对

set 键 值

设置键为hello,值为world的键值对,返回结果为OK代表设置成功

QQ截图20220217105608

获取:获取一个键值对的值

get 键

获取键hello的值,如果要获取的键不存在,则返回nil(空)

QQ截图20220217105718

删除:删除键,返回成功删除键的个数。

del 键 [键 键 ...]

删除键hello的值,如果要删除的键不存在,则返回0

QQ截图20220217105807

批量添加、批量获取

批量添加:批量添加键值对

mset 键 值 [键 值 ...]

通过mset命令一次性设置4个键值对:

QQ截图20220217110001

批量获取:批量获取键值

mget 键 [键 ...]

批量获取了键a、b、c、d、f的值,f键不存在,那么它的值为nil(空)

QQ截图20220217110059

批量操作命令可以有效提高开发效率,假如没有批量操作命令,执行n次get命令需要按下面方式来执行:

QQ截图20200517160112

使用批量操作命令后,要执行n次get命令操作只需要按照下面方式来完成:

QQ截图20200517160213

假设网络时间为1毫秒,命令时间为0.1毫秒(按照每秒处理1万条命令算),那么执行1000次get命令和1次mget命令的区别如下:

QQ截图20200517160348

长度、切片、替换、追加

长度:获取键值长度。

strlen 键

例如,设置键hello值为redisworld,使用 strlen 命令返回值为10:

QQ截图20220217110512

?> 提示:在Redis中每个汉字的长度为3。

**切片:**开始下标从0开始计算,前闭后闭区间

getrange 键 开始下标 结束下标

取键hello值中的前两个字符:

QQ截图20220217110639

替换:替换指定位置的字符。

setrange 键 下标 替换后的字符

将hello键中的下标为0的字符替换为 p 字符:

QQ截图20220217111329

追加: 向字符串尾部追加字符串。

append 键 字符串

向键hello值后面追加 vip 字符串:

QQ截图20220217111653

时间复杂度、使用场景

QQ截图20200523155943

Redis典型的使用场景是用作缓存:Redis作为缓存层,MySQL作为存储层,绝大部分请求的数据都是从Redis中获取。由于Redis具有支撑高并发的特性,所以缓存通常能起到加速读写和降低后端压力的作用。

QQ截图20200517190922

列表

列表是一种比较灵活的数据结构,它可以充当栈和队列的角色,在实际开发上有很多应用场景。

**列表(list)类型是用来存储多个有序的字符串。列表中的每个字符串称为元素(element),一个列表最多可以存储2^32-1个元素。**在Redis中,可以对列表两端插入(push)和弹出(pop),还可以获取指定范围的元素列表、获取指定索引下标的元素等。

列表类型有两个特点:

  • 列表中的元素是有序的,这就意味着可以通过索引下标获取某个元素或者某个范围内的元素列表
  • 列表中的元素可以是重复的

QQ截图20200520230103

QQ截图20200520230332

添加、长度、获取

右添加:从右边插入一个或多个元素,返回列表长度。

rpush 键 值 [值 ...]

keylist 列表最右边依次插入元素c、b、a:

QQ截图20220217114836

左添加:从左边插入一个或多个元素,返回列表长度。

lpush 键 值 [值 ...]

keylist 列表最左边依次插入元素1、2、3:

QQ截图20220217115116

指定添加:在指定元素的前(before)或后(after)插入一个新的元素。

linsert 键 before|after 指定元素 添加元素

在列表中的元素b后面插入python:

QQ截图20220217115438

长度:计算列表长度。

llen key

当前 keylist 列表长度为7:

QQ截图20220217141058

获取:通过指定下标获取列表中的元素。

  • 下标从左到右分别是0到N-1,但是从右到左分别是-1到-N。
lindex 键 下标

获取列表最后一个元素:

QQ截图20220217120329

修改、切片、弹出、删除

修改:通过指定下标修改列表中元素的值。

  • 索引下标从左到右分别是0到N-1,但是从右到左分别是-1到-N。
lset 键 下标值 新值

假切片:获取指定范围内的元素列表,原列表不发生改变。

lrange 命令会获取列表指定索引范围所有的元素。索引下标有两个特点:

  • 索引下标从左到右分别是0到N-1,但是从右到左分别是-1到-N。
  • lrange 中索引范围是前闭后闭,这个和很多编程语言前闭后开不太相同。
lrange 键 开始下标 结束下标

获取列表的第2到第4个元素:

QQ截图20220217141839

真切片:按照索引范围修剪列表,原列表变为切片后的列表。

ltrim 键 开始下标 结束下标

左弹出:从列表左侧弹出元素。

lpop 键

将列表最左侧的元素c会被弹出,弹出后列表变为java、b、a:

QQ截图20220217135617

右弹出:从列表右侧弹出。

rpop 键

QQ截图20220217135716

右弹出左推入:从列表A的最右边弹出元素,加入到列表B的最左边。

rpoplpush 键A 键B

QQ截图20220217140030

指定删除:删除指定元素。

lrem 键 count 指定元素

lrem 命令会从列表中找到等于 value 的元素进行删除,根据count的不同分为三种情况:

  • count>0,从左到右,删除最多count个元素。
  • count<0,从右到左,删除最多count绝对值个元素。
  • count=0,删除所有。
  1. key:列表的键名。
  2. count:要移除的元素数量。
    • 如果 count 大于 0,那么命令会从列表头部开始搜索,移除最多 count 个匹配的元素。
    • 如果 count 小于 0,那么命令会从列表尾部开始搜索,移除最多 count 个匹配的元素。注意这里的“最多”是因为列表中可能没有足够多的匹配元素。
    • 如果 count 等于 0,那么命令会移除列表中所有匹配的元素。
  3. value:要移除的元素的值。

QQ截图20220217140326

时间复杂度、使用场景

QQ截图20200523165734

**列表比较典型的使用场景就是消息队列。**使用 rpushlpush 操作入队列,lpoprpop 操作出队列。实际上列表的使用场景很多,在选择时可以参考以下口诀:lpush+lpop=Stack(栈)lpush+rpop=Queue(队列)lpush+ltrim=Capped Collection(有限集合)lpush+brpop=Message Queue(消息队列)

集合

集合(set)类型也是用来保存多个的字符串元素,但和列表类型不一样的是,集合中不允许有重复元素,并且集合中的元素是无序的,不能通过下标获取元素。

一个集合最多可以存储2的32次方减1个元素。

QQ截图20200523144351

添加、遍历、删除

添加元素:向集合里面添加元素,返回结果为添加成功的元素个数。

sadd 键 元素1 元素2 元素3 ...

先添加元素a、b、c,返回值为 3,说明成功添加3个元素:

QQ截图20220217145528

再向集合添加2个元素a、b,返回0说明没有元素成功添加,这是因为集合里已经有这两个元素了:

QQ截图20220217145747

遍历:遍历所有元素。

smembers 键

获取集合myset所有元素,返回结果是无序的:

QQ截图20220217145937

?>提示:smemberslrangehgetall 都属于比较重的命令,如果元素过多存在阻塞Redis的可能性,这时候可以使用 sscan 来完成。

删除元素:删除集合中的元素,返回结果为成功删除元素个数。

srem 键 元素 [元素 ...]

例如,删除集合中的a、b元素返回2,删除d元素返回0:

QQ截图20220217150037

长度、存在、随机

计算:计算元素个数。

scard key

**scard的时间复杂度为O(1),它不会遍历集合所有元素,而是直接用 Redis内部的变量,**例如:

QQ截图20220217150320

存在:判断指定元素是否在集合中,存在返回1,反之返回0。

sismember 键 元素

例如,判断元素c、d是否在集合当中:

QQ截图20220217150506

随机返回:随机从集合返回指定个数元素,如果指定个数超出集合长度,则全部返回。

srandmember 键 [count]
  • [count]是可选参数,如果不写默认为1,例如:

QQ截图20220217150820

随机弹出:从集合随机弹出元素,没有元素弹出时,返回null。

spop 键 [count]
  • [count]是可选参数,如果不写默认为1,例如:

QQ截图20220217150954

!> 提示:srandmemberspop 都是随机从集合选出元素,两者不同的是 spop 命令执行后,元素会从集合中删除,而 srandmember 不会。

交集、并集、差集

现在有两个集合,它们分别是 user1user2

127.0.0.1:6379> sadd user1 it music his sports 
(integer) 4 
127.0.0.1:6379> sadd user2 it news ent sports 
(integer) 4

QQ截图20220217151110

交集:多个集合中共有的元素。

sinter 键 [键 ...]

user1user2 两个集合的交集:

QQ截图20220217151203

并集:多个集合中所有的元素。

suinon 键 [键 ...]

user1user2 两个集合的并集:

QQ截图20220217151242

差集:多个集合中除开共有的元素。

sdiff 键 [键 ...]

user1 减去 user2 的差集:

QQ截图20220217151350

前面三个命令如图所示:

QQ截图20200523154633

时间复杂度、使用场景

QQ截图20200523155109

**集合类型比较典型的使用场景是标签(tag)。**例如一个用户可能对娱乐、体育比较感兴趣,另一个用户可能对历史、新闻比较感兴趣,这些兴趣点就是标签。有了这些数据就可以得到喜欢同一个标签的人,以及用户的共同喜好的标签,这些数据对于用户体验以及增强用户黏度比较重要。

有序集合

有序集合(zset):它保留了集合不能有重复成员的特性, 但不同的是,有序集合中的元素可以排序。但是它和列表使用索引下标作为排序依据不同的是,它给每个元素设置一个分数(score)作为排序的依据。

QQ截图20200523160230

!> 提示:有序集合中的元素不能重复,但是score可以重复,就和一个班里的同学学号不能重复,但是考试成绩可以相同。

QQ截图20200523160422

添加、长度、分数

添加:集合内添加成员,返回结果代表成功添加成员的个数。

zadd 键 分数 成员 [分数 成员 ...]

向有序集合 user:ranking 添加用户tom和他的分数251:

QQ截图20220217152036

长度:返回有序集合内的成员个数。

zcard 键

返回有序集合 user:ranking 的成员数:

QQ截图20220217153224

获取分数:获取某个成员的分数,如果成员不存在则返回 null

zscore 键 成员

QQ截图20220217153635

增加分数:给成员增加指定的分数,返回给成员增加后的分数。

zincrby 键 增加分数 成员

给tom增加了9分,分数变为了260分:

QQ截图20220217153930

范围、排名、删除

分数范围个数:返回指定分数范围成员个数。

zcount 键 最小分数 最大分数

返回从200分到221分的成员的个数:

QQ截图20220217154800

分数范围成员:返回指定分数范围的成员。

zrangebyscore 键 最小分数 最大分数 [withscores] [limit offset count] 
zrevrangebyscore 键 最小分数 最大分数 [withscores] [limit offset count]
  • zrangebyscore 按照分数从低到高返回,zrevrangebyscore 反之。
  • withscores 选项,同时会返回成员的分数
  • [limit offset count] 选项可以限制输出的起始位置和个数。

QQ截图20220217154944

成员排名:返回成员的排名(排名从0开始计算)。

zrank 键 成员
zrevrank 键 成员
  • zrank 是从分数从低到高返回排名,zrevrank 反之。

tom按分数由低到高排第5,按分数由高到低排第0:

QQ截图20220217155253

获取成员:返回指定排名范围的成员。

# 分值排名由低到高返回
zrange 键 开始排名 结束排名 [withscores]
# 分值排名由高到低返回
zrevrange 键 开始排名 结束排名 [withscores]
  • 其中 zrange 按照分数从低到高返回,zrevrange 反之。
  • 加上 withscores 选项,同时会返回成员的分数

返回排名最低、最高的三个成员和分数:

QQ截图20220217155523

删除:删除成员,返回结果为成功删除的个数。

zrem 键 成员 [成员 ...]

将成员mike从有序集合 user:ranking 中删除:

QQ截图20220217155645

删除排名范围:删除指定排名内的升序元素。

zremrangebyrank 键 开始排名 结束排名

删除排名第1到第3名的成员:

QQ截图20220217155909

删除分数范围:删除指定分数范围的成员,返回结果为成功删除的个数。

zremrangebyscore 键 最小分数 最大分数

将250分以上的成员全部删除:

QQ截图20220217160239

交集、并集

zadd user:ranking:1 1 kris 91 mike 200 frank 220 tim 250 martin 251 tom 
(integer) 6 
zadd user:ranking:2 8 james 77 mike 625 martin 888 tom 
(integer) 4

QQ截图20220217160601

QQ截图20200523163642

交集:多个集合中共有的元素。

zinterstore destination numkeys key [key ...] [weights weight [weight ...]] [aggregate sum|min|max]
  • destination:交集计算结果保存到这个键。
  • numkeys:需要做交集计算键的个数。
  • key[key...]:需要做交集计算的键。
  • weights weight[weight...]:每个键的权重,在做交集计算时,每个键中的每个member会将自己分数乘以这个权重,每个键的权重默认是1
  • aggregate sum|min|max:计算成员交集后,分值可以按照sum(和)、min(最小值)、max(最大值)做汇总,默认值是sum

user:ranking:1user:ranking:2 做交集,weights和aggregate使用了默认配置,可以看到目标键 user:ranking:1_inter_2 对分值做了sum操作:

QQ截图20220217160907

并集:多个集合中所有的元素。

zunionstore destination numkeys key [key ...] [weights weight [weight ...]] [aggregate sum|min|max]

该命令的所有参数和zinterstore是一致的,只不过是做并集计算。

计算 user:ranking:1user:ranking:2 的并集,weights和aggregate使用了默认配置,可以看到目标键 user:ranking:1_union_2 对分值做了sum操作:

QQ截图20220217161110

时间复杂度、使用场景

QQ截图20200523164712

**有序集合比较典型的使用场景就是排行榜系统。**例如视频网站需要对用户上传的视频做排行榜,榜单的维度可能是多个方面的:按照时间、按照播放数量、按照获得的赞数。本节使用赞数这个维度,记录每天用户上传视频的排行榜。

哈希

在Redis中,哈希类型是指键值本身又是一个键值对结构

QQ截图20200519224409

!> 注意:哈希类型中的映射关系叫作 field-value,注意这里的 value 是指 field 对应的值,不是键对应的值。

添加、获取、删除

添加:添加一对域值

hset 键 域 值

user 添加一对 {name:tom} 域值,如果设置成功会返回1,反之会返回0

QQ截图20220217112138

获取:获取域的属性值

hget 键 域

获取 username 域(属性)对应的值,如果键或 field 不存在,会返回 nil

QQ截图20220217112317

删除:删除哈希键中一个或多个 field,返回结果为成功删除 field 的个数

hdel 键 域 [域 ...]

删除 username 域,如果域不存在返回0

QQ截图20220217112446

批量添加、批量获取

hmset 批量设置 field-value,需要的参数是 key 和多对 field-value 域值。

hmset 键 域 值 [域 值 ...]

QQ截图20220217112814

hmget 是批量获取 field-value,需要的参数是 key 和多个 field,如果没有域就返回为null:

hmget 键 域 [域 ...]

QQ截图20220217113259

获取全域、获取全值

获取所有 field

hkeys 键

获取所有的 value

hvals 键

获取所有的 field-value

hgetall 键

获取 user 全部 fieldvaluefield-value

QQ截图20220217113532

?> 提示:如果哈希元素个数比较多,在使用 hgetall 时,会存在阻塞Redis的可能。如果一定要获取全部 field-value,可以使用 hscan 命令,该命令会渐进式遍历哈希类型。

存在、统计、计算

存在:判断 field 是否存在,存在返回结果为1,不存在时返回0

hexists 键 域

统计:统计 field 个数。

hlen 键

计算:计算 field 对应的 value 的字符串长度。

hstrlen 键 域

例如,user 有3个 field 域:

QQ截图20220217113905

时间复杂度、使用场景

QQ截图20200523155801

**哈希类型比较典型的使用场景是信息存储。**相比其他的存储,用户信息存储更直观,但Redis去模拟关系型复杂查询开发困难,维护成本高,而关系型数据库可以做复杂的关系查询

QQ截图20200520225028

**哈希类型是稀疏的,而关系型数据库是完全结构化的。**例如,哈希类型每个键可以有不同的 field,而关系型数据库一旦添加新的列,所有行都要为其设置值(即使为NULL)。

QQ截图20200520225340

内部编码

Redis对外有5种数据结构分别是: string(字符串)、hash(哈希)、list(列表)、set(集合)、zset(有序集合),实际上每种数据结构都有自己底层的内部编码实现,而且是多种实现,这样Redis会在合适的场景选择合适的内部编码。

QQ截图20200517152640

查询内部编码

可以看到每种数据结构都有两种以上的内部编码实现,可以通过 object encoding 命令查询内部编码:

127.0.0.1:6379> set hello world 
OK
127.0.0.1:6379> object encoding hello 
"embstr" 

127.0.0.1:6379> rpush mylist a b c d e f g 
(integer) 7
127.0.0.1:6379> object encoding mylist 
"ziplist"

Redis这样设计有两个好处:第一,可以改进内部编码,而对外的数据结构和命令没有影响,这样一旦开发出更优秀的内部编码,无需改动外部数据结构和命令。第二,多种内部编码实现可以在不同场景下发挥各自的优势。

字符串编码

字符串类型的内部编码有3种:

  • int:8个字节的长整型。

  • embstr:小于等于39个字节的字符串。

  • raw:大于39个字节的字符串。

Redis会根据当前值的类型和长度决定使用哪种内部编码实现。

整数类型示例如下:

127.0.0.1:6379> set key 8653
OK
127.0.0.1:6379> object encoding key
"int"

短字符串示例如下:

#小于等于39个字节的字符串:embstr 
127.0.0.1:6379> set key "hello,world"
OK
127.0.0.1:6379> object encoding key
"embstr"

长字符串示例如下:

#大于39个字节的字符串:raw 
127.0.0.1:6379> set key "one string greater than 39 byte........." 
OK
127.0.0.1:6379> object encoding key 
"raw" 
127.0.0.1:6379> strlen key 
(integer) 40

列表编码

列表类型的内部编码有两种:

  • ziplist(压缩列表):当列表的元素个数小于 list-max-ziplist-entries 配置(默认512个),同时列表中每个元素的值都小于 list-max-ziplist-value 配置时(默认64字节),Redis会选用 ziplist 来作为列表的内部实现来减少内存的使 用。
  • linkedlist(链表):当列表类型无法满足 ziplist 的条件时,Redis会使用 linkedlist 作为列表的内部实现。

当元素个数较少且没有大元素时,内部编码为 ziplist

127.0.0.1:6379> rpush listkey e1 e2 e3 
(integer) 3 
127.0.0.1:6379> object encoding listkey 
"ziplist"

当元素个数超过512个或者当某个元素超过64字节,内部编码变为 linkedlist

127.0.0.1:6379> rpush listkey e4 e5 ... e512 e513 
(integer) 513 
127.0.0.1:6379> object encoding listkey 
"linkedlist"
127.0.0.1:6379> rpush listkey "one string is bigger than 64 byte..." 
(integer) 4 
127.0.0.1:6379> object encoding listkey
"linkedlist"

集合编码

集合类型的内部编码有两种:

  • intset(整数集合):当集合中的元素都是整数且元素个数小于 set-max- intset-entries 配置(默认512个)时,Redis会选用 intset 来作为集合的内部实现,从而减少内存的使用。
  • hashtable(哈希表):当集合类型无法满足 intset 的条件时,Redis会使用 hashtable 作为集合的内部实现。

当元素个数较少且都为整数时,内部编码为 intset

127.0.0.1:6379> sadd setkey 1 2 3 4 
(integer) 4 
127.0.0.1:6379> object encoding setkey 
"intset"

当元素个数超过512个或者当某个元素不为整数时,内部编码变为 hashtable

127.0.0.1:6379> sadd setkey 1 2 3 4 5 6 ... 512 513 
(integer) 509 
127.0.0.1:6379> scard setkey 
(integer) 513 
127.0.0.1:6379> object encoding listkey 
"hashtable"
127.0.0.1:6379> sadd setkey a 
(integer) 1 
127.0.0.1:6379> object encoding setkey 
"hashtable"

有序集合编码

有序集合类型的内部编码有两种:

  • ziplist(压缩列表):当有序集合的元素个数小于 zset-max-ziplist- entries 配置(默认128个),同时每个元素的值都小于 zset-max-ziplist-value 配 置(默认64字节)时,Redis会用 ziplist 来作为有序集合的内部实现,ziplist 可以有效减少内存的使用。
  • skiplist(跳跃表):当 ziplist 条件不满足时,有序集合会使用 skiplist 作为内部实现,因为此时 ziplist 的读写效率会下降。

当元素个数较少且每个元素较小时,内部编码为 skiplist

127.0.0.1:6379> zadd zsetkey 50 e1 60 e2 30 e3 
(integer) 3 
127.0.0.1:6379> object encoding zsetkey 
"ziplist"

当元素个数超过128个或者当某个元素大于64字节时,内部编码变为 ziplist

127.0.0.1:6379> zadd zsetkey 50 e1 60 e2 30 e3 12 e4 ... 84 e129 
(integer) 129 
127.0.0.1:6379> object encoding zsetkey 
"skiplist"
127.0.0.1:6379> zadd zsetkey 20 "one string is bigger than 64 byte..." 
(integer) 1 
127.0.0.1:6379> object encoding zsetkey
"skiplist"

哈希编码

哈希类型的内部编码有两种:

  • ziplist(压缩列表):当哈希类型元素个数小于 hash-max-ziplist-entries 配置(默认512个)、同时所有值都小于 hash-max-ziplist-value 配置(默认64 字节)时,Redis会使用 ziplist 作为哈希的内部实现,ziplist 使用更加紧凑的结构实现多个元素的连续存储,所以在节省内存方面比 hashtable 更加优秀。
  • hashtable(哈希表):当哈希类型无法满足 ziplist 的条件时,Redis会使用 hashtable 作为哈希的内部实现,因为此时 ziplist 的读写效率会下降,而 hashtable 的读写时间复杂度为 O(1)

field 个数比较少且没有大的 value 时,内部编码为 ziplist

127.0.0.1:6379> hmset hashkey f1 v1 f2 v2 
OK
127.0.0.1:6379> object encoding hashkey 
"ziplist"

当有 value 大于64字节或者当 field 个数超过512,内部编码会由 ziplist 变为 hashtable

127.0.0.1:6379> hset hashkey f3 "one string is bigger than 64 byte..." 
OK
127.0.0.1:6379> object encoding hashkey 
"hashtable" 
127.0.0.1:6379> hmset hashkey f1 v1 f2 v2 f3 v3 ... f513 v513
OK
127.0.0.1:6379> object encoding hashkey 
"hashtable"

HINCRBY命令是Redis中用于对hash类型数据中某个字段的值进行增加操作的命令。它可以将指定字段的值作为整数处理,并将增加的结果保存在该字段中。

HINCRBY命令的语法格式如下:

HINCRBY key field increment

其中,key表示哈希数据结构的键名,field表示哈希数据结构中的字段名,increment表示要增加的值。增量increment可以是正数或负数,如果是负数,那么相当于对指定字段进行减法操作。

如果key不存在,那么HINCRBY命令会创建一个新的哈希表并执行增加操作。如果field不存在,那么在执行命令前,field的值会被初始化为0。但请注意,对一个存储字符串值的field执行HINCRBY命令会造成错误。

执行HINCRBY命令后,会返回哈希表key中field的新值。此命令的时间复杂度为O(1),操作的值被限制在64位有符号数字表示之内。

总的来说,HINCRBY命令是Redis中用于处理哈希表中字段值增减的非常有用的命令。