《Redis开发与运维》读书笔记(二)API介绍

第二章 API介绍

2.1 准备

首先了解一些通用的Redis全局命令、数据结构和内部编码、单线程命令处理机制。

2.1.1 全局命令

Redis有5种数据结构,它们是键值对中的值,对于键来说有一些通用的命令。

1. 查看所有键

1
keys *
1
2
3
4
5
6
7
8
9
10
127.0.0.1:6379> set hello world
OK
127.0.0.1:6379> set java jedis
OK
127.0.0.1:6379> set python redis-py
OK
127.0.0.1:6379> keys *
1) "python"
2) "java"
3) "hello"

2. 键总数

1
dbsize

dbsize命令会返回当前数据库中键的总数。

1
2
3
4
127.0.0.1:6379> rpush mylist a b c d e f g
(integer) 7
127.0.0.1:6379> dbsize
(integer) 4

dbsize命令在计算键总数时不会遍历所有键,而是直接获取Redis内置的键总数变量,所有dbsize命令的时间复杂度为O(1)。而keys命令则会遍历所有键,时间复杂度为O(n),当Redis保存了大量键时,线上环境禁止使用

3. 检查键是否存在

1
exists key

如果键存在就返回1,否则返回0。

1
2
3
4
127.0.0.1:6379> exists java
(integer) 1
127.0.0.1:6379> exists not_exist_key
(integer) 0

4. 删除键

1
del key [key ...]

del是一个通用命令,无论值是什么数据结构类型,del命令都可以将其删除。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
127.0.0.1:6379> del java
(integer) 1
127.0.0.1:6379> exists java
(integer) 0
127.0.0.1:6379> del mylist
(integer) 1
127.0.0.1:6379> exists mylist
(integer) 0
127.0.0.1:6379> del not_exist_key
(integer) 0
127.0.0.1:6379> set a
OK
127.0.0.1:6379> set b
OK
127.0.0.1:6379> set c
OK
127.0.0.1:6379> del a b c
(integer) 3

5. 键过期

1
expire key seconds

Redis支持对键添加过期时间,当超过过期时间后,会自动删除键。

1
2
3
4
5
6
7
8
9
10
11
12
127.0.0.1:6379> set hello world
OK
127.0.0.1:6379> expire hello 10
(integer) 1
127.0.0.1:6379> tll hello
(integer) 7
127.0.0.1:6379> tll hello
(integer) 1
127.0.0.1:6379> tll hello
(integer) -2
127.0.0.1:6379> get hello
(nil)

tll命令会返回键的剩余过期时间,返回值有三类:

  • 大于0的整数:键剩余的过期时间。
  • -1:键没设置过期时间。
  • -2:键不存在。

6. 键的数据结构类型

1
type key

返回键对应的数据类型。

1
2
3
4
5
6
7
8
9
10
127.0.0.1:6379> set a b
OK
127.0.0.1:6379> type a
string
127.0.0.1:6379> rpush mylist a b c d e f g
(integer) 7
127.0.0.1:6379> type mylist
list
127.0.0.1:6379> type not_exist_key
none

2.1.2 数据结构和内部编码

type命令实际返回的就是当前键的数据结构类型,分别是:string、hash、list、set、zset,这些只是Redis对外的数据结构。实际上每种数据结构都有自己的底层内部编码实现,而且是多种实现,这样Redis会在合适的场景选择合适的内部编码。

可以看到每种数据结构都有两种以上的内部编码实现,如list数据结构包含了linked和ziplist两种内部编码。同时有些内部编码,例如ziplist,可以作为多种外部数据结构的内部实现,可以通过 object encoding 命令查询内部编码。

1
2
3
4
127.0.0.1:6379> object encoding hello
"embstr"
127.0.0.1:6379> object encoding mylist
"ziplist"

Redis这样设计有两个好处:

  • 第一,可以改进内部编码,而对外的数据结构和命令没有影响,这样一旦开发出更优秀的内部编码,无需改动外部数据结构和命令。
  • 第二,多种内部编码实现可以在不同的场景下发挥各自的优势,如ziplist比较节省内存,但是在列表元素比较多的情况下性能会有所下降,此时Redis会根据配置选项将列表类型的内部实现转换为linkedlist。

2.1.3 单线程架构

Redis使用单线程架构和I/O多路复用模型来实现高性能的内存数据库服务。

1. 单线程模型

首先开启三个redis-cli客户端同时执行命令。

1
2
3
4
5
6
# 客户端1设置一个字符串键值对
127.0.0.1:6379> set hello world
# 客户端2对counter做自增操作
127.0.0.1:6379> incr counter
# 客户端3对counter做自增操作
127.0.0.1:6379> incr counter

Redis客户端和服务端的模型可以简化为图2-3,每次客户端调用都经历了:发送命令、执行命令、返回结果三个过程。Redis是单线程来处理命令的,所以一条命令从客户端到达服务端不会立刻被执行,而是进入一个队列中,然后逐个等待执行。因此上述三个命令的执行顺序并不确定。但一定不会发生并发问题,Redis通过I/O多路复用技术来解决I/O问题。

2. 为什么单线程可以这么快?

通常单线程处理能力要比多线程差,而Redis却反常的拥有高性能的单线程模型,主要原因有以下三点:

  • 纯内存访问,基础速度快,响应时间短
  • 非阻塞I/O,Redis使用epoll作为I/O多路复用技术的实现,再加上自身的事件处理模型将epoll中的连接、读写、关闭都转换为事件,不在网络I/O上浪费过多的时间,如图2-6。
  • 单线程避免了线程切换和竞态产生的消耗

单线程模型的优点:

  • 简化数据结构和算法的实现。
  • 避免了线程切换和竞态产生的消耗。

单线程模型有一个弊端是不能出现执行时间过长的命令,否则会导致阻塞,这对于Redis这种高性能的服务是致命的,所以Redis是面向快速执行场景的数据库。


2.2 字符串

所有的键都是字符串类型,其它数据结构类型也是基于字符串类型来构建的。字符串既可以是字符串(当然包括如JSON和XML等)、数字、二进制(图片、音频、视频)等,但最大不能超过512MB

2.2.1 命令

(1) 设置值

1
2
3
set key value [ex seconds] [px milliseconds] [nx|xx]
setex ...
setnx ...

可选参数:

  • ex seconds :为键设置秒级过期时间。
  • px milliseconds:为键设置毫秒级过期时间。
  • nx:键必须不存在,才可以设置成功,用于添加。
  • xx:与nx相反,键必须存在,才可以设置成功,用于更新。

