MENU

Redis基础及数据结构

May 16, 2021 • Read: 1991 • 编码

Redis 可以做什么

Redis 的应用范围非常广泛,让我们梳理一下 Redis 可以用在哪些地方:

  1. 记录帖子的点赞数、评论数和点击量(hash)
  2. 记录用户帖子ID列表(排序),便于快速显示用户的列表(zset)
  3. 记录帖子的标题、摘要、作者和封面信息,用于列表展示(hash)
  4. 记录帖子的点赞用户 ID 列表,评论 ID 列表,用于显示和去重(zset)
  5. 缓存近期热帖内容(帖子内容空间的占用比较大),减少数据库的压力 (hash)
  6. 记录帖子的相关文章 ID,根据内容推荐相关帖子(list)
  7. 如果帖子ID是整数自增的,可以使用 Redis 来分配帖子 ID (计数器)
  8. 收藏集和帖子直接的关系(zset)
  9. 记录热榜帖子 ID 列表、总热播和分类热榜(zset)
  10. 缓存用户的历史行为,过滤恶意行为(zset、hash)

基础数据结构

Redis 有五种基础数据结构,分别为:string(字符串)、list(列表)、hash(字典)、set(合集)和 zset(有序合集)。

string 字符串

字符串是 Redis 最简单的数据结构,它的内部就是一个字符串数组。

Redis 所有的数据结构都是以一唯一的 key 字符串作为名称,然后通过这个唯一的 Key 来获取相应的 Value 数据。

不同类型的数据结构差异就在于 Value 的结构不一样。

用途

字符串用途非常广泛,最常见的用途就是缓存用户信息。将用户信息结构体用 JSON 序列化成字符串,然后 序列化后的字符串塞进 Redis 缓存。取用户信息时,同样经过一次反序列化的过程。

存储

Redis 的字符串是动态字符串,可以修改的,内部结构的实现类似于 java 的 ArrayList,采用预分配冗余空间的方式来减少内存的频繁分配。一般分配的空间会高于实际字符串长度len。

当字符串长度小于 1MB 时,扩容都是加倍现有的空间,如果字符串长度超过 1MB,扩容时一次只会多扩 1MB 的空间。

需要注意的是:字符串最大长度为 512MB。

操作

键值对

# 设置字符串值
> set name codehole
OK
# 获取值
> get name
codehole
# key是否存在
> exists name
1
# 删除 key
> del name
1
> get name
null

批量键值对

redis> set name1 codehole
OK
redis> set name2 holycoder
OK
redis> mget name1 name2 name3 
codehole
holycoder
null
redis> mset name1 zhangsan name2 lisi name3 wangwu
OK
redis> mget name1 name2 name3 
zhangsan
lisi
wangwu

过期和 set 命令扩展

可以对 key 设置过期时间,到时间会被自动删除,这个功能常用来控制缓存的失效时间。不过这个自动删除机制比较复杂,后面会说到。

> set name codehole
OK
> get name
codehole
# 设置五秒后过期
> expire name 5
1
> get name
null

#### 计数

如果 Value 是一个整数,还可以对他进行自增操作。范围在signed long 的最大值和最小值之间,超出这范围

,Redis 会报错。

> SET age 30
OK
> incr age
31
> incrby age 5
36
> incrby age -5
31
> set age 9223372036854775807
OK
> incr age
ERR increment or decrement would overflow

字符串由多个字节组成,每个字节又由8个 bit 组成,如此便可将一个字符串看成是多个bit的组合,这便是 bitmap(位图)数据结构。具体使用方法后面会讲到。

list 列表

Redis 的列表相当于 Java 语言中的 LinkedList,注意他是链表不是数组。这意味着 list 的插入和删除操作非常快,时间复杂度为 O(1),但索引定位很慢,时间复杂第为O(n),这点非常令人意外。

列表中的每一个元素都可以使用双向指针顺序,串起来可以同时支持向前或向后遍历。

当列表弹出最后一个元素之后,该数据结构被自动删除,内存被回收。

用途

Redis 的列表常用来做异步队列使用。将需要延后处理的任务结构体序列化成字符串,塞进 Redis 列表中,另一个线程从这个列表中轮询数据进行处理。

