Featured image of post Redis 常见数据结构

Redis 常见数据结构

Redis 提供了丰富的数据类型,常见的有五种:

  • String 字符串
  • List 列表
  • Hash 哈希
  • Set 集合
  • Zset 有序集合

随着 Redis 版本的更新,后面又支持了四种数据类型:

  • BitMap (2.2)
  • HyperLogLog (2.8)
  • GEO (3.2)
  • Stream (5.0)

在线 Redis 体验 → https://try.redis.io/

🦮 String

String 是最基本的 key-value 结构。key 是唯一标识(可以是字符串、数字),value 是具体的值(最长可以容纳 521M)。

🎗️ 常用命令

  • 🧩 基本操作
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
# 设置
set name emery

# 获取
get name

# 判断 key 是否存在
exists name

# 获取字符串的长度
strlen name

# 删除
del name
  • 🧩 批量设置
1
2
3
4
5
# 批量设置 k-v
mset key1 value1 key2 value2

# 批量获取 k-v
mget key1 key2
  • 🧩 计数器(value 为整数)
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
# 设置
set num 0

# +1
incr num

# +10
incrby num 10

# -1
decr num

# -10
decrby num 10
  • 🧩 过期(默认为永不过期)
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
# 添加并设置过期时间 ex->秒 px->毫秒
set name emery ex 60

# 对已存在的 key 设置过期
expire name 60

# 查看过期时间
ttl name

# 移除过期时间(无过期时间 ttl 返回 -1)
persist name
  • 🧩 存在与不存在
1
2
3
4
5
# key 不存在才成功
set name emery nx

# key 存在才成功
set name emery xx

🎡 实现方式

String 的底层实现是自定义的 SDS(简单动态字符串)。

相比于 C 语言的字符串:

  • SDS 不仅可以保存文本,还可以保存二进制数据(如图片、音频、视频等)。

SDS 的 API 都会一处理二进制的方式处理保存在 buf[] 数组中的数据。

  • SDS 获取字符串长度的时间复杂度是 O(1)。

C 语言的字符串不会记录自身长度,获取需要遍历,时间复杂度为 O(n)。SDS 结构中记录了长度。

  • SDS 的 API 是安全的,拼接字符串不会导致缓冲区溢出。

SDS 在拼接前会检查空间是否满足要求,不够的话会自动扩容。

🧵 使用场景

  • 📐 缓存对象

    • 直接缓存 JSON。 SET user:1 '{"name":"emery", "age":18}'
    • 分离为 user:id 作为 key,用 mset 存储。 MSET user:1:name emery user:1:age 18 user:2:name lin user:2:age 20
  • 📐 常规计数

Redis 处理命令是单线程的,所以执行命令的过程是原子的。可以对访问访问次数、点赞、转发、库存数量进行统计。

1
2
3
4
set aritcle:readcount:1001 0

# 原子操作
incr aritcle:readcount:1001

NX 参数可以实现在 key 存在的时候,才插入数据,可以用来实现分布式锁。

1
2
3
4
5
# lock_key 分布式锁的 key
# unique_value 客户端生成的唯一标识
# NX 在 lock_key 不存在时,才进行操作
# PX 毫秒 10000ms -> 10s
set lock_key unique_value NX PX 10000
  • 📐 共享 session 信息

Session 保存在服务端,通常用于保存用户的会话状态(登录)。

在分布式系统中,用户多次请求不一定都在同一台服务器中,我们可以借助 Redis 对 Session 信息进行统一的存储和管理,服务器都去同一个 Redis 获取相关的 Session 信息,这样就解决了分布式系统下 Session 存储的问题。

🐅 List

List 是简单的字符串列表,按照插入顺序排序,可以从头部或尾部向 List 中添加元素。最大长度为 2^32 - 1

🎗️ 常用命令

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
# 插入
lpush key value
rpush key value

# 移除
lpop key
rpop key

# 返回指定区间内的元素(从 0 开始)
lrange key start end

# 弹出一个元素,并阻塞 timeout 秒(当 timeout=0 就一直阻塞)
blpop key timeout
brpop key timeout

🎡 实现方式