setnx和setxx在特殊的场景有其用处,比如多个客户端同时进行添加,使用setnx可以保证只有一个客户端能设置成功,算是一种分布式锁的实现方案

(2) 获取值

获取值,值不存在返回nil(空)。

1
get key

(3) 批量设置值

1
mset key value [key value ...]

(4) 批量获取值

1
mget key [key ...]

批量操作可以提高开发效率。

Redis可以支撑起每秒数万次的读写操作,但这是服务端的处理性能,客户端除了命令时间还要加上网络时间,假设网络时间为1毫秒,一次命令时间0.1毫秒,那么执行1000次get所需时间为1100毫秒,而1次mget则只需101毫秒。所以网络往往成为性能的瓶颈,不过要注意过量的批量操作也会导致Redis阻塞或网络拥塞。

(5) 计数

计数,对值进行自增操作。

1
incr key

返回值:

  • 值非整数 :返回错误。
  • 值为整数:返回自增后结果。
  • 键不存在:按照值为0自增,返回结果1。
  • xx:与nx相反,键必须存在,才可以设置成功,用于更新。

如下图所示,test本不存在,hello则是非整数键。

除了自增(incr)以外,还有自减(decr)、自增指定数字(incrby)、自减指定数字(decrby)、自增浮点数(incrbyfloat)。

1
2
3
4
decr key
incrby key increment
decrby key decrement
incrbyfloat key increment

(6) 向字符串尾部追加值

1
append key value

(7) 获取字符串长度

1
strlen key

(8) 设置并返回原值

1
getset key value

(9) 设置指定位置的字符

1
setrange key offeset value

(10) 获取部分字符串

1
getrange key start end

(11) 命令的时间复杂度

下图是字符串类型命令的时间复杂度,以供参考。

2.2.2 内部编码

字符串内部编码有三种:

  • int:8个字节的长整型。
  • embstr:小于等于39个字节的字符串。
  • raw:大于39个字节的字符串。

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

1
2
# 输出内部编码类型
object encoding key

2.2.3 使用场景

(1) 缓存功能

经典的缓存应用场景如图所示,Redis作为缓存层,MySql作为存储层,绝大部分数据都是从缓存中获取。由于Redis具有支撑高并发的特性,所以缓存能起到加速读写和降低后端压力的作用。

  1. 通过此函数获取用户信息。
1
2
3
UserInfo getUserInfo(long id){
...
}
  1. 首先从Redis获取用户信息。
1
2
3
4
5
6
7
8
9
// 定义键
userRedisKey = "user:info:" + id;
// 从Redis获取值
value = redis.get(userRedisKey);
if (value != null){
// 将值进行反序列化为UserInfo并返回结果
userInfo = deserialize(value);
return userInfo;
}
  1. 如果没有从Redis获取到用户信息,则需要从MySql中获取,并将结果写入Redis,并添加1小时过期时间。
1
2
3
4
5
6
// 从MySql获取用户信息
userInfo = mysql.get(id);
// 将userInfo序列化,并存入Redis
redis.setex(userRedisKey, 3600, serialize(userInfo));
// 返回结果
return userInfo;

合并后伪代码如下。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
UserInfo getUserInfo(long id){
// 定义键
userRedisKey = "user:info:" + id;
// 从Redis获取值
value = redis.get(userRedisKey);
if (value != null){
// 将值进行反序列化为UserInfo并返回结果
userInfo = deserialize(value);
} else {
// 从MySql获取用户信息
userInfo = mysql.get(id);
// 将userInfo序列化,并存入Redis
redis.setex(userRedisKey, 3600, serialize(userInfo));
}
// 返回结果
return userInfo;
}

(2) 计数

Redis可以实现快速技术、查询缓存的功能,同时数据可以异步落地到其他数据源。

1
2
3
4
5
// 用户观看后增加一次视频播放数,当然计数系统还有防作弊、不同维度计数、数据持久化到底层数据源等
long incrVideoCounter(long id){
key = "video:playCount:" + id;
return redis.incr(key);
}

(3) 共享Session

如图所示,一个分布式Web服务将用户的Session信息保存在各自的服务器中,这样会造成一个问题,出于负载均衡的考虑,分布式服务器会将用户的访问均衡到不同的服务器上,用户每刷新一次访问可能会发现要重新登录,这是用户无法容忍的。

为了解决这个问题,可以使用Redis将Session进行集中管理,如下图所示,这种模式下只要保证Redis是高可用和扩展性的,每次用户更新或查询登录信息都直接从Redis获取。

(4) 限速

很多应用出于安全的考虑,会在每次登录时让用户输入手机验证码,以确认是否本人操作。但为了使短信接口不被频繁访问,会限制用户每分钟获取验证码的频率,如图所示。

此功能可以通过Redis实现,伪代码如下。当然一些限制IP地址不能短时间多次访问也可以如此实现。

1
2
3
4
5
6
7
8
9
phoneNum = "138xxxxxxxx";
key = "shortMsg:limit:" + phoneNum;
// SET key value EX 60 NX 过期时间60秒+NX只做新增
isExists = redis.set(key.1, "EX 60", "NX");
if(isExists != null || redis.incr(key) <= 5){
// 通过
} else {
// 限速
}

2.3 哈希

hash,可以叫哈希、字典、关联数组。在Redis中,哈希类型是指键值本身又是一个键值对结构,形如 value=\{ \{field1, value1\}, ... ,\{filedN, valueN\} \} ,Redis键值对和哈希类型二者关系如下图。

2.3.1 命令

(1) 设置值

设置值,成功返回1,失败返回0。和set一样hset也有NX版本 -hsetnx ,作用域由键变为field。

1
2
3
4
hset key field value

127.0.0.1:6379> hset user:1 name tom
(integer) 1

(2) 获取值

获取值,键或field不存在时返回nil。

1
2
3
4
5
6
hget key field

127.0.0.1:6379> hget user:1 name
"tom"
127.0.0.1:6379> hget user:2 name
(nil)

(3) 删除一个或多个field

删除一个或多个field,返回结果为成功删除的field个数。

1
2
3
4
5
6
hdel key field [field ...]

127.0.0.1:6379> hdel user:1 name
(integer) 1
127.0.0.1:6379> hdel user:1 age
(integer) 0

(4) 计算field的个数

1
2
3
4
5
6
7
8
9
10
hlen key

127.0.0.1:6379> hset user:1 name tom
(integer) 1
127.0.0.1:6379> hset user:1 age 23
(integer) 1
127.0.0.1:6379> hset user:1 city tianjin
(integer) 1
127.0.0.1:6379> hlen user:1
(integer) 3

(5) 批量设置或获取field-value

批量设置或获取field-value,hmget需要key和多个field,hmset需要key和多对field-value

