【Redis】Redis 数据类型

2024-08-16 11:11:45 浏览数 (2)

前言

Redis 是基于 键值对 (key-value) 存储的 NoSQL 数据库,每一对键值对都是哈希类型,其中 Redis 的 key 固定为 string 类型,而 value 则提供了 string(字符串)、hash(哈希)、list(列表)、set(集合)、zset(有序集合)、Bitmaps(位图)、HyperLogLog、GEO(地理信息定位)等多种数据结构,本文将对其中最重要、最常用的五种数据结构进行介绍,包括 string、list、hash、set、zset,理解这些数据结构的特点与常见命令。

1 通用知识

在正式介绍 5 种数据结构之前,我们需要先了解一下 Redis 的一些全局命令、数据结构与内部编码,以及单线程命令处理机制,这能为我们后面内容的学习打下一个良好的基础。这样说的原因如下:

  • Redis 的命令有上百个,如果纯靠死记硬背比较困难,但是如果理解 Redis 的一些机制,会发现这些命令有很强的通用性。
  • Redis 不是万金油,有些数据结构和命令必须在特定场景下使用,一旦使用不当可能对 Redis 本身或者应用本身造成致命伤害。

Redis 的数据结构很多,每一种数据结构所对应的操作命令也很多且各不相同,因此遗忘某些不常用命令是很正常的,只要我们在使用到这些命令时会查阅 帮助文档 即可。

1.1 基本全局命令

Redis 的 value 支持很多数据结构,而全局命令就是能够搭配任意类型 value 来使用的命令。

Redis 中的命令不区分大小写。

keys

keys 的作用是返回所有满足样式(pattern)的 key,时间复杂度为 O(N),支持的统配样式如下:

  • ? 匹配任意一个字符。
  • * 匹配0个或任意多个字符。
  • a-e 匹配 a-e 中的任意一个字符,包含两侧边界。
  • abc 匹配 abc 中的某一个字符。
  • ^e 匹配除了 e 以外的任意一个字符。

语法及使用示例:

代码语言:javascript复制
keys pattern
代码语言:javascript复制
127.0.0.1:6379> keys *
1) "hcllo"
2) "hallo"
3) "hbllo"
4) "hello"
5) "h3llo"
127.0.0.1:6379> keys hell?
1) "hello"
127.0.0.1:6379> keys h*llo
1) "hcllo"
2) "hallo"
3) "hbllo"
4) "hello"
5) "h3llo"
127.0.0.1:6379> keys h[a-c]llo
1) "hcllo"
2) "hallo"
3) "hbllo"
127.0.0.1:6379> keys h[ace]llo
1) "hcllo"
2) "hallo"
3) "hello"
127.0.0.1:6379> keys h[^a]llo
1) "hcllo"
2) "hbllo"
3) "hello"
4) "h3llo"

在生产环境中一般都会禁用 keys 命令,特别是 keys * 命令(查询 Redis 中所有的 key),原因如下:

  • Redis 是单线程服务器,而 keys 命令的时间复杂度是 O(N),这就可能使得 Redis 线程长时间被占用,导致其他 redis-cli 得不到响应。
  • Redis 常被用作缓存,即为 MySQL 这样的关系型数据库分担大部分的请求压力,一旦 Redis 被 keys 阻塞住了,那么 Redis 就会超时返回 nil,此时服务器会认为 Redis 中没有该数据,就会直接从 MySQL 中读取数据,这就很可能导致 MySQL 承担不住压力然后直接挂掉。

EXISTS

exists 用于判断一个/多个 key 是否存在,返回值为存在的 key 的个数,时间复杂度为 O(K),其中 k 为查询的 key 的个数。

Redis 中关于命令的设计以及命令时间复杂度的理解:

  • Redis 是 客户端-服务器 的结构的程序,即 redis-cli 的命令需要通过网络发送给 redis-server,然后由 redis-server 去执行;但是我们知道,网络传输的速度相比于直接操作内存是要慢很多的。
  • 因此,为了尽可能的减少网络的影响,redis 很多命令都支持一次操作多个 key,比如 exists、mset、mget,即让多个指令只需要经历一次网络通信。
  • 但同时,redis 又是单线程模型的服务器,因此注定了我们一次操作的 key 不会太多,否则会导致其他客户端得不到响应,所以我们完全可以将 exists、mset、mget 这些类似命令的时间复杂度从 O(K) 看作 O(1),毕竟 K 很小。
  • 从这里我们也可以初步窥见 redis 在设计各种命令的时候,考虑是非常周全的。

语法及使用示例:

代码语言:javascript复制
EXISTS key [key ...] 
代码语言:javascript复制
127.0.0.1:6379> keys *
1) "key1"
2) "key3"
3) "key2"
127.0.0.1:6379> exists key1 key2 key4
(integer) 2

DEL

del 用于删除一个/多个指定的 key,返回值为成功删除的 key 的个数,时间复杂度为 O(K),其中 k 为操作的 key 的个数。

语法及使用示例:

代码语言:javascript复制
DEL key [key ...] 
代码语言:javascript复制
127.0.0.1:6379> keys *
1) "key1"
2) "key3"
3) "key2"
127.0.0.1:6379> del key1 key2 key4
(integer) 2
127.0.0.1:6379> keys *
1) "key3"

redis 通常被用来作为缓存,缓存中保存的是从 MySQL 中拷贝过来的热点数据,因此即使我们误删了 redis 中的某些 key,一般来说影响也不大,客户端访问该数据时重新从 MySQL 加载到 redis 中即可。但是也要注意,不要执行 del * 这样的命令,这虽然不会导致数据丢失,但很可能导致 MySQL 承受不足压力直接挂掉。

EXPIRE / PEXPIRE

expire 用于为指定的 key 设置秒级的过期时间,pexpire 用于设置毫秒级的过期时间,设置成功返回1,否则0,时间复杂度为 O(1)。

为 key 设置过期时间,那么时间到了之后该 key 就会自动被删除,过期时间是 redis 实现缓存最重要的机制。

语法及使用示例:

代码语言:javascript复制
EXPIRE key seconds 
代码语言:javascript复制
PEXPIRE key milliseconds
代码语言:javascript复制
127.0.0.1:6379> keys *
1) "key"
127.0.0.1:6379> expire key 10
(integer) 1
127.0.0.1:6379> ttl key
(integer) 7
127.0.0.1:6379> ttl key
(integer) 3
127.0.0.1:6379> ttl key
(integer) -2

TTL / PTTL

ttl 用于获取指定 key 的秒级过期时间,pttl 获取毫秒级过期时间,返回-1表示该 key 没有关联过期时间,返回-2表示该 key 不存在 (本身不存在/过期时间到了被删除),时间复杂度为 O(1)。

语法及使用示例:

代码语言:javascript复制
TTL key / PTTL key
代码语言:javascript复制
127.0.0.1:6379> keys *
1) "key"
127.0.0.1:6379> expire key 10
(integer) 1
127.0.0.1:6379> ttl key
(integer) 7
127.0.0.1:6379> ttl key
(integer) 3
127.0.0.1:6379> ttl key
(integer) -2

redis 中 key 的过期策略是如何实现的?即 redis 如何知道哪些 key 已经过期了?

redis 使用惰性删除 定期删除,配合内存淘汰策略实现过期策略,具体如下:

  • 惰性删除:Redis 会为某些 key 设置过期时间,但当这个 key 到达过期时间时,Redis 并不会立即删除该 key,而是等到下一次访问该 key 将其删除并返回 nil。
  • 定期删除:Redis 每隔一段时间会抽取 Redis 中第一部分 key,检查其过期时间,如果时间到了就删除 – Redis 每次只抽取一部分 key 是由于 Redis 是单线程模型,当存在很多 key 时一次检查完毕会导致其他客户端被阻塞。
  • 尽管 Redis 使用了定期删除 惰性删除,内存中仍然会存在一些没有被使用到,也没有被抽取到的 key 占据内存,因此 Redis 还提供了一系列的内存淘汰策略。

我们知道基于堆/时间轮的定时器可以很好的实现过期删除这样的任务,但是 Redis 并没有采用定时器的策略,具体原因可能是定时器需要引入多线程,但 Redis 是基于单线程实现的。

TYPE

type 用于获取 key 对应 value 的数据类型,key 不存在返回 none,时间复杂度为 O(1)。

语法及使用示例:

代码语言:javascript复制
TYPE key 
代码语言:javascript复制
127.0.0.1:6379> set key1 hello
OK
127.0.0.1:6379> lpush key2 1 2 3
(integer) 3
127.0.0.1:6379> type key1
string
127.0.0.1:6379> type key2
list
127.0.0.1:6379> type key3
none

通用命令小结

-命令

-介绍

时间复杂度

keys pattern

返回所有满足样式(pattern)的 key

O(N)

EXISTS key key …

判断一个/多个 key 是否存在

O(k), k 为查询的 key 的个数

DEL key key …

删除一个/多个指定的 key

O(k), k 为删除的 key 的个数

EXPIRE key secondsPEXPIRE key milliseconds

为指定的 key 设置秒级的过期时间为指定的 key 设置毫秒级的过期时间

O(1)

TTL keyPTTL key

获取指定 key 的秒级过期时间获取指定 key 的毫秒级过期时间

O(1)

TYPE key

获取 key 对应 value 的数据类型

O(1)

1.2 数据结构和内部编码

Redis 中有五种最常用、最重要的数据结构,它们分别是:string (字符串)、list (列表)、hash (哈希)、set (集合)、zset (有序集合)。

再次说明,Redis 是基于 key-value 的 NoSQL 数据库,即 Redis 本身就是哈希类型,且哈希的 key 只能是字符串,哈希的 value 可以为其他数据结构。

但上面这些只是 Redis 对外的数据结构,Redis 底层在实现这些数据结构的时候会在源码层面对其进行优化,为数据结构提供多种内部编码实现,从而使得 Redis 能够在合适的场景自动选择合适的内部编码,达到节省时间/空间的效果

-数据结构

内部编码-

string

raw 字节数组int 长整型embstr 短字符串

hash

hashtable 哈希表ziplist 压缩链表

list

quicklist

set

hashtable 哈希表intset 整数集合

zset

skiplist 跳表ziplist 压缩列表

可以看到很多数据结构都有两种或以上的内部编码实现,例如 string 数据结构包含了raw、int 以及 embstr 三种内部编码;同时有些内部编码,例如 ziplist,可以作为多种数据结构的内部实现。

我们可以通过 object encoding 命令来查询内部编码:

代码语言:javascript复制
127.0.0.1:6379> set key1 100
OK
127.0.0.1:6379> set key2 hello
OK
127.0.0.1:6379> set key3 weoweeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeesssssssssssseeeeee
OK
127.0.0.1:6379> object encoding key1
"int"
127.0.0.1:6379> object encoding key2
"embstr"
127.0.0.1:6379> object encoding key3
"raw"

Redis 这样设计的好处在于:

  • 可以改进内部编码,而对外的数据结构和命令没有任何影响,这样一旦开发出更优秀的内部编码,无需改动外部数据结构和命令。例如 Redis 3.2 提供了 quicklist,结合了 ziplist 和 linkedlist 两者的优势,为列表类型提供了一种更为优秀的内部编码实现,而对用户来说基本无感知。
  • 多种内部编码实现可以在不同场景下发挥各自的优势,例如 ziplist 比较节省内存,但是在哈希表元素比较多的情况下,性能会下降,这时候 Redis 会根据配置选项将哈希类型的内部实现转换为 hashtable,整个过程用户同样无感知。

2 String