Redis List 底层由(1)压缩列表 或 (2)双向链表 实现。

  • 压缩列表:

    • 如果列表的元素个数小于 512 个(默认值,可由 list-max-ziplist-entries 配置),
    • 列表每个元素的值都小于 64 字节(默认值,可由 list-max-ziplist-value 配置),
    • Redis 会使用压缩列表作为 List 类型的底层数据结构。
  • 双向列表:不满足压缩列表的条件底层就会使用双向列表。

压缩列表

是 Reids 为了节约内存而开发的,由连续内存块组成的顺序型数据结构,有点类型于数组。

在表头有三个字段: (1)zlbytes:压缩列表占用的内存字节数; (2)zltail:列表尾的偏移量(尾部距离首部有多少个字节); (3)zllen:压缩列表包含的节点数量;

在表尾有一个字段: (4)zlend:列表结束的标志(固定值 0xFF -> 255);

查找定位第一个和最后一个元素的时间复杂度为:O(1) 查找其他元素,只能逐个查找,O(n),所以压缩列表不适合保存过多的元素。

在 Redis中,List、Hash、Zset 对象,在元素数量较少,元素值不大时,会使用压缩列表作为底层数据结构。

🧵 使用场景

  • 📐 消息队列

🦛 Hash

Hash 是一个键值对的集合 key-value,特别适合存储对象。

🎗️ 常用命令

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
# 存储一个 key=people 的哈希表 
hset people name "Emery"
# 获取哈希表中的值
hget people name

# 存储多个键值对
hmset people name "Emery" age 18
# 批量获取键值对
hmget people name age

# 删除一个哈希表
hdel people

# 获得哈希表中key-value对的数量
hlen people

# 获取哈希表中所有的 key
hgetall people

🎡 实现方式

Hash 类型的底层数据结构由 (1)压缩列表 或 (2)哈希表 实现。

  • 压缩列表

    • 元素小于 512 个
    • 所有的值小于 64 字节
    • 使用压缩列表
  • 哈希表

    • 不满足以上条件使用 hash 表

🧵 使用场景

  • 📐 缓存对象

1
2
3
4
hmset uid:10001 name Tom age 15
hmset uid:10002 name Jerry age 13

hmget uid:10001

使用 String 类型缓存 JSON 也是缓存对象的一种凡是,如果对象中某些属性频繁变化,可以考虑使用 Hash。

  • 🛒 购物车
    • 😃 用户 id -> key
    • 🏪 商品 id -> field
    • 🔢 商品数量 -> value
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
# 添加商品 cart:{用户id} {商品id} 1
hset cart:10001 22223 1

# 添加数量
hincrby cart:10001 22223 1

# 获得商品数量
hget cart:10001 22223

# 删除商品
hdel cart:10001 22223

# 获取购物车所有商品
hgetall cart:10001

在实际业务中,Redis 中只存储了商品的 id 信息,在回显商品的具体信息时,再拿商品 id 查询一次数据库,获取完整的商品信息。

🐑 Set

Set 是一个无序并唯一的集合(存储顺序不会按照插入的先后顺序进行存储)。

集合中最多可以存储 2^32 - 1 个元素,除增删改查外,还支持多个集合的交集、并集和差集。

List 与 Set 的区别:

  • List 可以存储重复元素,Set 不可以
  • List 按照元素添加的顺序存储元素,Set 存储元素是无序的

🎗️ 常用命令

  • 🧩 常用操作
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
# 添加元素(已存在则忽略)
sadd key value1 [value2 ...]

# 删除元素
srem key value1 [value2 ...]

# 获取所有的元素
smembers key

# 获取集合中的元素个数
scard key

# 判断是否存在
sismember key value

# 从集合中随机选取 count 个元素,不从中删除
srandmember key [count]

# 从集合中随机选取 count 个元素,并删除
spop key [count]
  • 🧩 运算操作

⚠ Set 的交并差集运算的复杂度较高,在数据量较大时,如果直接执行计算,会导致 Redis 实例阻塞。 在实际中,推荐获取数据后,在客户端去完成计算。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
# 交集
sinter key1 [key2 ...]
# 交集并存储
sinterstore destination key1 [key2 ...]

# 并集
sunion key1 [key2 ...]
sunionstore destination key1 [key2 ...]