1
2
3
4
5
6
7
8
hmget key field [field ...]
hmset key field value [field value ...]

127.0.0.1:6379> hmset user:1 name mike age 12 city tianjin
OK
127.0.0.1:6379> hmget user:1 name city
1) "mike"
2) "tianjin"

(6) 判断field是否存在

判断field是否存在,存在为1,否则为0。

1
2
3
4
hexists key field

127.0.0.1:6379> hexists user:1 name
(integer) 1

(7) 获取所有field

1
2
3
4
5
6
hkeys key

127.0.0.1:6379> hkeys user:1
1) "name"
2) "age"
3) "city"

(8) 获取所有value

1
2
3
4
5
6
hvalues key

127.0.0.1:6379> hvalues user:1
1) "mike"
2) "12"
3) "tianjin"

(9) 获取所有的field-value

1
2
3
4
5
6
7
8
9
hgetall key

127.0.0.1:6379> hgetall user:1
1) "name"
2) "mike"
3) "age"
4) "12"
5) "city"
6) "tianjin"

如果哈希元素个数较多,可能会阻塞Redis,如果只须获取部分field,可以使用hmget;一定要获取全部field-value可以使用hscan命令,渐进式遍历哈希类型。

(10) field指定数字自增

同incrby和incrbyfloat一样,指定数字自增,但作用域是field。

1
2
hincrby key field
hincrbyfloat key field

(11) 计算value的字符串长度

1
2
3
4
hstrlen key field

127.0.0.1:6379> hstrlen user:1 name
(integer) 3

(12) 命令的时间复杂度

下图是哈希类型命令的时间复杂度,以供参考。

2.3.2 内部编码

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

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

2.3.3 使用场景

如图所示,为关系型数据库和Redis哈希类型存储用户信息的对比。

相比于使用字符串序列化缓存用户信息,哈希类型更为直观,并且在更新操作上更加便捷。可以将每个用户的id定义为键的后缀,多对field-value对应每个用户的属性。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
UserInfo getUserInfo(long id){
// 定义键,用户id作为key后缀
userRedisKey = "user:info:" + id;
// 从Redis获取值,使用hgetall获取所有用户信息映射关系
userInfoMap = redis.hgetAll(userRedisKey);
UserInfo userInfo;
if (userInfoMap != null){
// 将映射关系转化为UserInfo
userInfo = transferMapToUserInfo(userInfoMap);
} else {
// 从MySql获取用户信息
userInfo = mysql.get(id);
// 将UserInfo转换为映射关系并通过hmset保存到Redis
redis.hmset(userRedisKey, transferUserInfoToMap(userInfo));
// 添加过期时间
redis.expire(userRedisKey, 3600);
}
// 返回结果
return userInfo;
}

注意,哈希类型和关系型数据库的不同:

  • 哈希类型是稀疏的,而关系型数据库是完全结构化的。比如哈希类型每个键可以有不同的field,而关系型数据库一旦添加新的列,每行都要为其设置值(即使为null)
  • 关系型数据库可以做复杂的关系查询,而Redis去模拟关系型复杂查询开发比较困难,维护成本很高。

目前为止,有三种方法来缓存用户信息,下面分别列出其实现方法和优缺点。

(1) 原生字符串类型:每个属性一个键

1
2
3
set user:1:name tom
set user:1:age 23
set user:1:city beijing
  • 优点:简单直观,每个属性都支持更新操作。
  • 缺点:占用过多的键,内存占用量较大,同时用户信息内聚性比较差,所以此方案一般不会在生产环境使用。

(2) 序列化字符串类型:将用户信息序列化后用一个键保存

1
set user:1: serialize(userInfo)
  • 优点:简化编程,如果合理使用序列化可以提高内存的使用效率。
  • 缺点:序列化和反序列化有一定的开销,同时每次更新属性都需要把全部数据取出进行反序列化,更新后再序列化到Redis中。

(3) 哈希类型:每个用户属性使用一对field-value,但是只用一个键保存

1
hmset user:1:name tom age 23 city beijing
  • 优点:简单直观,如果合理使用可以减少内存空间的使用。
  • 缺点:要控制哈希在ziplist和hashtable两种内部编码的转换,hashtable会消耗更多内存。

2.4 列表

列表类型(list)用来存储多个有序的字符串。如图所示,5个元素从左到右组成了一个有序的列表,每个字符串即元素(element),一个列表最多可存储2^32-1个元素。

在Redis中,可以对列表两端插入(push)和弹出(pop),还可以获取指定范围的元素列表获取指定索引下标的元素等。总的来说,列表是一种比较灵活的数据结构,可以充当队列的角色。

列表类型有两个特点:

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

2.4.1 命令

(1) 添加操作

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# 从右边插入元素
rpush key value [value ...]
# 从左边插入元素
lpush key value [value ...]
# 向某个元素前或后插入元素
linsert key before|after pivot value


127.0.0.1:6379> rpush listkey c b a
(integer) 3
# lrange key 0 -1从左到右获取列表所有元素
127.0.0.1:6379> lrange listkey 0 -1
1) "c"
2) "b"
3) "a"
127.0.0.1:6379> linsert listkey before b java
(integer) 4
127.0.0.1:6379> lrange listkey 0 -1
1) "c"
2) "java"
3) "b"
4) "a"

(2) 查找操作

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 获取指定范围内的元素列表
lrange key start end
# 获取列表指定索引下标的元素
lindex key index
# 获取列表长度
llen key

127.0.0.1:6379> lrange listkey 1 3
1) "java"
2) "b"
3) "a"
127.0.0.1:6379> lindex listkey -1
"a"
127.0.0.1:6379> llen listkey
(integer) 4

索引下标有两个特点:

  • 索引下标从左到右分别是0到N-1,从右到左分别是-1到-N。

  • lrange中的end选项包含了自身,不同于大部分编程语言不包含end。

(3) 删除操作

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
# 从列表左侧弹出元素
lpop key
# 从列表右侧弹出元素
rpop key
# 删除指定元素
lrem key count value
# 按照索引范围修剪列表
ltrim key start end

127.0.0.1:6379> lpop listkey
"c"
127.0.0.1:6379> lrange listkey 0 -1
1) "java"
2) "b"
3) "a"
# 先从左插入4个a,再删除从左开始4个a元素
127.0.0.1:6379> lrem listkey 4 a
(integer) 4
127.0.0.1:6379> lrange listkey 0 -1
1) "a"
2) "java"
3) "b"
4) "a"
# 只保留第2到第4个元素
127.0.0.1:6379> ltrim listkey 1 3
OK
127.0.0.1:6379> lrange listkey 0 -1
1) "java"
2) "b"
3) "a"