2.1 类型介绍

字符串类型是 Redis 最基础的数据类型,Redis 中所有的键的类型都是字符串类型,且其他几种数据结构也都是在字符串类型基础上构建的,例如列表和集合的元素类型都是字符串。

同时,Redis 内存存储字符串是按照二进制流形式保存的,这带来两个好处:

  1. 可以使用字符串来存储任意类型的数据:字符串类型的值可以是字符串,包括一般格式的字符串或者类似 JSON、XML 格式的字符串;可以是数字,包括整型或者浮点型;也可以是二进制流数据,包括图片、音频、视频等。不过一个字符串的最大值不能超过 512 MB。
  2. Redis 基本不会遇到乱码问题:在学习 C 的时候,相信大家都被字符集编码的问题折磨过,包括 MySQL(MySQL 默认字符集是拉丁文,因此插入中文会失败),但 Redis 按二进制流保存字符串,即不处理字符集编码问题,客户端传入的命令中使用的是什么字符集编码,就存储什么字符集编码,因此基本没有乱码问题。

2.2 相关命令

2.2.1 SET/GET 系列命令

SET

set 用于将 string 类型的 value 设置进 key 中,成功返回 OK,失败返回 nil,时间复杂度为 O(1)。

注意:如果 set 的 key 之前存在,则直接覆盖 key 对应的 value,无论 value 原来的数据类型是什么,且之前关于 key 的 TTL 也会全部失效。

语法:

代码语言:javascript复制
SET key value [expiration EX seconds|PX milliseconds] [NX|XX]

从上面的语法格式可以看到,SET 命令支持多种选项来影响它的行为:

  • EX seconds:使用秒作为单位设置 key 的过期时间。
  • PX milliseconds:使用毫秒作为单位设置 key 的过期时间。
  • NX:只在 key 不存在时才进行设置,即如果 key 之前已经存在,设置不执行。
  • XX:只在 key 存在时才进行设置,即如果 key 之前不存在,设置不执行。

带选项的 SET 命令可以被 setnx、setxx、setex、psetex 命令代替,但后续的 redis 版本可能会将这些命令取消,只保留 set 命令。

使用示例:

代码语言:javascript复制
127.0.0.1:6379> set key1 hello nx ex 100
OK
127.0.0.1:6379> ttl key1
(integer) 98
127.0.0.1:6379> set key1 world nx
(nil)
127.0.0.1:6379> set key1 world
OK
127.0.0.1:6379> get key1
"world"

MSET

mset 支持一次设置多个 key,只会返回 OK,时间复杂度为 O(K),其中 k 为设置的 key 数量。

注意:mset 只是一个简单的批量设置命令,不支持 NX、XX、EX、PX 这些参数。

语法及使用示例:

代码语言:javascript复制
MSET key value [key value ...] 
代码语言:javascript复制
127.0.0.1:6379> mset key1 1 key2 2 key3 3
OK
127.0.0.1:6379> keys *
1) "key1"
2) "key3"
3) "key2"

GET

get 用于获取 key 对应的 value,如果 key 不存在返回 nil,如果 value 不是 string 类型报错,时间复杂度为 O(1)。

语法及使用示例:

代码语言:javascript复制
GET key 
代码语言:javascript复制
127.0.0.1:6379> set key1 hello
OK
127.0.0.1:6379> lpush key2 1 2 3
(integer) 3
127.0.0.1:6379> get key1
"hello"
127.0.0.1:6379> get key2
(error) WRONGTYPE Operation against a key holding the wrong kind of value

MGET

mget 支持一次获取多个 key 对应的 value,如果 key 不存在返回 nil,如果 value 不是 string 类型报错,时间复杂度为 O(k),其中 k 为获取的 key 的个数。

语法及使用示例:

代码语言:javascript复制
MGET key [key ...] 
代码语言:javascript复制
127.0.0.1:6379> mset key1 1 key2 2 key3 3
OK
127.0.0.1:6379> mget key1 key2 key3
1) "1"
2) "2"
3) "3"

使用 mset/mget 进行批量设置/获取能够有效减少网络时间,提高业务效率。

2.2.2 INCR/DECR 计数系列命令

INCR / INCRBY / INCRBYFLOAT

incr 系列命令用于增加 key 对应的 string 表示的数字,其中:

  • incr:将 value 对应的值加 1,如果 key 不存在则视为 key 对应的 value 是 0,如果 key 对应的 string 不是一个整型或者范围超过了 64 位有符号整型则报错。
  • incrby:将 value 对应的值加对应的值,如果 key 不存在则视为 key 对应的 value 是 0,如果 key 对应的 string 不是一个整型或者范围超过了 64 位有符号整型则报错。
  • incrbyfloat:将 value 对应的值加任意浮点数,如果对应的值是负数则视为减去对应的值。如果 key 不存在则视为 key 对应的 value 是 0。如果 key 对应的不是 string 或者不是一个浮点数则报错。允许采用科学计数法表示浮点数。

上述命令的返回值均为 value 增加之后的值,时间复杂度都为 O(1)。

语法及使用示例:

代码语言:javascript复制
INCR key 
INCRBY key decrement 
INCRBYFLOAT key increment 
代码语言:javascript复制
127.0.0.1:6379> mset key1 10 key2 10.5
OK
127.0.0.1:6379> incr key1 
(integer) 11
127.0.0.1:6379> incrby key1 10
(integer) 21
127.0.0.1:6379> INCRBYFLOAT key2 -1.5
"9"
127.0.0.1:6379> set key3 hello
OK
127.0.0.1:6379> incr key3
(error) ERR value is not an integer or out of range

DECR / DECRBY

decr 系列用于减少 value 对应的值,其中:

  • decr:将 key 对应的 string 表示的数字减一。如果 key 不存在,则视为 key 对应的 value 是 0。如果 key 对应的 string 不是一个整型或者范围超过了 64 位有符号整型,则报错。
  • decrby:将 key 对应的 string 表示的数字减去对应的值。如果 key 不存在,则视为 key 对应的 value 是 0。如果 key 对应的 string 不是一个整型或者范围超过了 64 位有符号整型,则报错。

上述命令的返回值均为 value 减少之后的值,时间复杂度都为 O(1)。

语法及使用示例:

代码语言:javascript复制
DECR key 
DECRBY key decrement 
代码语言:javascript复制
127.0.0.1:6379> mset key1 10 key2 hello
OK
127.0.0.1:6379> decr key1
(integer) 9
127.0.0.1:6379> decrby key1 -10
(integer) 19
127.0.0.1:6379> decr key2
(error) ERR value is not an integer or out of rang

从上面的示例可以看到,incr 系列命令与 decr 系列命令其实是可以相互替代的,减上一个负数等于加上一个正数,Redis 之所以将两种命令都设计出来是为了满足我们使用时的直觉。同时,incrbyfloat 命令没有 decr 版本,这是因为 Redis 中的计数场景一般都是使用整数。

另外,很多存储系统和编程语言内部使用 CAS 机制实现线程安全的计数功能,会有一定的 CPU 开销,但在 Redis 中完全不存在这个问题,因为 Redis 是单线程架构,任何命令到了 Redis 服务端都要顺序执行。

2.2.3 其他命令

APPEND

append 用于字符串追加,如果 key 已经存在并且是一个string,命令会将value 追加到原有 string的后边,如果 key 不存在则效果等同于 SET 命令;返回值为追加完成后 string 的长度;由于追加的字符串长度一般较短,因此时间复杂度可视为 O(1)。

语法及使用示例:

代码语言:javascript复制
APPEND KEY VALUE
代码语言:javascript复制
127.0.0.1:6379> set key1 hello
OK
127.0.0.1:6379> append key1 " world"
(integer) 11
127.0.0.1:6379> get key1
"hello world"
127.0.0.1:6379> lpush key2 1 2 
(integer) 2
127.0.0.1:6379> append key2 hello
(error) WRONGTYPE Operation against a key holding the wrong kind of value

GETRANGE

getrange 用于返回 key 对应的 string 的子串,由 start 和 end 确定(左闭右闭);可以使用负数表示倒数,-1 代表倒数第一个字符,-2 代表倒数第二个,其他的与此类似;超过范围的偏移量会根据 string 的长度调整成正确的值;时间复杂度为 O(K),其中 k 为 start,end 区间的长度。

Redis 中对于字符串的使用约定和 C/C /Java 中的有所不同:

  • 在 C/C /Java 中,我们表示区间通常是 [start, end),即左闭右开,但 Redis 中是 start, end,左闭右闭
  • 在 C/C ,数组下标越界是未定义行为,编译器并不会对数组下标进行合法性检查,因此可能会导致程序崩溃、可能会得到一个错误的结果,也可能会恰好得到一个正确的结果。在 Java 中,JVM 负责数组下标合法性检查,因此下标越界会直接抛出异常;而在 Redis 中,Redis 会将超过范围的偏移量自动调整为正确的值。

语法及使用示例:

代码语言:javascript复制
GETRANGE key start end 
代码语言:javascript复制
127.0.0.1:6379> set key "hello world"
OK
127.0.0.1:6379> GETRANGE key 0 -1
"hello world"
127.0.0.1:6379> GETRANGE key 6 100
"world"
127.0.0.1:6379> GETRANGE key 20 100
""
127.0.0.1:6379> getrange 20 10
(error) ERR wrong number of arguments for 'getrange' command

SETRANGE

setrange 用于从指定的位置开始覆盖字符串的一部分,覆盖的长度与给定符串的长度相同;返回值为覆盖后 string 的长度;时间复杂度为 O(k),其中 k 为给定字符串的长度。

语法及使用示例:

代码语言:javascript复制
SETRANGE key offset value 
代码语言:javascript复制
127.0.0.1:6379> set key hello
OK
127.0.0.1:6379> SETRANGE key 1 a
(integer) 5
127.0.0.1:6379> get key
"hallo"
127.0.0.1:6379> setrange key 1 "ello world"
(integer) 11
127.0.0.1:6379> get key
"hello world"

在使用 getrange/setrange 操作中文字符串时需要特别注意:

由于 Redis 中的字符串是字节数组,同时一个汉字在不同编码实现中占的字节数不同,比如 utf-8 中占三个字节,Unicode 中占两个字节,因此我们在计算中文字符串的下标时需要格外小心。

另外,redis-cli 默认显示原始字节数据,如果我们想要让其显示中文,需要在启动 redis-cli 时指定 --raw 选项让其尝试翻译二进制数据。

代码语言:javascript复制
127.0.0.1:6379> set key2 "你好"
OK
127.0.0.1:6379> get key2
"xe4xbdxa0xe5xa5xbd"

STRLEN

strlen 用于获取 key 对应的 string 的长度,当 key 存放的类型不是 string 时报错;时间复杂度为 O(1)。

语法及使用示例:

代码语言:javascript复制
STRLEN key 
代码语言:javascript复制
127.0.0.1:6379> set key1 hello
OK
127.0.0.1:6379> strlen key1
(integer) 5
127.0.0.1:6379> lpush key2 1 2 3
(integer) 3
127.0.0.1:6379> strlen key2
(error) WRONGTYPE Operation against a key holding the wrong kind of value
2.2.4 命令小结

-命令

-介绍

时间复杂度

SET key value expiration EX seconds | PX millisecondsMSET key value key value …

设置 key 对应的 value批量设置 key 对应的 value

O(1)

GET key MGET key key …

获取 key 对应的 value批量获取 key 对应的 value

O(1)

INCR key INCRBY key decrement INCRBYFLOAT key increment

增加 key 对应 value 的值