操作

队列

队列是先进先出的数据结构,常用于消息队列异步逻辑处理,它会确保元素的访问顺序性。

> rpush books python java golang
3
> llen books
3
> lpop books
python
> lpop books
java
> lpop books
golang
> lpop books
null

栈是先进后出的数据结构,跟队列正好相反。拿 Redis 的列表数据结构来做栈使用的业务场景并不多见。

> rpush books python java golang
3
> rpop books
golang
> rpop books
java
> rpop books
python
> rpop bo2oks
null

慢操作

  • lindex 相当于Java 链表中的 get(int index) 方法,她需要对链表进行遍历,性能随着参数 index 的增大而变差。
  • ltrim 和字面上的意思不太一样,叫 lretain(保留)更合适,因为 ltrim 的两个参数,start_index 和 end_index 定义了一个区间,保留在这个区间内的值,之外的不保留。我们可以使用 ltrim 实现一个定长的 链表。
  • index 可以是负数, -1 表示倒数第一个元素, -2 表示倒数第二个元素。
> rpush books python java golang
3
> lindex books 1     # O(n) 慎用
java
> lrange books 0  -1 # 获取所有元素, O(n) 慎用
python
java
golang
> ltrim books 1 -1    # O(n) 慎用
OK
> lrange books 0  -1
java
golang
> ltrim books 1 0
OK
> llen books
0

快速列表

Redis 底层存储的并不是一个简单的 linkedlist,而是称之为快速链表(quicklist)的一个结构。

在链表元素较少的情况下,会使用一块连续的内存存储,这个结构是 ziplist 即压缩列表。他所有的元素都是彼此紧挨着存储,分配的是一块连续的内存。

当数据量比较多的时候才会改成 quicklist 。因为普通的链表存储的只是 int 类型的数据,结构上还需要两个额外的指针 prev 和 next。所以 Redis 将链表和 ziplist 组合起来组成了 quicklist,也就是多个 ziplist 使用双向指针串联起来使用,既满足了快速插入的性能,又不会出现太大的空间冗余。

ziplist
ziplist
ziplist
ziplist
ziplist
ziplist
ziplist
ziplist
Viewer does not support full SVG 1.1

hash 字典

用途

Redis 的字典相当于 Java 语言的 HashMap,它是无序字典,内部存储了很多键值对。都是 “数组 + 链表”,的二维结构。不同的是,Redis的字典值只能是字符串,另外它们 rehash 的方式不一样,Java 的 HashMap 字典很大时,rehash 是一个很耗时的操作,一次性全部 rehash。 Redis 为了性能,采用了渐进式 rehash 的策略。

渐进式 rehash 会在 rehash 的同时,保留新旧两个 hash 结构,查询时会同时查询两个 hash 结构,然后在后续的定时任务或操作指令中,渐进的将旧的 Hash 内容一点点移动到新的 Hash 结构中。当移动完成,就会使用新的 hash 结构代替。

存储

当 hash 移除最后一个元素之后,该数据结构会被自动删除,内存被回收。

hash 结构也可以用于存储用户信息,并且不需要序列化,可以直接对用户的每个字段单独存储。当我们需要获取用户信息时可以部分获取,而以字符串存储用户信息只能全部读取,这样会浪费网络流量。

hash 也有缺点,hash 结构的存储消耗要远高于单个字符串,到底该使用 hash 还是字符串,应该根据实际情况再三权衡。

操作

> hset books java "think in java"
1
> hset books golang "think in go"
1
> hset books python "think in cookbook"
1
> hgetall books
java
think in java
golang
think in go
python
think in cookbook
> hlen books
3
> hget books java
think in java
> hset books golang "learning go progremming"
0
> hget books golang
learning

hash 的单个子 key 也可以进行计数,它对应的指令是 hincrby,使用方法和 incr 基本一样。

127.0.0.1:6379> hset user age 29
(integer) 1
127.0.0.1:6379> hincrby user age 1
(integer) 30
127.0.0.1:6379>

set 集合

Redis 的集合相当于 java 语言里面的 HashSet,它内部的键值对是无序的,唯一的。它内部相当于一个特殊的字典,字典中所有的 value 值都是 NULL。