count情况包括三种:

  • count>0:从左到右,删除最多count个元素。
  • count<0:从右到左,删除最多count绝对值个元素。
  • count=0:删除所有。

(4) 修改操作

1
2
3
4
5
6
7
8
9
lset key index newValue

# 设置第3个元素为python
127.0.0.1:6379> lset listkey 2 python
OK
127.0.0.1:6379> lrange listkey 0 -1
1) "java"
2) "b"
3) "python"

(5) 阻塞操作

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
blpop key [key ...] timeout
brpop key [key ...] timeout

# 1.列表为空:timeout=3,客户端等待3秒才能返回,timeout=0,客户端一直阻塞。
127.0.0.1:6379> brpop list:test 3
(nil)
127.0.0.1:6379> brpop list:test 0
...阻塞...
# 如果期间添加了数据element1,客户端立即返回。
127.0.0.1:6379> brpop list:test 3
1) "list:test"
2) "element1"
(2.06s)

# 2.列表不为空:客户端立即返回。
127.0.0.1:6379> brpop list:test 0
1) "list:test"
2) "element1"

blpop/brpop是lpop和rpop的阻塞版本,除了弹出方向不同,使用方法基本相同。

使用brpop时需要注意两点:

  • 如果是多个键,brpop会从左到右遍历键,一旦有一个可以弹出元素,客户端立即返回。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    127.0.0.1:6379> brpop list:1 list:2 list:3 0
    ..阻塞..
    # 另一个客户端同时分别插入元素
    client-lpush> lpush list:2 element2
    (integer) 1
    client-lpush> lpush list:3 element3
    (integer) 1
    # 客户端立即返回list:2中的element2,因为其最先有可以弹出的元素
    127.0.0.1:6379> brpop list:1 list:2 list:3 0
    1) "list:2"
    2) "element2_1"
  • 如果多个客户端对同一个键执行brpop,最先执行brpop命令的客户端可以获取到弹出的值。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    # 客户端1
    client-1> brpop list:test 0
    ..阻塞..
    # 客户端2
    client-2> brpop list:test 0
    ..阻塞..
    # 客户端3
    client-3> brpop list:test 0
    ..阻塞..

    # 此时另一个客户端lpush一个元素到list:test列表中
    client-lpush> lpush list:test element
    (integer) 1

    # 客户端1会获取到元素,因为其最先执行brpop,客户端2和3会继续阻塞
    127.0.0.1:6379> brpop list:1 list:2 list:3 0
    1) "list:test"
    2) "element"

2.4.2 内部编码

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

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

Redis 3.2版本提供了quicklist的内部编码,其结合了ziplist和linkedlist的优势,是一种更优秀的内部编码实现。

2.4.3 使用场景

(1) 消息队列

如下图所示,Redis的lpush+brpop命令组合即可实现阻塞队列,生产者客户端使用lpush从列表左侧插入元素,多个消费者客户端使用brpop命令阻塞式的“抢”列表尾部的元素,多个客户端保证了消费的负载均衡和高可用性。

(2) 文章列表

每个用户都有属于自己的文章列表,现需要分页展示文章列表。这种情况可以考虑列表,因为列表不但是有序的,同时支持按照索引范围获取元素。

  1. 每篇文章饰演哈希结构存储,例如每篇文章有三个属性:title、timestamp和content。

    1
    2
    3
    4
    hmset article:1 title xx timestamp 1476536196 content xxxx
    ...
    hmset article:k title yy timestamp 1476512536 content yyyy
    ...
  2. 向用户文章列表添加文章,user:{id}:articles作为用户文章列表的键。

    1
    2
    3
    4
    lpush user:1:articles article:1 article:3
    ...
    lpush user:k:articles article:5
    ...
  3. 分页获取用户文章列表,例如下面伪代码获取用户id=1的前10篇文章。

    1
    2
    3
    articles = lrange user:1:articles 0 9
    for article in {articles}
    hgetall {article}

使用列表类型来保存和获取文章列表会存在两个问题:

  • 如果每次分页获取的文章个数较多,需要执行多次hgetall操作,此时可以考虑使用Pipeline批量获取,或者考虑将文章数据序列化为字符串类型使用mget批量获取
  • 分页获取文章列表时,lrange命令在列表两端性能较好,但如果列表较大,获取列表中间范围的元素性能会变差,此时可以考虑将列表做二级拆分,或使用Redis 3.2的quicklist内部编码实现,获取中间的元素也可以高效完成。

使用列表时可以参考如下口诀:

  • lpush + lpop = Stack(栈)
  • lpush + rpop = Queue(队列)
  • lpush + ltrim = Capped Collection(有限集合)
  • lpush + brpop = Message Queue(消息队列)

2.5 集合

集合(set)类型也是用来保存多个字符串元素,但和列表不同的是,集合不允许有重复元素,且集合中的元素是无序的,不能通过索引下标获取元素。Redis除了支持集合内的增删改查,还支持了多个集合取交集、并集、差集。

2.5.1 命令

1到7为集合内操作,8到11为集合间操作。

(1) 添加元素

1
2
3
4
5
6
7
8
9
sadd key element [element ...]

# 返回结果为成功个数
127.0.0.1:6379> exists myset
(integer) 0
127.0.0.1:6379> sadd myset a b c
(integer) 3
127.0.0.1:6379> sadd myset a b
(integer) 0

(2) 删除元素

1
2
3
4
5
6
7
srem key element [element ...]

# 返回结果为成功个数
127.0.0.1:6379> srem myset a b
(integer) 2
127.0.0.1:6379> sadd myset hello
(integer) 0

(3) 计算元素个数

1
2
3
4
5
scard key

# scard时间复杂度为O(1),不会遍历集合,而是直接用Redis内部的变量(类似类中的计数器属性)
127.0.0.1:6379> scard myset
(integer) 1

(4) 判断元素是否在集合中

1
2
3
4
5
sismember key element

# 如果给定元素element在集合中就返回1,否则返回0
127.0.0.1:6379> srem myset c
(integer) 1

(5) 随机从集合返回指定个数元素

1
2
3
4
5
6
7
8
srandmember key [count]

# count是可选参数,默认为1
127.0.0.1:6379> srandmember myset 2
1) "a"
2) "c"
127.0.0.1:6379> srandmember myset
"d"

(6) 从集合随机弹出元素

1
2
3
4
5
6
7
8
9
spop key

# spop操作可以从集合随机弹出一个元素,如下一次pop后,元素变为(d, b, a)
127.0.0.1:6379> spop myset
"c"
127.0.0.1:6379> smembers myset
1) "d"
2) "b"
3) "a"