O(1)

DECR key DECRBY key decrement

减少 key 对应 value 的值

O(1)

APPEND KEY VALUE

字符串追加

O(1)

GETRANGE key start end

获取子串

O(K)

SETRANGE key offset value

插入并覆盖字符串

O(K)

STRLEN key

获取字符串长度

O(1)

2.3 内部编码

string 有三种内部编码方式:

  • int:64 位整数,用于表示整数。
  • embstr:压缩字符串,当字符串较短且内存占用较小时,Redis 将字符串数据直接存储在对象结构中,从而减少内存分配和指针跳转,提高访问效率。
  • raw:字符数组,当字符串较长或内存占用较大时,Redis 会为字符串数据分配一个单独的内存块 (字节数组),并通过指针将字符串对象与数据块关联起来。

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

代码语言:javascript复制
127.0.0.1:6379> set key1 100
OK
127.0.0.1:6379> set key2 hello
OK
127.0.0.1:6379> set key3 weoweeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeesssssssssssseeeeee
OK
127.0.0.1:6379> object encoding key1
"int"
127.0.0.1:6379> object encoding key2
"embstr"
127.0.0.1:6379> object encoding key3
"raw"

大家如果上网搜索可以发现 Redis string embstr 向 raw 编码的转换的临界点是 39 个字节,但是这里并不提倡大家去记忆这样的数字,因为这种数字大多数情况下都是可以通过配置项进行修改的,在实际工作中,我们需要根据公司实际业务需求来调整这些数字。(如果 Redis 没有提供相关配置项,我们可能还需要修改 Redis 源码来满足我们的业务需求)

2.4 应用场景

2.4.1 缓存功能

缓存是 string 类型典型的应用场景,使用 Redis 作为缓存层,MySQL 作为存储层:

  • 应用服务器访问数据时,先查询 Redis,如果 Redis 上数据存在就直接从 Redis 取数据交给应用服务器,不再需要访问 MySQL;
  • 如果 Redis 上数据不存在,则读取 MySQL,把读到的结果返回给应用服务器,同时把这个数据写入到 Redis 中,便于下次访问。

下面是业务数据访问的伪代码:

1)假设业务是根据用户 uid 获取用户信息:

代码语言:javascript复制
UserInfo getUserInfo(uint64_t uid) {
	...
}

2)首先从 Redis 获取用户信息,我们假设用户信息保存在 “user:info:<uid>” 对应的键中,如果缓存命中则直接返回:

代码语言:javascript复制
// 根据 uid 得到 Redis 的键
String key = "user:info:"   uid;

// 尝试从 Redis 中获取对应的值
String value = Redis 执⾏命令:get key;

// 如果缓存命中(hit)
if (value != null) {
    // 假设我们的用户信息按照 JSON 格式存储
    UserInfo userInfo = JSON 反序列化(value);
    return userInfo;
}

3)如果没有从Redis 中得到用户信息,即缓存 miss,则进一步从MySQL中获取对应的信息,随后写入缓存并返回:

代码语言:javascript复制
// 如果缓存未命中(miss)
if (value == null) {
    // 从数据库中,根据 uid 获取用户信息
    UserInfo userInfo = MySQL 执行 SQL:select * from user_info where uid = <uid>

    // 如果表中没有 uid 对应的用户信息
    if (userInfo == null) {
        // 响应 404
        return null;
	}

    // 将用户信息序列化成 JSON 格式
    String value = JSON 序列化(userInfo);

    // 写⼊缓存,为了防⽌数据腐烂(rot),设置过期时间为 1 ⼩时(3600 秒)
    Redis 执⾏命令:set key value ex 3600

    // 返回用户信息
    return userInfo;
}

通过增加缓存功能,在理想情况下,每个用户信息一个小时期间只会有一次 MySQL 查询,极大地提升了查询效率,也降低了 MySQL 的访问数。

与 MySQL 等关系型数据库不同的是,Redis 没有表、字段这种命名空间,且对键名也没有强制要求(除了不能使用一些特殊字符)。但设计合理的键名,有利于防止键冲突和项目的可维护性,比较推荐的方式是使用 “业务名:对象名:唯一标识:属性” 作为键名。

例如 MySQL 的数据库名为 vs,用户表名为 user_info,那么对应的键可以使用 “vs:user_info:6379”、“vs:user_info:6379:name” 来表示,如果当前 Redis 只会被一个业务使用,可以省略业务名 “vs:”。如果键名过长,则可以使用团队内部都认同的缩写替代,例如 “user:6379:friends:messages:5217” 可以被 “u:6379 : fr: m:5217” 代替,毕竟键名过长,还是会导致 Redis 的性能明显下降的。

2.4.2 计数功能

许多应用都会使用 Redis 作为计数的基础工具,它可以实现快速计数、查询缓存的功能,同时数据可以异步处理或者落地到其他数据源

例如:视频网站的视频播放次数可以使用 Redis 来完成,用户每播放一次视频,相应的视频播放数就会自增 1。

业务伪代码如下:

代码语言:javascript复制
// 在 Redis 中统计某视频的播放次数
long incrVideoCounter(long vid) {
    key = "video:"   vid;
    long count = Redis 执⾏命令:incr key
    return counter;
}

实际中要开发⼀个成熟、稳定的真实计数系统,要⾯临的挑战远不⽌如此简单:防作弊、按照不同维度计数、避免单点问题、数据持久化到底层数据源等。

2.4.3 共享会话

如下图所示,一个分布式 Web 服务将用户的 Session 信息(例如用户登录信息)保存在各自的服务器中,但这样会造成一个问题:出于负载均衡的考虑,分布式服务会将用户的访问请求均衡到不同的服务器上,并且通常无法保证用户每次请求都会被均衡到同一台服务器上,这样当用户刷新后,下一次访问可能会发现需要重新登录,这个问题是用户无法容忍的。

为了解决这个问题,可以使用 Redis 将用户的 Session 信息进行集中管理。在这种模式下,只要保证 Redis 是高可用和可扩展性的,无论用户被均衡到哪台 Web 服务器上,都集中从 Redis 中查询、更新 Session 信息。

2.4.4 验证码功能

很多应用出于安全考虑,会在每次进行登录时,让用户输入手机号并且配合给手机发送验证码,然后让用户再次输入收到的验证码并进行验证,以此来确定是否是用户本人。为了短信接口不会频繁访问,会限制用户每分钟获取验证码的频率,例如一分钟不能超过 5 次。

上述功能可以通过给 Redis key 设置过期时间来实现。下面是伪代码:

代码语言:javascript复制
String 发送验证码(String phoneNumber) {
    String key = "shortMsg:limit:"   phoneNumber;
    // 设置过期时间为 1 分钟(60 秒)
    // 使用 NX,只在不存在 key 时才能设置成功
    boolean r = Redis 执行命令:set(key, 1, "EX", 60, "NX");
    if (!r) {
        // 说明之前设置过该手机的验证码了
        long c = Redis 执行命令:incr(key);
        if (c > 5) {
            // 说明超过了一分钟 5 次的限制了
            // 限制发送
            return null;
        }
    }
    
    // 说明要么之前没有设置过手机的验证码;要么次数没有超过 5 次
    String validationCode = 生成随机的 6 位数的验证码();
    String validationKey = "validation:"   phoneNumber;
    
    // 验证码 5 分钟(300 秒)内有效
    Redis 执行命令:set(validationKey, validationCode, "EX", 300);
    
    // 返回验证码,随后通过手机短信发送给用户
    return validationCode;
}

// 验证用户输入的验证码是否正确
boolean 验证验证码(String phoneNumber, String validationCode) {
    String validationKey = "validation:"   phoneNumber;
    
    String value = Redis 执行命令:get(validationKey);
    if (value == null) {
        // 说明没有这个手机的验证码记录,验证失败
        return false;
    }
    
    if (value.equals(validationCode)) {
        return true;
    } else {
        return false;
    }
}

3 Hash

3.1 类型介绍

在 Redis 中,哈希类型是指值本身又是一个键值对结构,形如 key = “key”,value = { { field1, value1 }, …, { fieldN, valueN } }。字符串和哈希的对比如下图所示。

为了与 Redis 本身的键值对结构 key-value 进行区分,我们通常将 value 哈希数据结构中的键值对称为 field-value

3.2 相关命令

3.2.1 HSET/HGET/HDEL 系列命令

HSET

hset 用于设置一组/多组 hash 中指定字段(field)的值(value);返回值为添加的字段的个数;时间复杂度为 O(K),其中 K 为设置的字段的个数。

语法及使用示例:

代码语言:javascript复制
HSET key field value [field value ...] 
代码语言:javascript复制
127.0.0.1:6379> hset myhash field1 1 field2 hello
(integer) 2
127.0.0.1:6379> hvals myhash
1) "1"
2) "hello"

HSETNX

hsetnx 用于在 field 不存在的情况下设置 value 的值;返回1表示成功,0表示失败;时间复杂度为 O(1)。

语法及使用示例:

代码语言:javascript复制
HSETNX key field value
代码语言:javascript复制
127.0.0.1:6379> hkeys myhash
1) "field1"
2) "field2"
127.0.0.1:6379> hsetnx myhash field1 hello
(integer) 0
127.0.0.1:6379> hsetnx myhash field3 world
(integer) 1
127.0.0.1:6379> hkeys myhash
1) "field1"
2) "field2"
3) "field3"

HGET

hget 用于获取指定 field 的值;返回值为字符串,field 不存在返回 nil,时间复杂度为 O(1)。

语法及使用示例:

代码语言:javascript复制
HGET key field 
代码语言:javascript复制
127.0.0.1:6379> hget myhash field1
"1"
127.0.0.1:6379> hget myhash field2
"hello"

HMGET

hmget 用于一次获取多个 field 的值;返回字段对应的值或 nil;时间复杂度为 O(k),其中 k 为获取的 field 的个数。

语法及使用示例:

代码语言:javascript复制
HMGET key field [field ...] 
代码语言:javascript复制
127.0.0.1:6379> hmget myhash field1 field2 field3
1) "1"
2) "hello"
3) "world"

HGETALL

hgetall 用于获取 key 所有的 field-value;时间复杂度为 O(N),其中 N 为哈希表中的元素个数。

语法及使用示例:

代码语言:javascript复制
HGETALL key 
代码语言:javascript复制
127.0.0.1:6379> hkeys myhash
1) "field1"
2) "field2"
3) "field3"
127.0.0.1:6379> hgetall myhash
1) "field1"
2) "1"
3) "field2"
4) "hello"
5) "field3"
6) "world"

HDEL

hdel 用于删除哈希中指定的一个/多个 field;返回删除的字段数;时间复杂度为 O(K),其中 k 为删除的 field 数。

语法及使用示例:

代码语言:javascript复制
HDEL key field [field ...] 
代码语言:javascript复制
127.0.0.1:6379> hkeys myhash
1) "field1"
2) "field3"
3) "field2"
127.0.0.1:6379> hdel myhash field1 field2
(integer) 2
127.0.0.1:6379> hkeys myhash
1) "field3"

注意区分通用命令 del 与哈希中的 hdel,前者用于删除指定的 key,后者用于删除哈希中指定的 field。

3.2.2 HINCR 计数系列命令

Redis 哈希表中也支持计数相关命令,用于增加 field 对应 value 的值,不过只支持 incrby 与 incrbyfloat 命令,返回值及注意事项等与 string 中 incr 系列命令相同。

语法及使用示例:

代码语言:javascript复制
HINCRBY key field increment 
HINCRBYFLOAT key field increment
代码语言:javascript复制
127.0.0.1:6379> hset myhash field1 10 field2 10.5
(integer) 2
127.0.0.1:6379> hincrby myhash field1 -5
(integer) 5
127.0.0.1:6379> hincrbyfloat myhash field2 2
"12.5"
127.0.0.1:6379> hvals myhash
1) "5"
2) "12.5"
3.2.3 其他命令

HEXISTS

hexists 用于判断某个 field 是否存在;返回1表示存在,0表示不存在;时间复杂度为 O(1)。

语法及使用示例:

代码语言:javascript复制
HEXISTS key field
代码语言:javascript复制
127.0.0.1:6379> hkeys myhash
1) "field1"
2) "field2"
127.0.0.1:6379> hexists myhash field1
(integer) 1
127.0.0.1:6379> HEXISTS myhash field3
(integer) 0

HKEYS

hkeys 用于获取哈希表中所有的 filed;时间复杂度为 O(N),其中 N 为哈希表中的元素个数。

语法及使用示例:

代码语言:javascript复制
HKEYS key 
代码语言:javascript复制
127.0.0.1:6379> HKEYS myhash
1) "field1"
2) "field2"

HVALS

hvals 用于获取哈希表中所有 field 对应的 value;时间复杂度为 O(N),其中 N 为哈希表中的元素个数。

语法及使用示例:

代码语言:javascript复制
HVALS key 
代码语言:javascript复制
127.0.0.1:6379> HVALS myhash
1) "5"
2) "12.5"

hgetall / hkeys / hvals 时间复杂度过高,强烈不建议在生产环境中使用。

HLEN

hlen 用于获取哈希表中的元素个数,时间复杂度为 O(1)。

语法及使用示例:

代码语言:javascript复制
HLEN key
代码语言:javascript复制
127.0.0.1:6379> HKEYS myhash
1) "field1"
2) "field2"
127.0.0.1:6379> hlen myhash
(integer) 2
3.2.4 命令小结

-命令

-介绍

时间复杂度

HSET key field value field value … HSETNX key field value

设置 filed 对应的 value当 field 不存在时设置 value

O(K)O(1)

HGET key field HMGET key field field … HGETALL key

获取 field 对应的 value获取多组 field 对应的 value获取所有 field 对应的 value

O(1)O(K)O(N)

HDEL key field field …

删除指定的 field

O(1)

HINCRBY key field increment HINCRBYFLOAT key field increment

增加 field 对应 value 的值

O(1)

HEXISTS key field

判断 field 是否存在

O(1)

HKEYS key

获取哈希表中所有的 field

O(N)

HVALS key

获取哈希表中所有 field 对应的 value

O(N)

HLEN key

获取哈希表中 field 的个数

O(1)

HSTRLEN key field

获取 field 对应 value 的长度

O(1)

3.3 内部编码

哈希的内部编码有两种:

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

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

代码语言:javascript复制
127.0.0.1:6379> hset hash1 f1 v1 f2 v2
(integer) 2
127.0.0.1:6379> object encoding hash1
"ziplist"

2)当有 value 大于64 字节时,内部编码会转换为 hashtable:

代码语言:javascript复制
127.0.0.1:6379> hset hashkey f3 "one string is bigger than 64 bytes ... 省略 ..."
(integer) 1
127.0.0.1:6379> object encoding hashkey
"hashtable"

3)当 field 个数超过512时,内部编码也会转换为 hashtable:

代码语言:javascript复制
127.0.0.1:6379> hmset hashkey f1 v1 h2 v2 f3 v3 ... 省略 ... f513 v513
(integer) 513
127.0.0.1:6379> object encoding hashkey
"hashtable"

3.4 应用场景

哈希的典型应用场景也是作为缓存,使用 hash 与使用 string 作为缓存的区别如下:

  • string:将用户信息格式化为 json 格式字符串进行表示。 优点在于针对总是以整体作为操作的信息比较合适,编程也简单。同时,如果序列化方案选择合适,内存的使用效率很高; 缺点在于本身序列化和反序列需要一定开销,同时如果总是操作个别属性则非常不灵活,依然需要获取所有的属性。
  • hash:使用 hash 表示格式化的用户信息。 优点在于更加直观,并且在更新操作上变得更灵活,可以仅对单个字段进行操作。 缺点在于需要控制哈希在 ziplist和 hashtable 两种内部编码的转换,可能会造成一定的内存消耗。

使用 hash 作为缓存时,可以将每个用户的 id 定义为键后缀,多对 field-value 对应用户的各个属性,类似如下伪代码:

代码语言:javascript复制
UserInfo getUserInfo(long uid) {
    // 根据 uid 得到 Redis 的键
    String key = "user:"   uid;
    
    // 尝试从 Redis 中获取对应的值
    Map<String, String> userInfoMap = Redis 执行命令:hgetall(key);
    
    // 如果缓存命中(hit)
    if (userInfoMap != null) {
        // 将映射关系还原为对象形式
        UserInfo userInfo = 利用映射关系构建对象(userInfoMap);
        return userInfo;
    }
    
    // 如果缓存未命中(miss)
    // 从数据库中,根据 uid 获取用户信息
    UserInfo userInfo = MySQL 执行 SQL:select * from user_info where uid = uid;
    
    // 如果表中没有 uid 对应的用户信息
    if (userInfo == null) {
        响应 404;
        return null;
    }
    
    // 将缓存以哈希类型保存
    Redis 执行命令:hmset(key, "name", userInfo.name, "age", userInfo.age, "city", userInfo.city);
    
    // 写入缓存,为了防止数据腐烂(rot),设置过期时间为 1 小时(3600 秒)
    Redis 执行命令:expire(key, 3600);
    
    // 返回用户信息
    return userInfo;
}

需要注意的是:

  • 哈希类型是稀疏的,而关系型数据库是完全结构化的,例如哈希类型每个键可以有不同的 field,而关系型数据库一旦添加新的列,所有行都要为其设置值,即使为 null。
  • 关系数据库可以做复杂的关系查询,而 Redis 去模拟关系型复杂查询,例如联表查询、聚合查询等基本不可能,维护成本太高。

4 List

4.1 类型介绍

列表类型用于存储多个有序的字符串,类似于 std::dequeue,在双端插入删除都很高效。如下图所示,a、b、c、d、e 五个元素从左到右组成了一个有序的列表,列表中的每个字符串称为元素(element),一个列表最多可以存储 2^32 - 1 个元素。在 Redis 中,可以对列表两端执行插入(push)和弹出(pop)操作,还可以获取指定范围的元素列表、获取指定索引下标的元素等。列表是一种比较灵活的数据结构,它可以充当栈和队列的角色,在实际开发上有很多应用场景。

同侧存取 (lpush lpop 或 rpush rpop) 为,异侧存取 (rpush lpop 或 lpush rpop) 为队列

4.2 相关命令

4.2.1 PUSH/POP 系列命令

LPUSH

lpush 用于将一个/多个元素从左侧插入到列表中(头插);返回值为插入后列表的长度;时间复杂度为 O(K),其中 k 为插入的元素的个数。

语法及使用示例:

代码语言:javascript复制
LPUSH key element [element ...] 
代码语言:javascript复制
127.0.0.1:6379> lpush mylist 1 2 3 4
(integer) 4
127.0.0.1:6379> lrange mylist 0 -1
1) "4"
2) "3"
3) "2"
4) "1"

LPUSHX

lpushx 仅在 key 存在时,将一个/多个元素从左侧插入到列表中(头插);返回值为插入后列表的长度;时间复杂度为 O(K),其中 k 为插入的元素的个数。

语法及使用示例:

代码语言:javascript复制
LPUSHX key element [element ...] 
代码语言:javascript复制
127.0.0.1:6379> keys *
1) "mylist"
127.0.0.1:6379> lpushx mylist 5 6
(integer) 6
127.0.0.1:6379> lpushx mylist1 1 2 3
(integer) 0

RPUSH / RPUSHX

rpush 用于将一个/多个元素从右侧插入到列表中(尾插);返回值为插入后列表的长度;时间复杂度为 O(K),其中 k 为插入的元素的个数。

语法及使用示例:

代码语言:javascript复制
RPUSH key element [element ...] 
RPUSHX key element [element ...] 
代码语言:javascript复制
127.0.0.1:6379> rpush mylist 1 2 3 4
(integer) 4
127.0.0.1:6379> lrange mylist 0 -1
1) "1"
2) "2"
3) "3"
4) "4"

LPOP

lpop 用于删除左侧的元素并返回(头删);当列表为空时返回 nil;时间复杂度为 O(1)。

语法及使用示例:

代码语言:javascript复制
LPOP key 
代码语言:javascript复制
127.0.0.1:6379> lrange mylist 0 -1
1) "1"
2) "2"
3) "3"
4) "4"
127.0.0.1:6379> lpop mylist
"1"
127.0.0.1:6379> lrange mylist 0 -1
1) "2"
2) "3"
3) "4"

RPOP

rpop 用于删除右侧的元素并返回(尾删);当列表为空时返回 nil;时间复杂度为 O(1)。

语法及使用示例:

代码语言:javascript复制
RPOP key 
代码语言:javascript复制
127.0.0.1:6379> lrange mylist 0 -1
1) "2"
2) "3"
3) "4"
127.0.0.1:6379> rpop mylist
"4"
127.0.0.1:6379> lrange mylist 0 -1
1) "2"
2) "3"
4.2.2 BLPOP/BRPOP 阻塞命令

blpop 和 brpop 是 lpop 和 rpop 的阻塞版本,和对应非阻塞版本的作⽤基本一致,不同在于:

  • 在列表中有元素的情况下,阻塞和非阻塞表现是一致的。但如果列表中没有元素,非阻塞版本会立即返回 nil,但阻塞版本会根据 timeout 阻塞一段时间,期间 Redis 可以执行其他命令,但要求执行该命令的客户端会表现为阻塞状态。
  • 命令中如果设置了多个键(同时阻塞 pop 多个 key 中 value),那么会从左向右进行遍历键,一旦有一个键对应的列表中可以弹出元素,命令立即返回。
  • 如果多个客户端同时对一个键执行 pop,则最先执行命令的客户端会得到弹出的元素。

语法如下:

代码语言:javascript复制
BLPOP key [key ...] timeout 
BRPOP key [key ...] timeout 

阻塞命令需要使用多个列表、多个客户端按照上面三种情况逐一配置,然后现场观察 Redis 的执行结果,建议大家自行尝试。

4.2.3 其他命令

LSET

lset 用于设置并覆盖指定下标位置的元素;成功返回 OK,列表不存在或下标非法时报错;时间复杂度为 O(N),其中 N 为列表的长度。

语法及使用示例:

代码语言:javascript复制
LSET key index element
代码语言:javascript复制
127.0.0.1:6379> rpush mylist one two three
(integer) 3
127.0.0.1:6379> lrange mylist 0 -1
1) "one"
2) "two"
3) "three"
127.0.0.1:6379> lset mylist 0 four
OK
127.0.0.1:6379> lset mylist -1 five
OK
127.0.0.1:6379> lrange mylist 0 -1
1) "four"
2) "two"
3) "five"
127.0.0.1:6379> lset mylist 100 six
(error) ERR index out of range
127.0.0.1:6379> lset list1 0 1
(error) ERR no such key

与 list lrange 以及 string getrange 命令会自动处理下标越界不同,当下标非法时 lset 命令会直接报错。

LINSERT

linsert 用于在特定元素前面或后面插入元素;返回值为插入后列表的长度;时间复杂度为 O(N)。