# 差集
sdiff key1 [key2 ...]
sdiffstore destination key1 [key2 ...]

🎡 实现方式

Set 类型的底层数据结构由 (1)哈希表 或 (2)整数集合实现。

  • 整数集合

    • 元素个数少于 512 个,底层采用整数集合作为数据结构
  • 哈希表

    • 不满足以上条件,使用哈希表作为底层数据结构

🧵 使用场景

Set 主要是无续,不可重复,可求交并差的特性。

  • 📐 点赞

Set 类型可以保证用户只能点一个赞。

  • key -> 文章 id
  • value -> 用户 id
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
# 文章 1 被 id 为 1、2、3 的用户点赞
sadd article:1 uid:1
sadd article:1 uid:2
sadd article:1 uid:3

# 取消点赞
srem article:1 uid:1

# 获取 文章 1 的点赞用户 id
smembers article:1

# 获取 文章 1 的点赞数量
scard article:1

# 判断 用户 1 是否对文章 1 进行了点赞
sismember article:1 uid:1
  • 📐 共同关注

Set 支持交集运算,可以用来计算共同关注的好友,公众号等。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
# id 为 1、2 的用户关注的公众号
sadd uid:1 5 6 7 8 9
sadd uid:2 7 8 9 10 11

# 获取共同关注
sinter uid:1 uid:2

# 给 uid:2 推荐 uid:1 关注的公众号
sdiff uid:1 uid:2

# 验证某个公众号是否被同时关注
sismember uid:1 5  # -> 1 关注
sismember uid:2 5  # -> 0 未关注
  • 📐 抽奖活动

key->活动名,value->用户名,存储某活动中中奖的用户名 ,Set 类型因为有去重功能,可以保证同一个用户不会中奖两次。

1
2
3
4
5
6
7
8
9
sadd lucky Tom Jerry John Sean Marry Lindy Sary Mark

# 抽取 1 个一等奖:
srangememeber lucky 1
# 抽取 2 个二等奖:
srangememeber lucky 2

# 不允许重复获奖
spop lucky 1

🦥 Zset

Zset(有序集合,从小到大) 相比于 Set 类型多了一个排序分值 socre。

对于有序集合 ZSet 来说,每个存储的元素由两个值组成,一个是有序集合的元素值,一个是排序值。

Zset 中元素不能重复,分数值可以重复,并且可以排序。

🎗️ 常用命令

  • 🧩 常用操作
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
# 向有序集合中加入带分值的元素
zadd key socre number [[score number] ...]

# 从有序集合中删除元素
zrem key number [number ...]

# 返回有序集合中元素的分数
zscore key number

# 返回有序集合中元素的个数
zcard key

# 为有序集合中 member 的分值加上 increment
zincrby key increment member

# 正序获取有序集合中 start->end 的下表元素
zrange key start end [withscores]

# 倒序获取有序集合中 start->end 的下标元素
zrevrange key start end [withscores]

🎡 实现方式

有序集合 Zset 底层使用 (1)压缩列表 或 (2)跳表 作为数据结构。

  • 压缩列表:有序集合中元素小于 128 个,并且每个元素的值小于 64 字节时,使用压缩列表作为底层数据结构。

  • 跳表:不满足上述条件,使用调表。

🧵 使用场景

  • 📐 排行榜

例如游戏积分排行榜,视频播放排名,电商系统中商品的销量排名。

key->排行榜的类型;member->需排行的内容(如文章id);score->点击数、点赞数等

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
# emery 发的id=1的文章获得了 200 个赞
zadd user:emery:ranking 200 arcticle:1 

# 给文章 1 点赞
zincrby user:emery:ranking 1 article:1

# 查看某篇文章的点赞数
zscore user:emery:ranking article:1

# 查看点赞数最多的 3 篇文章
zrevrange user:emery:ranking 0 2 withscores

# 获取 100 赞到 200 赞的文章
zrangebysocre user:emery:ranking 100 200 withscores
  • 📐 延时队列

利用 score 存储延时执行的时间,使用 zrangebysocre 查询所有符合条件的待处理任务,再进行处理。

🔗 参考

Licensed under CC BY-NC-SA 4.0