Redis 3.2版本后,spop也支持[count]参数。srandmember和spop都是随机选出元素,只不过spop会同时删掉此元素。

(7) 获取所有元素

1
2
3
4
5
6
7
smembers key

# 返回结果是无序的
127.0.0.1:6379> smembers myset
1) "d"
2) "b"
3) "a"

smembers和lrange、hgetall一样,都属于比较的命令,如果元素过多就存在阻塞Redis的可能,可以通过sscan来代替。

(8) 求多个集合的交集

1
2
3
4
5
6
7
8
9
10
11
sinter key [key ...]

# 新增两个集合
127.0.0.1:6379> sadd user:1:follow it music his sports
(integer) 4
127.0.0.1:6379> sadd user:2:follow it news ent sports
(integer) 4
# 获取交集
127.0.0.1:6379> sinter user:1:follow user:2:follow
1) "sports"
2) "it"

(9) 求多个集合的并集

1
2
3
4
5
6
7
8
9
10
sunion key [key ...]

# 获取并集
127.0.0.1:6379> sunion user:1:follow user:2:follow
1) "sports"
2) "it"
3) "his"
4) "news"
5) "music"
6) "ent"

(10) 求多个集合的差集

1
2
3
4
5
6
sdiff key [key ...]

# 获取差集
127.0.0.1:6379> sinter user:1:follow user:2:follow
1) "music"
2) "his"

(11) 将交集、并集、差集的结果保存

1
2
3
4
5
6
7
8
9
10
11
12
sinterstore destination key [key ...]
sunionstore destination key [key ...]
sdiffstore destination key [key ...]

# 集合间的运算在元素较多时会比较耗时,可以通过上述三个命令将结果保存在destination key中。
127.0.0.1:6379> sinterstore user:1_2:inter user:1:follow user:2:follow
(integer) 2
127.0.0.1:6379> type user:1_2:inter
set
127.0.0.1:6379> smembers user:1_2:inter
1) "it"
2) "sports"

2.5.2 内部编码

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

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

  • hashtable(哈希表):集合无法满足intset的条件时,选用hashtable作为集合内部实现。

2.5.3 使用场景

集合类型比较典型的使用场景是标签

例如一个用户可能对娱乐、体育比较感兴趣,另一个用户可能对历史、新闻比较感兴趣,这些兴趣点就可以是标签。有了这些用户数据,就可以从这个维度得到兴趣相投的用户,以及共同喜欢的标签等,这些对于提高用户体验以及增强用户粘度比较重要。

(1) 给用户添加标签

1
2
3
4
sadd user:1:tags tag1 tag2 tag5
sadd user:2:tags tag2 tag3 tag5
...
sadd user:k:tags tag1 tag2 tag4

(2) 给标签添加用户

1
2
3
4
sadd tag1:users user:1 user:3
sadd tag2:users user:1 user:2 user:3
...
sadd tagk:users user:1 user:2

(3) 删除用户下的标签

1
2
srem user:1:tags tag1 tag5
...

(4) 删除标签下的用户

1
2
3
srem tag1:users user:1
srem tag5:users user:1
...

3和4都是尽量放在一个事务下执行。

(5) 计算用户共同感兴趣的标签

可以通过sinter命令,来计算用户共同感兴趣的标签。

1
2
sinter user:1:tags user:2:tags
...

上述只是一种简单的标签,实际应用上可能会复杂很多。

集合类型一般有这几种应用场景:

  • sadd = Tagging(标签)

  • spop/srandmember = Random item(队列)

  • sadd + sinter = Social Graph(社交需求)


2.6 有序集合

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

下图给出了列表、集合、有序集合的异同点。

2.6.1 命令

1到11为集合内操作,12到13为集合间操作。

(1) 添加成员

1
2
3
4
5
zadd key store member [store member ...]

# 向有序集合user:ranking中添加用户tom和分数251,返回表示成功个数
127.0.0.1:6379> zadd user:ranking 251 tom
(integer) 1

Redis 3.2版本为zadd命令添加了nx、xx、ch、incr四个选项:

  • nx:member必须不存在,才可以设置成功,用于添加。
  • xx:member必须存在,才可以设置成功,用于更新。
  • ch:返回此次操作后,有序集合元素和分数变化的个数。
  • incr:对score做增加,相当于后面介绍的zincrby。

有序集合相比集合提供了排序字段,但是也产生了代价,zadd的时间复杂度为O(log(n)),sadd的时间复杂度为O(1)。

(2) 计算成员个数

1
2
3
4
5
zcard key

# zcard和scard一样,时间复杂度为O(1)
127.0.0.1:6379> zcard user:ranking
(integer) 5

(3) 计算某个成员分数

1
2
3
4
5
6
7
zscore key member

# 存在返回分数,不存在返回nil
127.0.0.1:6379> zscore user:ranking tom
"251"
127.0.0.1:6379> zscore user:ranking test
(nil)

(4) 计算成员的排名

1
2
3
4
5
6
7
8
zrank key member
zrevrank key member

# zrank是分数从低到高返回排名,zrevrank则相反
127.0.0.1:6379> zrank user:ranking tom
(integer) 5
127.0.0.1:6379> zrevrank user:ranking tom
(integer) 0

(5) 删除成员

1
2
3
4
5
zrem key member [member ...]

# 移除集合中元素,返回表示成功个数
127.0.0.1:6379> zrem user:ranking mike
(integer) 1

(6) 增加成员的分数

1
2
3
4
zincrby key increment member

127.0.0.1:6379> zincrby user:ranking 9 tom
"260"

(7) 返回指定排名范围内的成员

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
zrange key start end [withscores]
zrevrange key start end [withscores]

# 有序集合是按照分值排名的,zrange是从低到高返回,zrevrange则相反,withscores选项会同时返回分数。
127.0.0.1:6379> zrange user:ranking 0 2 withscores
1) "kris"
2) "1"
3) "frank"
4) "200"
5) "trim"
6) "220"
127.0.0.1:6379> zrevrange user:ranking 0 2 withscores
1) "tom"
2) "260"
3) "martin"
4) "250"
5) "trim"
6) "220"

(8) 返回指定分数范围内的成员

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
zrangebyscore key min max [withscores] [limit offset count]
zrevrangebyscore key min max [withscores] [limit offset count]