需要特别注意的是,linsert 插入位置是以列表元素,而不是不是元素的下标为基准的。当列表中存在多个相同元素时,linsert 会在第一个元素的前面或后面进行插入。

语法及使用示例:

代码语言:javascript复制
LINSERT key <BEFORE | AFTER> pivot element 
代码语言:javascript复制
127.0.0.1:6379> rpush mylist 1 2 3 2 5 4
(integer) 6
127.0.0.1:6379> lrange mylist 0 -1
1) "1"
2) "2"
3) "3"
4) "2"
5) "5"
6) "4"
127.0.0.1:6379> linsert mylist before 2 -1
(integer) 7
127.0.0.1:6379> linsert mylist after 2 -2
(integer) 8
127.0.0.1:6379> lrange mylist 0 -1
1) "1"
2) "-1"
3) "2"
4) "-2"
5) "3"
6) "2"
7) "5"
8) "4"

LREM

lrem (list remove) 用于删除列表中某个特定的元素;返回值为删除的元素个数;时间复杂度为 O(N M),其中N是列表的长度,M是删除的元素数。

语法如下:

代码语言:javascript复制
LREM key count element

关于 count 参数有三个不同的取值:

  • count > 0 :从头到尾删除 count 个值为 element 的元素。
  • count < 0:从尾到头删除等于 count 个值为 element 的元素。
  • count = 0:删除所有值为 element 的元素。

使用示例如下:

代码语言:javascript复制
127.0.0.1:6379> rpush mylist 1 1 2 1 1 2 1
(integer) 7
127.0.0.1:6379> lrem mylist 2 1
(integer) 2
127.0.0.1:6379> lrange mylist 0 -1
1) "2"
2) "1"
3) "1"
4) "2"
5) "1"
127.0.0.1:6379> lrem mylist -1 1
(integer) 1
127.0.0.1:6379> lrange mylist 0 -1
1) "2"
2) "1"
3) "1"
4) "2"
127.0.0.1:6379> lrem mylist 0 1
(integer) 2
127.0.0.1:6379> lrange mylist 0 -1
1) "2"
2) "2"

LTRIM

ltrim 的作用是修剪列表,使其仅保留 start, stop 区间中的元素;成功返回 OK;时间复杂度为 O(N),其中 N 为被删除的元素个数。

语法及使用示例:

代码语言:javascript复制
LTRIM key start stop
代码语言:javascript复制
127.0.0.1:6379> rpush mylist 1 2 3 4 5
(integer) 5
127.0.0.1:6379> ltrim mylist 2 3
OK
127.0.0.1:6379> lrange mylist 0 -1
1) "3"
2) "4"

LRANGE

lrange 用于获取列表 start, end 区间的所有元素,当下标非法时,Redis 会尝试自动处理;时间复杂度为 O(N),其中 N 为列表长度。

语法及使用示例:

代码语言:javascript复制
LRANGE key start end
代码语言:javascript复制
127.0.0.1:6379> rpush mylist 1 2 3
(integer) 3
127.0.0.1:6379> LRANGE mylist 0 -1
1) "1"
2) "2"
3) "3"
127.0.0.1:6379> LRANGE mylist 0 1
1) "1"
2) "2"
127.0.0.1:6379> LRANGE mylist 0 100
1) "1"
2) "2"
3) "3"

LINDEX

lindex 用于获取列表 index 下标位置的元素;时间复杂度为 O(N),其中 N 为列表长度。

语法及使用示例:

代码语言:javascript复制
LINDEX key index 
代码语言:javascript复制
127.0.0.1:6379> LRANGE mylist 0 100
1) "1"
2) "2"
3) "3"
127.0.0.1:6379> lindex mylist 0
"1"
127.0.0.1:6379> lindex mylist 10
(nil)

LLEN

llen 用于获取列表长度;时间复杂度为 O(1)。

代码语言:javascript复制
LINDEX key index 
代码语言:javascript复制
127.0.0.1:6379> llen mylist
(integer) 3
4.2.4 命令小结

-命令

介绍-

-时间复杂度

LPUSH key element element … LPUSHX key element element …

列表头插

O(1)

RPUSH key element element … RPUSHX key element element …

列表尾插

O(1)

LINSERT key <BEFORE | AFTER> pivot element

列表中间插入

O(N)

LPOP key

列表头删

O(1)

RPOP key

列表尾删

O(1)

LREM key count element

删除列表中的特定元素,支持指定数量

O(N M)

LTRIM key start stop

删除区间外列表的所有元素

O(N)

LSET key index element

修改列表指定下标位置的元素

O(N)

LRANGE key start end

获取区间中的列表元素

O(N)

LINDEX key index

获取列表指定下标元素

O(N)

LINDEX key index

获取列表长度

O(1)

BLPOP key key … timeout BRPOP key key … timeout

阻塞版本的 POP

O(1)

4.3 内部编码

早期 Redis 使用 ziplist 以及 linklist 作为列表的内部编码,但在 Redis 3.2 版本,Redis 引入了 quicklist 作为列表类型的内部编码,用以替代之前的 ziplist 和 linkedlist。

quicklist 结合了 ziplist 和 linklist 的优点,在两端插入删除的效率都很高。首先,它是一种双向链表(linked list),同时,它由多个 ziplist 组成,每个 ziplist 可以看作是链表的一个节点,这样可以利用 ziplist 的紧凑存储特性,同时避免了单个 ziplist 过大时的性能问题。

我们可以通过 list-max-ziplist-size 配置项来控制每个 ziplist 节点的最大内存大小,以及通过 list-compress-depth 来控制压缩的深度,即在何时将 ziplist 转换为 quicklist。

代码语言:javascript复制
127.0.0.1:6379> rpush mylist 1 2 3 4
(integer) 4
127.0.0.1:6379> object encoding mylist
"quicklist"

4.4 应用场景

4.4.1 消息队列

Redis 可以使用 lpush brpop 命令组合实现经典的阻塞式生产者-消费者模型队列,生产者客户端使用 lpush 从列表左侧插入元素,多个消费者客户端使用 brpop 命令阻塞式地从队列中 “争抢” 队首元素。通过多个客户端来保证消费的负载均衡(轮询)和高可用性。

同时,我们还可以实现分频道的消息队列:通过不同的键模拟频道的概念,不同的消费者可以通过 brpop 不同的键值,实现订阅不同频道的理念。

4.4.2 微博 Timeline

微博的每个用户都有属于自己的 Timeline(微博列表),我们可以使用列表来完成 Timeline 的分页展示,因为列表不仅是有序的,同时支持按照索引范围获取元素。

数据伪代码如下:

1)每篇微博使用哈希结构存储,假如微博中3个属性: title、timestamp、content:

代码语言:javascript复制
hmset mblog:1 title xx timestamp 1476536196 content xxxxx
...
hmset mblog:n title xx timestamp 1476536196 content xxxxx

2)向用户 Timeline 添加微博,user:<uid>:mblogs 作为微博的键:

代码语言:javascript复制
lpush user:1:mblogs mblog:1 mblog:3
...
lpush user:k:mblogs mblog:9

3)分页获取用户的 Timeline,例如获取用户1的前10 篇微博:

代码语言:javascript复制
keylist = lrange user:1:mblogs 0 9
for key in keylist {
    hgetall key
}

上述方案在实际中可能存在两个问题:

  1. 1 n 问题,即如果每次分页获取的微博个数较多,需要执行多次 hgetall 操作,此时可以考虑使用 pipeline(流水线)模式批量提交命令,或者微博不采用哈希类型,而是使用序列化的字符串类型,使用 mget 获取。
  2. 分页获取文章时,lrange 在列表两端表现较好,获取列表中间的元素表现较差,此时可以考虑将列表做拆分,将一个大的列表拆分为多个较小的列表。

5 Set

5.1 类型介绍

集合类型用于将一些有关联的数据放在一起,这些元素被称为集合中的成员 member, 集合中的每个 member 都是字符串类型,一个集合中最多可以存储 2^32 - 1 个 member。集合中的元素有如下特点:

  1. 集合中的元素是无序的。例如 {2, 1, 3} 与 {3, 1, 2} 是同一个集合。
  2. 集合中不允许出现重复元素。

Redis 除了支持集合内的增删查改操作,同时还支持多个集合取交集、并集、差集,合理地使用好集合类型,能在实际开发中解决很多问题。

注意与 C 中的集合 std::set 相区别,std::set 底层使用红黑树实现,因此元素是有序的 (以搜索树的形式进行组织);而 Redis 中的 set 底层则更多使用哈希表实现,因此元素是无序的;不要看到 set 就认为底层是红黑树,这会导致我们误判时间复杂度。

5.2 相关命令

5.2.1 SADD/SPOP 系列命令

SADD

sadd 命令用于将一个或多个元素添加到 Set 中,不能添加重复元素;返回值为成功添加的元素个数;时间复杂度为 O(K),其中 k 为添加的元素的个数。

语法及使用示例:

代码语言:javascript复制
SADD key member [member ...] 
代码语言:javascript复制
127.0.0.1:6379> sadd key hello world
(integer) 2
127.0.0.1:6379> smembers key
1) "world"
2) "hello"

SPOP

spop 命令用于从集合中随机删除一个或多个元素;返回值为被删除的元素个数;时间复杂度为 O(K),其中 k 为删除的元素的个数。

注意:

由于 set 内的元素是无序的,所以取出哪个元素实际上是未定义行为,即可以看作是随机的。 并且这个随机是真随机,即使我们构造两个相同的集合,使用 spop 命令删除元素的顺序也是不同的。实际上 Redis 上源码中删除元素时采用了 “生成随机数” 的方式。

语法及使用示例:

代码语言:javascript复制
SPOP key [count]
代码语言:javascript复制
127.0.0.1:6379> sadd key1 1 2 3 4
(integer) 4
127.0.0.1:6379> sadd key2 1 2 3 4
(integer) 4
127.0.0.1:6379> spop key1 2
1) "3"
2) "1"
127.0.0.1:6379> spop key2 2
1) "1"
2) "2"
127.0.0.1:6379> smembers key1
1) "2"
2) "4"
127.0.0.1:6379> smembers key2
1) "3"
2) "4"

SREM

srem 命令用于从 Set 中删除一个或多个指定元素;返回值为被删除的元素个数;时间复杂度为 O(K),其中 k 为删除的元素的个数。

语法及使用示例:

代码语言:javascript复制
SREM key member [member ...] 
代码语言:javascript复制
127.0.0.1:6379> sadd key 1 2 3 4
(integer) 4
127.0.0.1:6379> srem key 2 4
(integer) 2
127.0.0.1:6379> smembers key
1) "1"
2) "3"

SMOVE

smove 命令用于将一个元素从源 set 取出并放入目标 set 中;返回1表示成功,0表示失败;时间复杂度为 O(1)。

注意:

  • 如果目标集合不存在,smove 会创建目标集合并将元素放入其中。
  • 如果将 key1 中的 member1 smove 到 key2 中,然后再向 key1 中 sadd menber1 并 smove 到 key2,Redis 并不会报错。

语法及使用示例:

代码语言:javascript复制
SMOVE source destination member 
代码语言:javascript复制
127.0.0.1:6379> sadd key1 1 2 3 4
(integer) 4
127.0.0.1:6379> smove key1 key2 4
(integer) 1
127.0.0.1:6379> sadd key1 4
(integer) 1
127.0.0.1:6379> smove key1 key2 4
(integer) 1
127.0.0.1:6379> smembers key1
1) "1"
2) "2"
3) "3"
127.0.0.1:6379> smembers key2
1) "4"
5.2.2 SMEMBER 成员系列命令