用途

set的结构可以用来存储某个活动中奖的用户ID,因为有去重功能,可以保证同一个用户不会中将两次。

存储

当集合中最后一个元素被移除后,数据结构被自动删除,内存被回收。

操作

127.0.0.1:6379> sadd books python
(integer) 1
127.0.0.1:6379> sadd books python # 重复添加
(integer) 0
127.0.0.1:6379> sadd books java golang
(integer) 2
127.0.0.1:6379> smembers books # 与插入顺序不一致,因为set是无序的
1) "python"
2) "golang"
3) "java"
127.0.0.1:6379> sismember books java # 查询某个 value 是否存在
(integer) 1
127.0.0.1:6379> sismember books rust
(integer) 0
127.0.0.1:6379> scard books # 获取长度
(integer) 3
127.0.0.1:6379> spop books # 弹出一个
"python"
127.0.0.1:6379>

zset 有序列表

zset 可能是 Redis 中提供的最具有特色的数据结构,一方面它是一个 set 保证了内部 value 唯一性,另一方面它可以给每一个内部 value 赋予一个 score,代表这个value 的排位权重。它内部实现的方式是一种叫做 “跳跃列表” 的数据结构。

用途

zset 可以用来存储粉丝列表,value 值是粉丝的 用户ID,score 是关注时间。我们可以对粉丝列表按照关注时间进行排序。

zset可以用来存储学生的成绩,value 值是学生的 ID,score 是他的考试成绩,我们按照分数排名就能得到他的名次。

存储

zset 中的最后一个 value 被移除后,数据结构被自动删除,内存被回收。

操作

#  插入 zset 数据
> zadd lang 9 "java"
1
> zadd lang 8.9 "php"
1
> zadd lang 7 "golang"
1

## 按 score 顺序列出,参数区间为排名范围
> zrange lang 0 -1
golang
php
java

# 按 score 逆序列出,参数区间为排名范围
> zrevrange lang 0 -1
java
php
golang

# 取 value 数量,相当于 count()
> zcard lang
3

# 获取指定 value 的 score 值,由于redis内部使用 double 类型进行存储,所以存在小数点精度丢失问题
> zscore lang "php"
8.9000000000000004
# 获取value值的排名,(从0开始)
> zrank lang "php"
1
# 根据 score 范围遍历 zset
> zrangebyscore lang 0 8.91
golang
php
# 根据 score 范围遍历 zset,同时返回分值。inf代表 infinite,无穷大的意思
> zrangebyscore lang -inf 8.91 withscores
golang
7
php
8.9000000000000004

# 删除 value
> zrem lang "java"
1
> zrange lang 0 -1
golang
php

容器型数据结构通用规则

list、set、hash、set 这四种数据结构是容器型的数据结构,它们共享下面两条通用的规则:

  • create if not exists: 如果容器不存在,那就创建一个,在进行操作。
  • drop if no elements: 如果容器里的元素没有了,就会立即删除容器,释放缓存。

过期时间

Redis 的所有数据类型都可以设置过期时间,时间到了,Redis 会自动删除相应的对象。需要注意的是,过期是以对象为单位的,比如一个 hash 结构过期是对整个 hash 对象的过期,而不是过期某一个子 key。

还需要注意的是,如果一个字符串已经设置了过期时间,然后使用 set 方法对字符串进行了修改,它的过期时间会消失。

127.0.0.1:6379> set codehole yoyo
OK
127.0.0.1:6379> EXPIRE codehole 600
(integer) 1
127.0.0.1:6379> ttl codehole
(integer) 539
127.0.0.1:6379> set codehole yuyu
OK
127.0.0.1:6379> ttl codehole
(integer) -1
127.0.0.1:6379>

笔记内容来自:《Redis深度历险:核心原理与应用实战》

Last Modified: September 12, 2021
Leave a Comment

3 Comments
  1. 写得非常好!排版舒服

  2. SET 命令可以将任意类型(如map、hash)进行字符串赋值修改。

    万物皆可string……

    1. @konakona是的 string 准确的来说是一个 key-value 的类型,而 Redis 所说的数据类型,更像是一个数据结构。