# zrangebyscore按照分值从低到高返回,zrevrangebyscore则相反,withscores选项会同时返回分数,[limit offset count]可以限制输出的起始位置和个数。
127.0.0.1:6379> zrangebyscore user:ranking 200 221 withscores
1) "frank"
2) "200"
3) "trim"
4) "220"
127.0.0.1:6379> zrevrangebyscore user:ranking 200 221 withscores
1) "trim"
2) "220"
3) "frank"
4) "200"
# min和max还支持开区间(小括号)和闭区间(中括号),-inf和+inf分别代表无限小和无限大
127.0.0.1:6379> zrangebyscore user:ranking (200 +inf withscores
1) "trim"
2) "220"
3) "martin"
4) "250"
5) "tom"
6) "260"

(9) 返回指定分数范围内的成员个数

1
2
3
4
zcount key min max

127.0.0.1:6379> zcount user:ranking 221 200
(integer) 1

(10) 删除指定排名内的升序元素

1
2
3
4
zremrangebyrank key start end

127.0.0.1:6379> zremrangebyrank user:ranking 0 2
(integer) 3

(11) 删除指定分数范围内的成员

1
2
3
4
zremrangebyscore key min max

127.0.0.1:6379> zremrangebyscore user:ranking (250 +inf
(integer) 2

(12) 交集

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

添加如图所示两个集合。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
127.0.0.1:6379> zadd user:ranking:1 1 kris 91 mike 200 frank 220 tim 250 martin 251 tom
(integer) 6
127.0.0.1:6379> zadd user:ranking:2 8 james 77 mike 625 martin 888 tom
(integer) 4

# 求两个集合的交集,weights和aggregate取默认值,分值做了sum操作
127.0.0.1:6379> zinterstore user:ranking:1_inter_2 2 user:ranking:1 user:ranking:2
(integer) 3
127.0.0.1:6379> zrange user:ranking:1_inter_2 0 -1 withscores
1) "mike"
2) "168"
3) "martin"
4) "875"
5) "tom"
6) "1139"
# 配置权重,使集合2权重为集合1一半,并使聚合效果变为max
127.0.0.1:6379> zinterstore user:ranking:1_inter_2 2 user:ranking:1 user:ranking:2 weights 1 0.5 aggregate max
(integer) 3
127.0.0.1:6379> zrange user:ranking:1_inter_2 0 -1 withscores
1) "mike"
2) "91"
3) "martin"
4) "312.5"
5) "tom"
6) "444"

(13) 并集

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
zunionstore destination numkeys key [key ...] [weights weight [weight ...]] [aggregate sum|min|max]

127.0.0.1:6379> zunionstore user:ranking:1_union_2 2 user:ranking:1 user:ranking:2
(integer) 7
127.0.0.1:6379> zrange user:ranking:1_inter_2 0 -1 withscores
1) "kris"
2) "1"
3) "james"
4) "8"
5) "mike"
6) "168"
7) "frank"
8) "200"
9) "tim"
10) "220"
11) "martin"
12) "875"
13) "tom"
14) "1139"

2.6.2 内部编码

有序集合有两种内部编码:

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

2.6.3 使用场景

有序集合比较常见的使用场景是排行榜系统。例如视频网站需要对用户上传的视频做排行榜,榜单的维度可能有多方面:时间、播放数量、获得的赞数。我们试着实现一个简单的赞数的维度。

(1) 添加用户赞数

1
2
3
4
# 用户mike上传了一个视频,并获得了3个赞
zadd user:ranking:2016_03_15 3 mike
# 又获得了一个赞
zincrby user:ranking:2016_03_15 1 mike

(2) 取消用户赞数

1
2
# 因为如用户注销或用户作弊等原因需要将用户以及获得赞数删除
zrem user:ranking:2016_03_15 mike

(3) 展示获取赞数最多的十个用户

1
zrevrangebyrank user:ranking:2016_03_15 0 9

(4) 展示用户信息以及用户分数

1
2
3
4
# 将用户名作为键后缀,将用户信息保存在哈希类型中
hgetall user:info:tom
zscore user:ranking:2016_03_15 mike
zrank user:ranking:2016_03_15 mike

2.7 键管理

从单个键管理、遍历键、数据库管理三个维度来介绍一些通用命令。

2.7.1 单个键管理

(1) 键重命名

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
rename key newkey

127.0.0.1:6379> get python
"jedis"
127.0.0.1:6379> set python jedis
OK
127.0.0.1:6379> rename python java
OK
127.0.0.1:6379> get python
(nil)
127.0.0.1:6379> get java
"jedis"
# 若rename之前,键java就已经存在,那么其值就会被覆盖
127.0.0.1:6379> set a b
OK
127.0.0.1:6379> set c d
OK
127.0.0.1:6379> rename a c
OK
127.0.0.1:6379> get a
(nil)
127.0.0.1:6379> get c
"b"
# 为了防止误操作被强行rename,Redis提供了renamenx命令,确保只有newKey不存在时才能覆盖
127.0.0.1:6379> set java jedis
OK
127.0.0.1:6379> set python redis-py
OK
127.0.0.1:6379> renamenx python java
(integer) 0
127.0.0.1:6379> get java
"jedis"
127.0.0.1:6379> get python
"redis-py"

使用重命名命令时需要注意:

  • 由于重命名键期间会执行del命令删除旧的键,如果键对应的值较大,会存在阻塞Redis的可能性。

  • 如果rename和renamenx中的key和newkey是相同的,在Redis3.2版本之后和之前结果略有不同。

    1
    2
    3
    4
    5
    6
    # 3.2
    127.0.0.1:6379> rename key key
    OK
    # 3.2之前
    127.0.0.1:6379> rename key key
    (error) ERR source and destination objects are the same

(2) 随机返回一个键

1
2
3
4
5
6
7
8
randomkey

127.0.0.1:6379> dbsize
1000
127.0.0.1:6379> randomkey
"hello"
127.0.0.1:6379> randomkey
"jedis"

(3) 键过期

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
# 键在seconds秒后过期
expire key seconds
# 键在秒级时间戳timestamp后过期
expireat key timestamp
# 键在milliseconds毫秒后过期
pexpire key milliseconds
# 键在毫秒级时间戳timestamp后过期
pexpireat key milliseconds-timestamp
# 查询键的剩余过期时间,返回值(大于等于0:表示剩余时间,-1:键没有设置过期时间,-2:键不存在)
ttl key
# 查询键的剩余过期时间,毫秒级
pttl key

127.0.0.1:6379> set hello world
OK
127.0.0.1:6379> expire hello 10
(integer) 1
# 还剩7秒
127.0.0.1:6379> ttl hello
(integer) 7
...
127.0.0.1:6379> ttl hello
(integer) 0
# 返回结果为-2,表示键已被删除
127.0.0.1:6379> ttl hello
(integer) -2
127.0.0.1:6379> expireat hello 1469980800
(integer) 1