SMEMBERS

smembers 命令用于获取集合中的所有元素,需要注意元素是无序的;时间复杂度为 O(N)。

语法及使用示例:

代码语言:javascript复制
SMEMBERS key 
代码语言:javascript复制
127.0.0.1:6379> sadd key 1 2 3 4
(integer) 4
127.0.0.1:6379> smembers key
1) "1"
2) "2"
3) "3"
4) "4"

SISMEMBER

sismember 用于判断一个元素在不在 Set 中;返回1表示在,0表示不在;时间复杂度为 O(1)。

语法及使用示例:

代码语言:javascript复制
SISMEMBER key member 
代码语言:javascript复制
127.0.0.1:6379> smembers key
1) "1"
2) "2"
3) "3"
4) "4"
127.0.0.1:6379> sismember key 4
(integer) 1
127.0.0.1:6379> sismember key 10
(integer) 0

SRANDMEMBER

srandmember 用于从集合中随机获取一个或多个元素;时间复杂度为 O(K),其中 k 为获取的元素的个数。

语法及使用示例:

代码语言:javascript复制
SRANDMEMBER key [count]
代码语言:javascript复制
127.0.0.1:6379> smembers key
1) "1"
2) "2"
3) "3"
4) "4"
127.0.0.1:6379> srandmember key
"4"
127.0.0.1:6379> srandmember key 2
1) "3"
2) "1"
127.0.0.1:6379> srandmember key 8
1) "1"
2) "2"
3) "3"
4) "4"

SCARD

scard 用于获取集合中的元素个数;时间复杂度为 O(1)。

语法及使用示例:

代码语言:javascript复制
SCARD key 
代码语言:javascript复制
127.0.0.1:6379> smembers key
1) "1"
2) "2"
3) "3"
4) "4"
127.0.0.1:6379> scard key
(integer) 4
5.2.3 集合间操作

在 Redis 中,集合间的操作一共有三种,分别是求交集 (inter)、求并集 (union) 以及求差集 (diff),它们的示例如下:

SINTER / SINTERSTORE

sinter 用于获取给定集合中的交集元素;时间复杂度为 O(N * M),其中 N 是最小集合的元素个数,M 是最大集合的元素个数。

语法及使用示例:

代码语言:javascript复制
SINTER key [key ...] 
代码语言:javascript复制
127.0.0.1:6379> sadd key1 1 2 3 4
(integer) 4
127.0.0.1:6379> sadd key2 3 4 5 6
(integer) 4
127.0.0.1:6379> sinter key1 key2
1) "3"
2) "4"

sinterstore 与 sinter 不同的地方在于它可以将交集元素直接存储到另一个集合中去。

语法及使用示例:

代码语言:javascript复制
SINTERSTORE destination key [key ...] 
代码语言:javascript复制
127.0.0.1:6379> sadd key1 1 2 3 4
(integer) 4
127.0.0.1:6379> sadd key2 3 4 5 6
(integer) 4
127.0.0.1:6379> sinterstore key3 key1 key2
(integer) 2
127.0.0.1:6379> smembers key3
1) "3"
2) "4"

SUNION / SUNIONSTORE

sunion 用于获取给定集合中的并集元素;时间复杂度为 O(N),其中 N 为给定的所有集合的总的元素个数。

语法及使用示例:

代码语言:javascript复制
SUNION key [key ...] 
代码语言:javascript复制
127.0.0.1:6379> sadd key1 1 2 3 4
(integer) 4
127.0.0.1:6379> sadd key2 3 4 5 6
(integer) 4
127.0.0.1:6379> sunion key1 key2
1) "1"
2) "2"
3) "3"
4) "4"
5) "5"
6) "6"

sunionstore 与 sunion 不同的地方在于它可以将并集元素直接存储到另一个集合中去。

语法及使用示例:

代码语言:javascript复制
SUNIONSTORE destination key [key ...] 
代码语言:javascript复制
127.0.0.1:6379> sadd key1 1 2 3 4
(integer) 4
127.0.0.1:6379> sadd key2 3 4 5 6
(integer) 4
127.0.0.1:6379> sunionstore key3 key1 key2
(integer) 6
127.0.0.1:6379> smembers key3
1) "1"
2) "2"
3) "3"
4) "4"
5) "5"
6) "6"

SDIFF / SDIFFSTORE

sdiff 用于获取给定集合中的差集元素,时间复杂度为 O(N),其中 N 为给定的所有集合的总的元素个数。

与 sinter/sunion 不同,差集对于集合的次序是有要求的,即 sdiff key1 key2sdiff key2 key1 表示的含义是完全不同的。

语法及使用示例:

代码语言:javascript复制
SDIFF key [key ...] 
SDIFFSTORE destination key [key ...] 
代码语言:javascript复制
127.0.0.1:6379> sadd key1 1 2 3 4
(integer) 4
127.0.0.1:6379> sadd key2 3 4 5 6
(integer) 4
127.0.0.1:6379> sdiff key1 key2
1) "1"
2) "2"
127.0.0.1:6379> sdiff key2 key1
1) "5"
2) "6"
127.0.0.1:6379> sdiffstore key3 key1 key2
(integer) 2
127.0.0.1:6379> smembers key3
1) "1"
2) "2"
5.2.4 命令小结

-命令

介绍-

-时间复杂度

SADD key member member …

将一个或多个元素添加到 Set 中

O(K)

SPOP key count

从集合中随机删除一个或多个元素

O(K)

SREM key member member …

从集合中指定删除一个或多个元素

O(K)

SMOVE source destination member

从源 Set 中取出元素放入目标 Set 中

O(1)

SMEMBERS key

获取集合中的所有元素

O(N)

SISMEMBER key member

判断一个元素是否在 Set 中

O(1)

SRANDMEMBER key count

从集合中随机获取一个或多个元素

O(K)

SCARD key

获取集合中元素个数

O(1)

SINTER key key … SINTERSTORE destination key key …

求多个集合的交集

O(N * M)N/M 为最小/最大集合的元素个数

SUNION key key … SUNIONSTORE destination key key …

求多个集合的并集

O(N) N 为所有集合元素个数之和

SDIFF key key … SDIFFSTORE destination key key …

求多个集合的差集

O(N)N 为所有集合元素个数之和

5.3 内部编码

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

  • intset(整数集合):当集合中的元素都是整数并且元素的个数小于 set-max-intset-entries 配置(默认 512 个)时,Redis 会选用 intset 来作为集合的内部实现,从而减少内存的使用。
  • hashtable(哈希表):当集合类型无法满足 intset 的条件时,Redis 会使用 hashtable 作为集合的内部实现。
代码语言:javascript复制
127.0.0.1:6379> sadd key1 1 2 3 4
(integer) 4
127.0.0.1:6379> object encoding key1
"intset"
127.0.0.1:6379> sadd key2 hello world
(integer) 2
127.0.0.1:6379> object encoding key2
"hashtable"

5.4 应用场景

集合类型比较典型的使用场景是保存用户标签 (tag),例如 A 用户对娱乐、体育板块比较感兴趣,B 用户对历史、新闻比较感兴趣,这些兴趣点可以被抽象为标签。有了这些标签之后就可以分析出用户的一些特征,也就是所谓的 “用户画像”,进而根据这些数据进行商品推荐、视频推荐等等。

同时,由于集合可以很方便的计算交集,因此得到喜欢同一个标签的人,以及用户的共同喜好的标签,这些数据对于增强用户体验和用户黏度都非常有帮助。

另外,基于 set 的去重功能,我们还可以使用 set 来统计一个网站的 UV (user view),即有多少个用户访问过你这个网站。由于同一个用户多次访问网站不会增加 UV,因此刚好可以利用 set 的去重功能。

6 Zset

6.1 类型介绍

有序集合 zset 与 set 相似,都用于存储相关联的数据,并且数据不允许重复;但与 set 不同的地方在于,有序集合中的每个元素都有一个唯一的浮点类型的分数(score)与之关联,且不同元素的分数可以相同,这使得有序集合中的元素可以以分数为基准进行排序,即 zset 中的元素是有序的

需要注意的是:

  • 与 Hash 类型中的 field-value 键值对结构不同,zset 中 member-score 不是键值对结构,因为我们既可以根据 member 找到 score,也可以根据 score 找到 member,它们的关系类似于 C 中的 std::pair,member 与 score 在查询时没有主次之分。
  • 有序集合中的元素是不能重复的,但分数允许重复。类似于一次考试之后,每个人一定有一个唯一的分数,但分数允许相同。

有序集合提供了获取指定分数和元素范围查找、计算成员排名等功能,合理地利用有序集合,可以帮助我们在实际开发中解决很多问题.下面是三国武将的有序集合示例,其中分数代表武力值:

6.2 相关命令

6.2.1 ZADD/ZPOP 系列命令

ZADD

zadd 用于向有序集合中添加或更新指定元素及其关联的分数,其中分数为 double 类型,支持输入 inf/-inf 作为正负极限;返回值为成功添加的元素个数;时间复杂度为 O(log(N) * K),其中 N 为集合大小,K 为插入的元素个数。

语法:

代码语言:javascript复制
ZADD key [NX | XX] [GT | LT] [CH] [INCR] score member [score member ...]

其中各个选项的含义如下:

  • NX | XX:NX – 不存在才添加,用于新增;XX – 存在才添加,用于更新。
  • GT | LT:GT (greater than) – 更新元素对应的分数时,大于原来的分数才更新;LT (less than) – 小于原来的分数才更新。
  • CH:影响 zadd 的返回值,正常情况下 zadd 仅返回新增的元素个数,CH 选项会将 新增 修改 的元素个数全部返回。
  • INCR:让 zadd 命令表现出 zincrby 命令的行为,此时只能指定一个元素。

使用示例:

代码语言:javascript复制
127.0.0.1:6379> zadd key 10 zhangsan 40 lisi 20 wangwu
(integer) 3
127.0.0.1:6379> zrange key 0 -1 withscores
1) "zhangsan"
2) "10"
3) "wangwu"
4) "20"
5) "lisi"
6) "40"
127.0.0.1:6379> zadd key 30 zhaoliu
(integer) 1
127.0.0.1:6379> zrange key 0 -1 withscores  
1) "zhangsan"
2) "10"
3) "wangwu"
4) "20"
5) "zhaoliu"     # 元素插入时会根据分数寻找合适的位置进行插入
6) "30"
7) "lisi"
8) "40"

ZPOPMIN / ZPOPMAX

zpopmin 用于删除集合中分数最小的元素,zpopmax 用于删除集合中分数最大的元素;返回值为被删除的分数及元素列表;时间复杂度为 O(log(N) * K),其中 N 为集合大小,K 为删除的元素个数。

可以看到,zset 头删/尾删的效率是 O(log(N)),但 Redis zset 源码实现中其实是记录了头部与尾部这样的特定位置的,但是 zpopmin/zpopmax 并没有使用该特性,而是调用通用的删除函数来实现尾删,因此效率是 O(log(N)),而不是 O(1)。

语法及使用示例:

代码语言:javascript复制
ZPOPMIN key [count] 
ZPOPMAX key [count] 
代码语言:javascript复制
127.0.0.1:6379> zadd key 1 m1  1 m2 2 m3 3 m4 4 m5 5 m6 5 m7 6 m8 7 m9
(integer) 9
127.0.0.1:6379> zpopmin key 2
1) "m1"
2) "1"
3) "m2"
4) "1"
127.0.0.1:6379> zpopmax key 2
1) "m9"
2) "7"
3) "m8"
4) "6"
127.0.0.1:6379> zrange key 0 -1 withscores
 1) "m3"
 2) "2"
 3) "m4"
 4) "3"
 5) "m5"
 6) "4"
 7) "m6"
 8) "5"
 9) "m7"