不论是过期时间还是时间戳,秒级还是毫秒级,在Redis内部最终使用的都是pexpireat。

使用过期命令需要注意以下几点:

  1. 如果 expire key 的键不存在,返回结果为0。

    1
    2
    127.0.0.1:6379> expire not_exist_key 30
    (integer) 0
  2. 如果过期时间为负值,键会立即被删除,犹如使用del命令一样。

    1
    2
    3
    4
    5
    6
    127.0.0.1:6379> set hello world
    OK
    127.0.0.1:6379> expire hello -2
    (integer) 1
    127.0.0.1:6379> get hello
    (nil)
  3. persist命令可以将键的过期时间清除。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    127.0.0.1:6379> hset key f1 v1
    (integer) 1
    127.0.0.1:6379> expire key 50
    (integer) 1
    127.0.0.1:6379> ttl key
    (integer) 46
    127.0.0.1:6379> persist key
    (integer) 1
    127.0.0.1:6379> ttl key
    (integer) -1
  4. 对于字符串类型键,执行set命令会去掉过期时间,这个问题在开发中很容易被忽视。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    // set命令的函数setKey,可以看到最后执行了removeExpire(db, key)函数去掉了过期时间
    void setKey(redisDb *db, robj *key, robj *val) {
    if (lookupKeyWrite(db, key) == NULL) {
    dbAdd(db, key, val);
    } else {
    dbOverwrite(db, key, val);
    }
    incrRefCount(val);
    // 去掉过期时间
    removeExpire(db, key);
    signalModifiedKey(db, key);
    }
    1
    2
    3
    4
    5
    6
    7
    8
    127.0.0.1:6379> expire hello 50
    (integer) 1
    127.0.0.1:6379> ttl hello
    (integer) 46
    127.0.0.1:6379> set hello world
    OK
    127.0.0.1:6379> ttl hello
    (integer) -1
  5. Redis不支持二级数据结构(例如哈希、列表)内部元素的过期功能,例如不能对列表类型的一个元素做过期时间设置。

  6. setex命令作为set + expire的组合,不但是原子执行,同时减少了一次网络通讯的时间。

(4) 迁移键

有时我们会想把部分数据由一个Redis转移到另一个Redis,Redis发展历程中提供了move、restore、migrate三组迁移键的方法。

1) move
1
move key db

move指令用于Redis内部进行数据迁移,Redis内部可以有多个数据库,彼此数据是隔离的。move指令把指定的键从源数据库移动到目标数据库,但不建议在生产环境使用多数据库。

2) dump+restore
1
2
dump key
restore key ttl value

dump+restore可以实现在不同的Redis实例之间进行数据迁移的功能。

整个迁移的过程分为两步:

  1. 在源Redis上,dump命令会将键值序列化,格式采用的是RDB格式。
  2. 在目标Redis上,restore命令将上面序列化的值进行复原,其中ttl参数代表过期时间,如果ttl=0代表没有过期时间。

需要注意两点:第一,整个迁移过程并非原子性的,而是通过客户端分布完成的。第二,迁移过程是开启了两个客户端连接,所以dump的结果不是在源Redis和目标Redis之间进行传输。

演示过程:

  1. 在源Redis上执行dump。

    1
    2
    3
    4
    redis-source> set hello world
    OK
    redis-source> dump hello
    "\x00\x05world\x06\x00\x8f<T\x04%\xfcNQ"
  2. 在目标Redis上执行restore。

    1
    2
    3
    4
    5
    6
    redis-target> get hello
    (nil)
    redis-target> restore hello 0 "\x00\x05world\x06\x00\x8f<T\x04%\xfcNQ"
    OK
    redis-target> get hello
    "world"

    上面2步对应的伪代码如下。

    1
    2
    3
    Redis sourceRedis = new Redis("sourceMachine", 6379);
    Redis targetRedis = new Redis("targetMachine", 6379);
    targetRedis.restore("hello", 0, sourceRedis.dump(key));
  3. migrate

    1
    migrate host port key|"" destination-db timeout [copy] [replace] [keys key [key ...]]

    migrate命令也是用于在Redis实例间进行数据迁移的,实际上migrate命令就是将dump、restore、del三个命令进行组合,从而简化了操作流程。migrate命令具有原子性,而且从Redis 3.0.6版本以后已经支持迁移多个键的功能,有效地提高了迁移效率,migrate在水平扩容中起到重要作用。

如图所示,实现过程和dump+restore基本类似,但有三点不同。

  1. 整个过程是原子执行的,不需要在多个Redis实例上开启客户端的,只需要在源Redis上执行migrate命令即可。
  2. migrate命令的数据传输直接在源Redis和目标Redis上完成的。
  3. 目标Redis完成restore后会发送OK给源Redis,源Redis接收后会根据migrate对应的选项来决定是否在源Redis上删除对应的键。

migrate参数说明:

  • host:目标Redis的IP地址。
  • port:目标Redis的端口。
  • **key|“”**:在Redis 3.0.6版本之前,migrate只支持迁移一个键,所以此处是要迁移的键,但Redis 3.0.6版本之后支持迁移多个键,如果当前需要迁移多个键,此处为空字符串””。
  • destination-db:目标Redis的数据库索引,例如要迁移到0号数据库,这里就写0。
  • timeout:迁移的超时时间(单位为毫秒)。
  • **[copy]**:如果添加此选项,迁移后并不删除源键。
  • **[replace]**:如果添加此选项,migrate不管目标Redis是否存在该键都会正常迁移进行数据覆盖。
  • **[keys key [key …]]**:迁移多个键。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 源Redis使用6379端口,目标Redis使用6380端口

# 情况1:源Redis有键hello,目标Redis没有
127.0.0.1:6379> migrate 127.0.0.1 6380 hello 0 1000
OK
# 情况2:源Redis和目标Reids都有键hello,需要加replace选项
127.0.0.1:6379> get hello
"world"
127.0.0.1:6380> get hello
"redis"
127.0.0.1:6379> migrate 127.0.0.1 6380 hello 0 1000
(error) ERR Target instance replied with error: BUSYKEY Target key name already exists.
127.0.0.1:6379> migrate 127.0.0.1 6380 hello 0 1000 replace
OK
# 情况3:源Redis没有键hello
127.0.0.1:6379> migrate 127.0.0.1 6380 hello 0 1000
NOKEY

Redis 3.0.6版本以后迁移多个键的功能:

  • 源Redis批量添加多个键

    1
    2
    127.0.0.1:6379> mset key1 value1 key2 value2 key3 value3
    OK
  • 源Redis执行如下命令完成多个键的迁移

    1
    2
    127.0.0.1:6379> migrate 127.0.0.1 6380 "" 0 5000 keys key1 key2 key3
    OK

2.7.2 遍历键

(1) 全量遍历键

1
2
3
4
5
6
7
8
9
10
11
12
keys pattern

127.0.0.1:6379> dbsize
(integer) 0
127.0.0.1:6379> mset hello world redis best jedis best hill high
OK
# 获取所有键
127.0.0.1:6379> keys *
1) "hill"
2) "jedis"
3) "redis"
4) "hello"

pattern使用glob风格的通配符:

  • * :代表匹配任意字符。
  • ? :代表匹配一个字符。
  • [] :代表匹配部分字符,如[1, 3]代表匹配1和3,[1-10]代表匹配1到10的任意数字。
  • \x :用来做转移,例如要匹配星号、问号需要进行转义。
1
2
3
4
5
6
7
127.0.0.1:6379> keys [j,r]edis
1) "jedis"
2) "redis"

127.0.0.1:6379> keys h?ll*
1) "hill"
2) "hello"

当需要遍历所有键时(如检测过期或闲置时间、寻找大对象),keys是一个很有帮助的命令,例如想删除所有以video字符串开头的键,可以如下操作。

1
redis-cli keys video* | xargs redis-cli del

但Redis是单线程架构,如果对大量的键执行keys命令很可能会造成Redis阻塞,所以一般不建议在生产环境下使用keys命令。

生产环境下代替方案:

  • 在一个不对外提供服务的Redis从节点上执行,这样不会阻塞到客户端的请求,但会影响到主从复制。
  • 如果确认键值总数确实比较少,可以执行此命令。
  • 使用scan命令渐进式的遍历所有键,可以有效防止阻塞。

(2) 渐进式遍历

Redis 2.8版本后,提供了一个新的命令scan,有效的解决了keys命令存在的问题。与keys命令遍历所有键的方式不同,scan采用渐进式遍历的方式来解决keys命令可能带来的阻塞问题,scan命令的时间复杂度为O(1),但要真正的实现keys的功能,需要多次执行scan。Redis存储键值对实际使用的是hashtable的数据结构,简化模型如下图所示。

1
2
# 每次执行scan,可以想象成只扫描一个字典中的一部分键,知道将字典中的所有键遍历完毕
scan cursor [match pattern] [count number]
  • cursor:必须参数,实际上cursor是一个游标,第一次遍历从0开始,每次scan遍历完都返回当前游标的值,直到游标值为0,表示遍历结束。
  • match pattern:可选参数,它的作用是做模式的匹配,这点和keys的模式匹配很像。
  • count number:可选参数,它的作用是表明每次要遍历的键个数,默认值是10,此参数可以适当增大。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
# 现有一个Redis有26个键,遍历所有的键,第一次执行返回两个部分,第一部分的6就是下次scan需要的cursor,第二部分是10个键。
127.0.0.1:6379> scan 0
1) "6"
2) 1) "w"
2) "i"
3) "e"
4) "x"
5) "j"
6) "q"
7) "y"
8) "u"
9) "b"
10) "o"
# 使用新的cursor="6",执行scan 6
127.0.0.1:6379> scan 6
1) "11"
2) 1) "h"
2) "n"
3) "m"
4) "t"
5) "c"
6) "d"
7) "g"
8) "p"
9) "z"
10) "a"
# 这次得到的11,继续执行scan 11得到结果cursor为0,说明所有的键都被遍历过了
127.0.0.1:6379> scan 11
1) "0"
2) 1) "s"
2) "f"
3) "r"
4) "v"
5) "k"
6) "l"

除了scan以外,Redis还提供了面向哈希类型、集合类型、有序集合的扫描遍历命令,解决如hgetall、smembers、zrange可能产生的阻塞问题,对应的命令分别为hscan、sscan、zscan。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 当前集合有两种类型的元素,分别以old:user和new:user开头
String key = "myset";
//
String pattern = "old:user";
//
String cursor = "0";
while (true) {
//
ScanResult scanResult = redis.sscan(key, cursor, pattern);
List elements = scanResult.getResult();
if (elements != null && elements.size() > 0) {
//
redis.rem(key, elements);
}
//
cursor = scanResult.getStringCursor();
//
if ("0".equals(cursor)) {
break;
}
}

渐进式遍历可以有效的解决相应命令的阻塞问题,但scan并非完美,如果在遍历的过程中出现键的变化(增删改),遍历效果就可能会遇到:新增的键没被遍历到,遍历出重复的键等情况。即scan并不能保证完整的遍历出来所有的键

2.7.3 数据库管理

(1) 切换数据库

许多关系型数据库支持一个实例下有多个数据库存在,与关系型数据库用字符来区分不同数据库名相比,Redis使用数字作为多个数据库的实现,默认是16个数据库。

1
2
3
4
5
6
7
8
9
10
11
select dbIndex
database 16

127.0.0.1:6379> set hello world # 默认进到0号数据库
OK
127.0.0.1:6379> get hello
"world"
127.0.0.1:6379> select 15 # 切换到15号数据库
OK
127.0.0.1:6379[15]> get hello # 数据库间是隔离的
(nil)

上述过程如图所示,还有当使用 redis-cli -h {ip} -p {port} 链接Redis时,默认也是0号数据库,其他数据库会显示index下标。

那么可以像关系型数据库那样,0号数据库作为正式库,1号数据库作为测试库,这样二者的数据也不会互相影响呢?Redis 3.0开始慢慢的在淡化此功能,例如Redis的分布式实现Redis Cluster只允许使用0号数据库,只是为了向下兼容老版本才没有完全废弃此功能。

废弃这个功能的原因有三点:

  • Redis是单线程的,如果使用多个数据库,这些库仍然只使用一个CPU,还是会影响到彼此的。
  • 多数据库的使用方式,会让调试和运维不同业务的数据库变得困难。假如有一个慢查询存在,依然会影响其他数据库,这样会使得别的业务方定位问题变得困难。
  • 部分Redis客户端不支持这种方式,即使支持,在开发时来回切换数字形式的数据库也容易混乱。

所以一般建议在一台机器上部署多个Redis来实现,以端口号来区分,这样既保证了业务之间不会受影响,有利用了多核CPU的资源。

(2) 清除数据库

flushdb/flushall命令用于清除数据库,区别是flushdb只清除当前数据库,flushall会清除所有数据库。

flushdb/flushall命令存在的问题:

  • flushdb/flushall命令会将所有的数据清除,一旦误操作很难处理。
  • 如果当前数据库键值比较多,flushdb/flushall命令存在阻塞Redis的可能性。

参考:

🔗 《Redis开发与运维》