10) "5"
127.0.0.1:6379> zpopmax key
1) "m7"
2) "5"
127.0.0.1:6379> zrange key 0 -1 withscores
1) "m3"
2) "2"
3) "m4"
4) "3"
5) "m5"
6) "4"
7) "m6"
8) "5"

ZREM

zrem 用于删除指定的元素;返回值为删除的元素个数;时间复杂度为 O(log(N) * K),其中 N 为集合大小,K 为要删除的元素个数。

语法及使用示例:

代码语言:javascript复制
ZREM key member [member ...] 
代码语言:javascript复制
127.0.0.1:6379> zadd key 10 zhangsan 20 lisi 30 wangwu
(integer) 3
127.0.0.1:6379> zrem key zhangsan lisi zhaoliu
(integer) 2
127.0.0.1:6379> zrange key 0 -1
1) "wangwu"

ZREMRANGEBYRANK

zremrangebyrank 用于根据下标进行范围删除 (小标从0开始,左闭右闭);返回值为删除的元素;时间复杂度为 O(log(N) K)。

zremrangebyrank 时间复杂度为 log(N) K,而不是 log(N) * K,这是因为只需要进行一次查找,后续顺序删除区间中的元素即可。

语法及使用示例:

代码语言:javascript复制
ZREMRANGEBYRANK key start stop 
代码语言:javascript复制
127.0.0.1:6379> zadd key 10 zhagnsan 20 lisi 30 wangwu 40 zhaoliu
(integer) 4
127.0.0.1:6379> zremrangebyrank key 2 10
(integer) 2
127.0.0.1:6379> zrange key 0 -1
1) "zhagnsan"
2) "lisi"

ZREMRANGEBYSCORE

zremrangebyscore 用于根据分数进行范围删除;返回值为删除的元素;时间复杂度为 O(log(N) K)。

语法及使用示例:

代码语言:javascript复制
ZREMRANGEBYSCORE key min max 
代码语言:javascript复制
127.0.0.1:6379> zadd 1 m1 2 m2 3 m3 4 m4 5 m5
(error) ERR syntax error
127.0.0.1:6379> zadd key 1 m1 2 m2 3 m3 4 m4 5 m5
(integer) 5
127.0.0.1:6379> zremrangebyscore key 1 3
(integer) 3
127.0.0.1:6379> zrange key 0 -1 
1) "m4"
2) "m5"
127.0.0.1:6379> zremrangebyscore key -inf inf
(integer) 2
127.0.0.1:6379> zrange key 0 -1 
(empty list or set)
6.2.2 BZPOPMIN/BZPOPMAX 阻塞命令

bzpopmin/bzpopmax 是 zpopmin/zpopmax 的阻塞版本,zpopmin/zpopmax 可以实现类似 “优先级队列 (堆)” 的效果,而 bzpopmin/bzpopmax 则可以实现阻塞版本的优先级队列。

语法:

代码语言:javascript复制
BZPOPMIN key [key ...] timeout 
BZPOPMAX key [key ...] timeout 
6.2.3 其他命令

ZCARD

zcard 用于获取集合的元素个数;时间复杂度为 O(1)。

语法及使用示例:

代码语言:javascript复制
ZCARD key 
代码语言:javascript复制
127.0.0.1:6379> zadd key 1 m1 2 m2 3 m3
(integer) 3
127.0.0.1:6379> zcard key
(integer) 3

ZCOUNT

zcount 用于获取范围分数内的元素个数,默认包含边界,可以通过 ( 排除边界值;时间复杂度为 O(log(N)),其中 N 为集合大小。

语法及使用示例:

代码语言:javascript复制
ZCOUNT key min max 
代码语言:javascript复制
127.0.0.1:6379> zrange key 0 -1 withscores
1) "m1"
2) "1"
3) "m2"
4) "2"
5) "m3"
6) "3"
7) "m4"
8) "4"
127.0.0.1:6379> zcount key 1 3
(integer) 3
127.0.0.1:6379> zcount key (1 3
(integer) 2
127.0.0.1:6379> zcount key 1 (3
(integer) 2

特别注意:排除右边界是使用 (max,而不是 max)

ZRANGE / ZREVRANGE

zrange 用于返回指定下标区间中的元素,按照分数升序返回,带上 withscores 选项可以显示分数;时间复杂度为 O(log(N) K),其中 N 为集合大小,K 为区间大小。

语法及使用示例:

代码语言:javascript复制
ZRANGE key start stop [WITHSCORES] 
代码语言:javascript复制
127.0.0.1:6379> zadd key 1 m1 2 m2 3 m3
(integer) 3
127.0.0.1:6379> zrange key 0 -1 
1) "m1"
2) "m2"
3) "m3"
127.0.0.1:6379> zrange key 1 10 withscores
1) "m2"
2) "2"
3) "m3"
4) "3"

zrevrange 与 zrange 的区别在于 zrevrange 返回的元素按照分数降序排序。

语法:

代码语言:javascript复制
ZREVRANGE key start stop [WITHSCORES] 

ZRANGEBYSCORE

zrangebyscore 用于返回分数在 min 和 max 之间的元素,默认情况下包含边界,可以通过 ( 排除;时间复杂度为 O(log(N) K),其中 N 为集合大小,K 为区间中的元素个数。

语法及使用示例:

代码语言:javascript复制
ZRANGEBYSCORE key min max [WITHSCORES] 
代码语言:javascript复制
127.0.0.1:6379> zadd key 1 m1 5 m2 10 m3 40 m4
(integer) 4
127.0.0.1:6379> zrangebyscore key 10 30
1) "m3"
127.0.0.1:6379> zrangebyscore key 10 40 withscores
1) "m3"
2) "10"
3) "m4"
4) "40"

ZRANK / ZREVRANK

zrank 用于返回指定元素在升序排序中的排名;时间复杂度为 O(log(N)),其中 N 为集合大小。

实际上 Zset 内部会记录每个元素当前的 “排行” 或者 “次序”,因此查询到元素,就直接知道了元素所在的 “次序”。

语法及使用示例:

代码语言:javascript复制
ZRANK key member 
代码语言:javascript复制
127.0.0.1:6379> zrangebyscore key -inf inf withscores
1) "m1"
2) "1"
3) "m2"
4) "5"
5) "m3"
6) "10"
7) "m4"
8) "40"
127.0.0.1:6379> zrank key m3
(integer) 2
127.0.0.1:6379> zrank key m1
(integer) 0
127.0.0.1:6379> zrank key m8
(nil)

zrevrank 与 zrank 的区别在于 zrank 返回元素在降序排序中的排名。

语法:

代码语言:javascript复制
ZREVRANK key member 

ZSCORE

zscore 用于返回元素对应的分数;时间复杂度为 O(1)

我们前面查找元素的各种东西时间复杂度都是 O(log(N)),但查找元素对应的分数确是 O(1),这说明 Redis 对此操作进行了特殊优化,比如通过哈希表将 member 与 score 进行了一一对应;这样做的原因应该是 zscore 操作非常频繁,造成了 zset 的性能瓶颈。

语法及使用示例:

代码语言:javascript复制
ZSCORE key member
代码语言:javascript复制
127.0.0.1:6379> zrangebyscore key -inf inf withscores
1) "m1"
2) "1"
3) "m2"
4) "5"
5) "m3"
6) "10"
7) "m4"
8) "40"
127.0.0.1:6379> zscore key m3
"10"
127.0.0.1:6379> zscore key m8
(nil)

ZINCRBY

zincrby 用于为指定元素关联的分数添加指定的分数值,分数值可以是正数或负数,也可以是整数或浮点数;时间复杂度为 O(log(N)),其中 N 为集合大小。

语法及使用示例:

代码语言:javascript复制
ZINCRBY key increment member
代码语言:javascript复制
127.0.0.1:6379> zadd key 10 zhangsan
(integer) 1
127.0.0.1:6379> zincrby key 20 zhangsan
"30"
127.0.0.1:6379> zscore key zhangsan
"30"
127.0.0.1:6379> zincrby key -12.5 zhangsan
"17.5"
127.0.0.1:6379> zscore key zhangsan
"17.5"
6.2.4 集合间操作

与 set 一样,zset 集合间操作也包括求交集 (inter)、求并集 (union) 以及求差集 (diff)。但与 set 不同的地方在于,zset 每个元素多了一个关联的分数,因此在进行集合间操作时,对于相同的 member 我们要考虑如何处理它们的 score。

为了解决这个问题,zset 集合操作多了两个参数 – weights 与 aggregate:

  • weights 描述了每个集合在运算时所占的权重是多少,在集合运算时 member 的 score 等于 原来的 score 乘以权重。
  • aggregate 描述了相同元素之间 score 的处理方式,其中 SUM 表示求和,MIN 表示取最小的 score,MAX 表示取最大的 score。

ZINTERSTORE

我们以求交集为例,求交集的语法如下:

代码语言:javascript复制
ZINTERSTORE destination numkeys key [key ...] [WEIGHTS weight [weight ...]] [AGGREGATE <SUM | MIN | MAX>]

其中,由于命令中含有 weights 与 aggregate 参数,因此需要增加 numkeys 表示参与运算的集合个数,避免集合名与后面的命令混淆。

代码语言:javascript复制
127.0.0.1:6379> zadd key1 1 kris 91 mike 200 frank 220 tim 250 martin 251 tom
(integer) 6
127.0.0.1:6379> zadd key2 8 james 77 mike 625 martin 888 tom
(integer) 4
127.0.0.1:6379> zinterstore key3 2 key1 key2 weights 0.5 2 aggregate sum
(integer) 3
127.0.0.1:6379> zrange key3 0 -1 withscores
1) "mike"
2) "199.5"
3) "martin"
4) "1375"
5) "tom"
6) "1901.5"

如上:

  • key1 与 key2 之间的交集元素为 mike martin tom,并且 key1 的权重为 0.5,key2 的权重为 2;因此求交集时 key1 交集元素的 score 分别是 45.5 125 125.5,key2 交集元素 score 分别为 154 1250 1776。
  • 同时 key1 与 key2 score 的处理方式是 SUM,即求和,因此最终得到的交集元素的 socre 分别为 199.5 1375 1901.5。

需要注意的是,zinterstore 命令的时间复杂度是 O(N * K) O(M * log(M)),其中 N 是输入的有序集合中最小的有序集合的元素个数,K 是输入了几个有序集合,M 是最终结果的有序集合的元素个数。

ZUNIONSTORE

zunionstore 用于求出给定有序集合中元素的并集并保存进目标有序集合中,在合并过程中以元素为单位进行合并,元素对应的分数按照不同的聚合方式和权重得到新的分数;返回值为目标集合元素个数;时间复杂度为 O(N) O(M * log(M)),其中 N 是输入的有序集合总的元素个数,M 是最终结果的有序集合的元素个数。

注意:weights 与 aggregate 参数虽然是用于对集合中相同 member 的 score 进行加权求和 ,但即使 member 只在一个集合中出现,那么目标集合中该 member 的 score 也会乘以该集合的权重。

语法及使用示例:

代码语言:javascript复制
ZUNIONSTORE destination numkeys key [key ...] [WEIGHTS weight [weight ...]] [AGGREGATE <SUM | MIN | MAX>]
代码语言:javascript复制
127.0.0.1:6379> zunionstore key4 2 key1 key2 weights 0.5 2 aggregate sum
(integer) 7
127.0.0.1:6379> zrange key4 0 -1 withscores
 1) "kris"
 2) "0.5"
 3) "james"
 4) "16"
 5) "frank"
 6) "100"
 7) "tim"
 8) "110"
 9) "mike"
10) "199.5"
11) "martin"
12) "1375"
13) "tom"
14) "1901.5"

ZINTER / ZUNION / ZDIFF / ZDIFFSTORE

我们上面提到的 ZINSTERSTORE 与 ZUNIONSTORE 命令都是在 Redis 2.0.0 版本提供的;而其他的集合间操作相关命令,如 ZINTER ZUNION ZDIFF ZDIFFSTORE 则是在 Redis 6.2.0 版本才提供的。由于我们主要基于 Redis5 版本进行学习,所以其他命令不再展开叙述,但其实操作都一样。

6.2.5 命令小结

-命令

介绍-

-时间复杂度

ZADD key NX | XX CH score member score member …

向集合中添加元素及其关联的分数

O(log(N) K)

ZPOPMIN key count ZPOPMAX key count

删除集合中分数最大的元素删除集合中分数最小的元素

O(log(N) K)

ZREM key member member …

删除集合中指定的元素

O(log(N) K)

ZREMRANGEBYRANK key start stop

根据下标进行范围删除

O(log(N) K)

ZREMRANGEBYSCORE key min max

根据分数进行范围删除

O(log(N) K)

ZCARD key

获取集合的元素个数

O(1)

ZCOUNT key min max

获取范围分数内的元素个数

O(log(N))

ZRANGE key start stop WITHSCORESZREVRANGE key start stop WITHSCORES

按分数升序返回下标区间中的元素按分数降序返回下标区间中的元素

O(log(N) K)

ZRANGEBYSCORE key min max WITHSCORES

返回分数在 min 和 max 之间的元素

O(log(N) k)

ZRANK key member ZREVRANK key member

返回指定元素在升序排序中的排名返回指定元素在降序排序中的排名

O(log(N))

ZSCORE key member

返回元素对应的分数

O(1)

ZINCRBY key increment member

增加指定元素关联的分数

O(log(N))

BZPOPMIN key key … timeout BZPOPMAX key key … timeout

zpopmin/zpopmax 的阻塞版本

O(log(N) K)

ZINTERSTORE destination numkeys key key …WEIGHTS weight weight …] AGGREGATE <SUM | MIN | MAX>ZUNIONSTORE destination numkeys key key …WEIGHTS weight weight …] AGGREGATE <SUM | MIN | MAX>…

集合间操作

6.3 内部编码

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

  • ziplist(压缩列表):当有序集合的元素个数小于 zset-max-ziplist-entries 配置(默认 128 个),同时每个元素的值都小于 zset-max-ziplist-value 配置(默认 64 字节)时,Redis 会用 ziplist 来作为有序集合的内部实现,ziplist 可以有效减少内存的使用。
  • skiplist(跳表):当 ziplist 条件不满足时,有序集合会使用 skiplist 作为内部实现,因为此时 ziplist 的操作效率会下降。
代码语言:javascript复制
127.0.0.1:6379> zadd key1 1 m1 2 m2 3 m3
(integer) 3
127.0.0.1:6379> object encoding key1
"ziplist"
127.0.0.1:6379> zadd key2 1 mmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmm1
(integer) 1
127.0.0.1:6379> object encoding key2
"skiplist"

关于跳表

很多同学可能以前并没有接触过跳表,跳表其实就是一个多层链表,跳表的每一个节点都有很多层,其中最下面一层用于真正存储数据,其余层用于指向后面的节点,这使得我们查找时一次可以跳过多个节点,最终实现出类似于二分查找的效果,因此跳表增加、删除以及查找的时间复杂度都是 O(log(N)

代码语言:javascript复制
struct SkiplistNode {
    int _val;
    vector<SkiplistNode *> _nextVec;
};

跳表相关的博客以及题目:

Skip List–跳表(全网最详细的跳表文章没有之一)

Redis内部数据结构详解(6)——skiplist

1206. 设计跳表

6.4 应用场景

有序集合比较典型的应用场景就是排行榜系统,比如:

  • 微博热搜
  • 游戏天梯排行
  • 成绩排行

我们以微博热搜为例,一条微博的热度评判可能是多维度的,与浏览量、点赞量、转发量、评论量等等都可能有关,并且这些指标所占的权重是不同的。在 Redis 中,我们就可以把上述每个维度的数值都放到一个有序集合中,其中 member 就是微博的 id,score 就是各自维度的数值,然后通过 zinterstore 或者 zunionstore 把上述有序集合按照约定好的权重,进行集合间运算即可,得到的结果集合的分数就是热度(排行榜也就顺带出来了)。

又比如王者农药的癫疯榜排行,我们可以把玩家ID与玩家对应的癫疯分数放到有序集合中,由于 zset 是有序的,所以此时就自动形成了一个排行榜,然后我们就可以基于此进行范围查询、玩家癫疯分数修改等等。

另外,我们也不用担心玩家太多内存存不下,我们假设一个游戏有一亿玩家,其中 userID 占四个字节,score 占八个字节,那么一个玩家用12字节来表示,一亿玩家就是 12 亿字节 ≈ 1.2GB,这对于服务器内存来说是小 case。

计算机中的单位换算 (估算):

  • thousand (1000) ≈ KB
  • million (百万) ≈ MB
  • billion (十亿) ≈ GB

7 其他数据类型

Redis 除了 string、list、hash、set、zset 这五种基本数据类型外,还提供一些作用于特定场景的数据类型,比如:

  • Streams:流类型,作用类似于仅附加日志,主要用来实现消息队列,相当于 List blpop/brpop 的升级版。
  • Geospatial:地理空间类型,用来存储坐标(经纬度),此数据结构对于查找给定半径或边界框内的邻近点非常有用。
  • Probabilistic:概率数据结构,包括用于估计集合基数的 HyperLogLog,用于检查集合中是否存在元素的 Bloom filter 等等。
  • Bitmaps:位图类型,使用 bit 位来表示整数以节省空间,可以参考 C 中 的 bitset 。
  • BitFields:位域类型,可以参考我们 C语言 中学习的位段类型,位域可以指定每一个元素所占比特位,从而更方便的进行位操作。

关于这些数据类型更多的信息以及相关的命令,可以参考 Redis 官方文档:

Reids – Understand Redis data types

Redis – Commands

8 补充内容

8.1 渐进式遍历

我们在文章最开始学习全局命令 keys 的时候提到此命令最好不要使用,因为存在长时间占用 redis-server 导致其他客户端得不到响应的风险;为了解决这个问题,Redis 设计出了 scan 命令:

  • scan 命令用于渐进式遍历键,所谓渐进式是指 scan 一次只遍历一部分键,如果要获取所有键需要多次遍历。
  • scan 命令首次遍历从 0 开始,并且会返回下一次遍历的游标 cursor,当 scan 返回的下次位置为 0 时表示所有键都遍历结束。
  • scan 其实是一组命令,包括 scan、 hscan、sscan、zscan,它们的语法相同,但使用场景不同,其中:
    • scan 用于遍历数据库中的 key 集合。
    • hscan 用于遍历哈希中的元素,即 field-value。
    • sscan 用于遍历集合中的元素,即 member。
    • zscan 用于遍历有序集合中的元素,即 member-score。

观察上面的示例我们可以发现,scan 命令返回的游标并不是元素的下标,其实对于 redis-cli 来说,scan 返回的游标仅作为下一次调用 scan 命令的参数,其余没有任何意义;而 redis-server 则能够通过此游标找到对应元素的位置。

语法及使用示例:

代码语言:javascript复制
SCAN cursor [MATCH pattern] [COUNT count] [TYPE type]
HSCAN key cursor [MATCH pattern] [COUNT count] [NOVALUES]
SSCAN key cursor [MATCH pattern] [COUNT count]
ZSCAN key cursor [MATCH pattern] [COUNT count]
代码语言:javascript复制
127.0.0.1:6379> mset k1 v1 k2 v2 k3 v3 k4 v4 k5 v5 k6 v6 k7 v7 k8 v8
OK
127.0.0.1:6379> scan 0 count 2
1) "4"
2) 1) "k3"
   2) "k5"
127.0.0.1:6379> scan 4 count 3
1) "2"
2) 1) "k1"
   2) "k2"
   3) "k4"
127.0.0.1:6379> scan 2 count 5
1) "0"
2) 1) "k8"
   2) "k7"
   3) "k6"

需要注意的是,scan 的 count 参数是一个建议性参数,即 scan 命令遍历的 key 的数量可能与你传递的 count 不同。

渐进性遍历 scan 虽然解决了阻塞的问题,但如果在遍历期间键有所变化(增加、修改、删除),可能导致遍历时键的重复遍历或者遗漏,这点务必在实际开发中考虑。

8.2 数据库管理

Redis 提供了几个面向 Redis 数据库的操作,分别是 dbsizeselectflushdbflushall ,下面我们简单了解一下这些命令。

切换数据库 – select

在关系型数据库中,一个数据库服务器实例下是允许多个数据库存在的,例如 MySQL 的 create database 语句,Redis 中同样也存在多个数据库。

Redis 使用数字作为多个数据库的实现,默认有 16 个数据库,编号为 0 - 15,各个数据库中的数据互不影响,我们可以使用 select 命令来切换数据库:

代码语言:javascript复制
select dbIndex 
代码语言:javascript复制
127.0.0.1:6379> select 10
OK
127.0.0.1:6379[10]> select 15
OK
127.0.0.1:6379[15]> select 16
(error) ERR DB index is out of range
127.0.0.1:6379[15]> select 0
OK
127.0.0.1:6379> 

Redis 中虽然支持多数据库,但随着版本的升级,其实不是特别建议使用多数据库特性。如果真的需要完全隔离的两套键值对,更好的做法是维护多个 Redis 实例,而不是在一个 Redis 实例中维护多数据库。这是因为本身 Redis 并没有为多数据库提供太多的特性,其次无论是否有多个数据库,Redis 都是使用单线程模型,所以彼此之间还是需要排队等待命令的执行。同时多数据库还会让开发、调试和运维工作变得复杂。所以实践中,始终使用数据库 0 其实是一个很好的选择。

清空数据库 – flushdb / flushall

Redis 提供了 flushdbflushall 命令来清除数据库中的数据,区别是 flushdb 只清除当前数据库中的数据,而 flushall 会清除所有数据库中的数据:

代码语言:javascript复制
flushdb
代码语言:javascript复制
127.0.0.1:6379> keys *
1) "k3"
2) "k5"
3) "k8"
4) "k7"
5) "k1"
6) "k2"
7) "k4"
8) "k6"
127.0.0.1:6379> FLUSHALL
OK
127.0.0.1:6379> keys *
(empty list or set)
代码语言:javascript复制
flushall
代码语言:javascript复制
127.0.0.1:6379> keys *
1) "k1"
2) "ke"
127.0.0.1:6379> select 2
OK
127.0.0.1:6379[2]> keys *
1) "k2"
2) "k1"
127.0.0.1:6379[2]> FLUSHALL
OK
127.0.0.1:6379[2]> keys *
(empty list or set)
127.0.0.1:6379[2]> select 0
OK
127.0.0.1:6379> keys *
(empty list or set)

永远不要在线上环境中执行 flushdb/flushall 命令,这和在 Linux 中执行 sudo rm -rf / 一样严重。


0 人点赞