Redis提供了5种数据结构,理解每种数据结构的特点对于Redis开发运维非常重要,同时掌握Redis的单线程命令处理机制,会使数据结构和命令的选择事半功倍。在此之前,我们先要了解一下Redis的一些全局命令、数据结构和内部编码以及单线程命令处理机制。

# 全局命令

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

# keys-查看所有键

keys *

为了演示这个命令的效果,我们先插入3条数据。

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

keys*命令会将所有的键输出。

127.0.0.1:6379> keys *
1) "hello"
2) "python"
3) "java"
127.0.0.1:6379>

# dbsize-统计键总数

dbsize

下面,我们插入一个列表类型的键值对(值是由多个元素组成)

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

dbsize命令会返回当前数据库中键的总数。例如当前数据库有4个键,分别是hello、java、python、mylist,所以dbsize的结果是4。即

127.0.0.1:6379> dbsize
(integer) 4

WARNING

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

# exists-检查键是否存在

exists key

如果键存在则返回1,不存在则返回0,例如,下面这个例子

127.0.0.1:6379> exists java
(integer) 1
127.0.0.1:6379> exists not_exists_key
(integer) 0

# del-删除键

del key [key ...]

del是一个通用命令,无论值是什么数据结构类型,del命令都可以将其删除,例如下面将字符串类型的键java和列表类型的键mylist分别删除

127.0.0.1:6379> del java
(integer) 1
127.0.0.1:6379> exists java
(integer) 0
127.0.0.1:6379> del mulist
(integer) 0
127.0.0.1:6379> del mylist
(integer) 1
127.0.0.1:6379> exists mylist
(integer) 0

返回结果为成功删除键的个数,假设删除一个不存在的键,就会返回0。

同时del命令可以支持删除多个键

127.0.0.1:6379> set a 1
OK
127.0.0.1:6379> set b 2
OK
127.0.0.1:6379> set c 3
OK
127.0.0.1:6379> del a b c
(integer) 3

# expire-键过期

expire key seconds

Redis支持对键添加过期时间,当超过过期时间后,会自动删除键,例如为键hello设置了10秒过期时间。

127.0.0.1:6379> set hello world
OK
127.0.0.1:6379> expire hello 10
(integer) 1

ttl命令会返回键的剩余过期时间,它有3种返回值:

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

# type-返回键的数据结构类型

type key

假设键hello是字符串类型,则返回string,键mylist是列表类型,返回结果为list。

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

如果键不存在,则返回none。

127.0.0.1:6379>  type not_exsit_key
none

# 数据结构和内部编码

type命令实际返回的就是当前键的数据结构类型,它们分别是:string(字符串)、hash(哈希)、list(列表)、set(集合)、zset(有序集合),但这些只是Redis对外的数据结构。

实际上每种数据结构都有自己底层的内部编码实现,而且是多种实现,这样Redis会在合适的场景选择合适的内部编码,可以通过object encoding命令查询数据类型的内部编码。

127.0.0.1:6379> object encoding hello
"embstr"
127.0.0.1:6379> object encoding mylist
"quicklist"

使用内部编码的好处:

  • 改进内部编码,对外的数据结构和命令没有影响,这样一旦开发出更优秀的内部编码,无需改动外部数据结构和命令
  • 多种内部编码实现可以在不同场景下发挥各自的优势

# Redis的单线程架构

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

现在我们同时开启多个redis-cli客户端同时执行命令,对于客户端来说,每次客户端调用都经历了发送命令、执行命令、返回结果三个过程。

其中第二个过程即执行命令过程是相当关键的,因为Redis是单线程来处理命令的,所以一条命令从客户端到达服务端不会被立即执行,所有的命令都会进入一个队列中,然后被逐个执行。所以三个客户端执行命令的先后顺序是不确定的。但是可以确定不会有两条命令被同时执行,不会产生并发问题,这就是Redis单线程的基本模型。但是对像发送命令、返回结果、命令排队这些过程的处理,Redis使用了I/O多路复用技术来解决I/O的问题。

通常来讲,单线程处理能力要比多线程差,Redis使用单线程模型会达到每秒万级别的处理能力。究其原因,可以归为以下几个方面:

  • 纯内存访问,Redis将所有数据放在内存中,内存的响应时长大约为100纳秒,这是Redis达到每秒万级别访问的重要基础。
  • 非阻塞I/O,Redis使用epoll作为I/O多路复用技术的实现,再加上Redis自身的事件处理模型将epoll中的连接、读写、关闭都转换为事件,不在网络I/O上浪费过多的时间。
  • 单线程避免了线程切换和竞态产生的消耗,但是单线程会有一个问题:对于每个命令的执行时间是有要求的。如果 某个命令执行过长,会造成其他命令的阻塞,对于Redis这种高性能的服务来说是致命的,所以Redis是面向快速执行场景的数据库。

# Redis数据结构-字符串

字符串类型是Redis最基础的数据结构。 首先键都是字符串类型,而且其他几种数据结构都是在字符串类型基础上构建的,所以字符串类型能为其他四种数据结构的学习奠定基础。

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

字符串类型的命令比较多,本小节将按照常用和不常用两个维度进行说明。

# 常用命令-设置值

set key value [ex seconds] [px milliseconds] [nx|xx]

下面操作设置键为hello,值为world的键值对,返回结果为OK代表设置成功。

127.0.0.1:6379> set hello world
OK

下面是set命令几个常用的选项

命令格式 作用
ex seconds 为键设置秒级过期时间
px milliseconds 为键设置毫秒级过期时间
nx 键必须不存在,才可以设置成功,若键存在,则设置失败
xx 与nx相反,键必须存在,才可以设置成功,用于更新。

除了set选项外,Redis还提供了setex和setnx两个命令。

setex key seconds value
setnx key value

它们的作用和ex和nx选项是一样的。清除字符串类型的hello字符串,重新设置字符串hello的值为world

 set hello world

因为键hello已存在,所以setnx失败,返回结果为0

127.0.0.1:6379> setnx hello redis
(integer) 0

因为键hello已存在,所以set xx成功,返回结果为OK。

127.0.0.1:6379> set hello jedis xx
OK

由于Redis的单线程命令处理机制,如果有多个客户端同时执行setnx key value,根据setnx的特性只有一个客户端能设置成功,setnx可以作为分布式锁的一种实现方案。

# 常用命令-获取值

get key

例如下面,我们来获取键hello的值。

127.0.0.1:6379> get hello
"jedis"

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

127.0.0.1:6379> get not_exist_key
(nil)

当然,我们也可以批量设置值或批量获取值,批量获取值的命令格式如下。

mset key value [key value ...]

下面操作通过mset命令一次性设置4个键值对

127.0.0.1:6379> mset a 1 b 2 c 3 d 4
OK

批量获取值的命令格式如下

mget key [key ...]

下面操作批量获取了键a、b、c、d的值。

127.0.0.1:6379> mget a b c d
1) "1"
2) "2"
3) "3"
4) "4"

如果有些键不存在,那么它的值为nil(空),结果是按照传入键的顺序返回

127.0.0.1:6379> mget a f c d
1) "1"
2) (nil)
3) "3"
4) "4"

其中,键f是不存在的,所以返回nil(空)。

Redis可以支撑每秒数万的读写操作,但是这指的是Redis服务端的处理能力,对于客户端来说,一次命令除了命令时间还是有网络时间,学会使用批量操作,有助于提高业务处理效率,但是要注意的是每次批量操作所发送的命令数不是无节制的,如果数量过多可能造成Redis阻塞或者网络拥塞。

# 常用命令-计数

incr key

incr命令用于对值做自增操作,返回结果分为三种情况:

  • 值不是整数,返回错误
  • 值是整数,返回自增后的结果
  • 键不存在,按照值为0自增,返回结果为1

例如对一个不存在的键执行incr操作后,返回结果是1。

127.0.0.1:6379> exists key
(integer) 0
127.0.0.1:6379> incr key
(integer) 1

再次对键执行incr命令,返回结果是2。

127.0.0.1:6379> incr key
(integer) 2

如果值不是整数,则返回一个错误。

127.0.0.1:6379> exists hello
(integer) 1
127.0.0.1:6379> incr hello
(error) ERR value is not an integer or out of range

除了incr命令,Redis提供了decr(自减)、incrby(自增指定数字)、decrby(自减指定数字)、incrbyfloat(自增浮点数)。

decr key
incrby key increment
decrby key decrement
incrbyfloat key increment

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

# 不常用命令-追加值

追加值的命令为append,其格式如下:

append key value

append可以向字符串尾部追加值,并返回追加后的字符串总长度。例如:

127.0.0.1:6379> get hello
"jedis"
127.0.0.1:6379> append hello client
(integer) 11
127.0.0.1:6379> get hello
"jedisclient"

# 不常用命令-计算字符串长度

计算字符串长度的命令为strlen,其格式如下:

strlen key

例如,字符串hello的当前值为jedisclient,其长度为11,所以使用strlen命令返回值11。

127.0.0.1:6379> strlen hello
(integer) 11

对于中文来说,每个字符占用三个字节

127.0.0.1:6379> set hello 世界 xx
OK
127.0.0.1:6379> strlen hello
(integer) 6

# 不常用命令-设置并返回原值

getset key value

getset和set一样会设置值,但是不同的是,它同时会返回键原来的值

127.0.0.1:6379> getset hello world
"\xe4\xb8\x96\xe7\x95\x8c"

\xe4\xb8\x96\xe7\x95\x8c其实就是中文"世界"。

# 不常用命令-设置指定位置的字符

设置指定位置的字符命令格式如下:

setrange key offeset value

例如,我们将下面的键redis的值由pest改为best。

127.0.0.1:6379> set redis pest
OK
127.0.0.1:6379> setrange redis 0 b
(integer) 4
127.0.0.1:6379> get redis
"best"

# 不常用命令-获取部分字符串

获取部分字符串的命令格式如下:

getrange key start end

start和end分别是开始和结束的偏移量,偏移量从0开始计算。例如下面的命令获取键redis的前两个字符。

127.0.0.1:6379> getrange redis 0 1
"be"

# 字符串类型命令时间复杂度

下表展示了字符串类型命令的时间复杂度,开发人员可以参考此表,结合自身业务需求和数据大小选择适合的命令。

命令 时间复杂度
set key value O(1)
get key O(1)
del key [key...] O(k),k是键的个数
mset key value [key value....] O(k),k是键的个数
mget key [key....] O(k),k是键的个数
incr key O(1)
decr key O(1)
incrby key increment O(1)
decrby key increment O(1)
incrbyfloat key increment O(1)
append key value O(1)
strlen key value O(1)
setrange key offset value O(1)
getrange key start end O(n),n是字符串长度,由于获取字符串非常快,所以如果字符串不是很长,可以视为O(1)

# 字符串类型的内部编码

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

  • int:8个字节的长整型。
  • embstr:小于等于39个字节的字符串(3.2版本以前),在3.2版本之后,则变成了44字节。
  • raw:大于39个字节的字符串,(3.2版本以前),在3.2版本之后,则变成了44字节。

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

整型类型示例:

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

短字符串示例如下:

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

长字符串示例如下:

127.0.0.1:6379> set key "one string greater than 39 byte.............."
OK
127.0.0.1:6379> object encoding key
"raw"

# 字符串的典型应用场景

  • 缓存功能: 将数据缓存在redis中,避免频繁访问数据库。
  • 计数: 可能用于统计视频播放次数,每播放一次则递增1。
  • 共享session: 在分布式web服务时,使用Redis将用户的Session进行集中管理,避免用户重复登录。
  • 限速: 很多应用出于安全的考虑,会在每次进行登录时,让用户输入手机验证码,从而确定是否是用户本人。但是为了短信接口不被频繁访问,会限制用户每分钟获取验证码的频率。

# Redis数据结构-哈希

几乎所有的编程语言都提供了哈希(hash)类型,它们的叫法可能是哈希、字典、关联数组。在Redis中,哈希类型是指键值本身又是一个键值对结构

哈希类型中的映射关系叫作field-value,这里的value是指field对应的值,不是键对应的值,请注意value在不同上下文的作用。

# 常用命令-设置值

设置值的命令格式如下:

hset key field value

下面为user:1添加一对field-value

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

如果设置成功会返回1,反之会返回0。此外Redis提供了hsetnx命令,它们的关系就像set和setnx命令一样,只不过作用域由键变为field。

# 常用命令-获取值

获取值的命令格式如下:

hget key field

例如,下面操作获取user:1的name域(属性)对应的值。

127.0.0.1:6379> hget user:1 name
"tom"

如果键或field不存在,会返回nil。

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

# 常用命令-删除field

和前面的类似,删除field的命令格式如下:

hdel key field [field ...]

hdel会删除一个或多个field,返回结果为成功删除field的个数,例如:

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

# 常用命令-计算field个数

计算filed个数的命令格式如下:

hlen key

例如user:1有3个field:

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

# 常用命令-批量设置或获取field-value

hmget key field [field ...]
hmset key field value [field value ...]

hmsethmget命令分别是批量设置和获取field-value,hmset需要的参数是key和多对field-value,hmget需要的参数是key和多个field。

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"

# 常用命令-判断field是否存在

判断field是否存在的命令格式如下:

hexists key field

例如,user:1包含name域,所以返回结果为1,不包含时返回0。

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

# 常用命令-获取所有field

获取所有field的命令格式如下:

hkeys key

hkeys命令应该叫hfields更为恰当,它返回指定哈希键所有的field,例如:

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

# 常用命令-获取所有value

获取所有value的命令格式如下:

hvals key

下面的命令获取user:1全部value

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

# 常用命令-获取所有的field-value

获取所有field-value的命令格式如下:

hgetall key

下面操作获取user:1所有的field-value

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

TIP

在使用hgetall命令时,如果哈希元素个数比较多,会存在阻塞Redis的可能。如果开发人员只需要获取部分field,可以使用hmget命令,如果一定要获取全部field-value,可以使用hscan命令,该命令会渐进式遍历哈希类型。

# 常用命令-计数

计数命令和字符串的计数命令类似,其命令格式如下:

hincrby key field
hincrbyfloat key field

hincrbyhincrbyfloat命令,就像incrbyincrbyfloat命令一样,但是它们的作用域是filed

# 常用命令-计算value的字符串长度

若我们需要计算value的字符串长度,可以使用如下命令:

hstrlen key field

例如hget user:1 name的value是mike,那么hstrlen的返回结果是3:

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

# 哈希命令类型的时间复杂度

下表是哈希类型命令的时间复杂度,开发人员可以参考此表选择适合的命令。

命令 时间复杂度
hset key field value O(1)
hget key field O(1)
hdel key field [field...] O(k),k是field的个数
hlen key O(1)
hgetall key O(n),n是field的个数
hmget field [field...] O(k),k是field的个数
hmset key field value [field value...] O(k),k是field的个数
hexists key field O(1)
hkeys key O(n),n是field的个数
hvals key O(n),n是field的个数
hsetnx key field value O(1)
hincrby key field increment O(1)
hincrbyfloat key field increment O(1)
hstrlen key field O(1)

# 哈希类型的内部编码

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

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

# 哈希的使用场景

与使用字符串序列化缓存用户信息相比,哈希类型变得更加直观,并且在更新操作上会更加便捷。可以将每个用户的id定义为键后缀,多对fieldvalue对应每个用户的属性,类似如下伪代码。

UserInfo getUserInfo(long id){
// 用户id作为key后缀
userRedisKey = "user:info:" + id;
// 使用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. 原生字符串类型:每个属性一个键。
    • 优点:简单直观,每个属性都支持更新操作。
    • 缺点:占用过多的键,内存占用量较大,同时用户信息内聚性比较差,所以此种方案一般不会在生产环境使用。
  2. 序列化字符串类型:将用户信息序列化后用一个键保存。
    • 优点:简化编程,如果合理的使用序列化可以提高内存的使用效率。
    • 缺点:序列化和反序列化有一定的开销,同时每次更新属性都需要把全部数据取出进行反序列化,更新后再序列化到Redis中。
  3. 哈希类型:每个用户属性使用一对field-value,但是只用一个键保存。
    • 优点:简单直观,如果使用合理可以减少内存空间的使用。
    • 缺点:要控制哈希在ziplist和hashtable两种内部编码的转换,hashtable会消耗更多内存。

# Redis数据结构-列表

列表(list)类型是用来存储多个有序的字符串,例如,a、b、c、d、e五个元素从左到右组成了一个有序的列表,列表中的每个字符串称为元素(element)。在Redis中,可以对列表两端插入(push)和弹出(pop),还可以获取指定范围的元素列表、获取指定索引下标的元素等。

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

# 列表的主要特点

列表主要有两个比较重要的特点:

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

总而言之,列表中的元素是有序且可重复的。

# 列表的四种操作类型

下面将按照对列表的5种操作类型对命令进行介绍。

操作类型 操作
添加 rpush、lpush、linsert
lrange、lindex、llen
删除 lpop、rpop、lrem、ltrim
修改 lset
阻塞操作 blpop、brpop

# 常用命令-添加

  1. 从右边插入元素

    rpush key value [value ...]
    

    下面我们从右向左插入元素c、b、a。

    127.0.0.1:6379> rpush listkey c b a
    (integer) 3
    

    lrange 0 -1命令可以从左到右获取列表的所有元素。

    127.0.0.1:6379> lrange listkey 0 -1
    1) "c"
    2) "b"
    3) "a"
    
  2. 从左边插入元素

    lpush key value [value ...]
    

    使用方法和rpush命令相同,只不过从左侧插入。

  3. 向某个元素前或者后插入元素

    linsert key before|after pivot value
    

    linsert命令会从列表中找到等于pivot的元素,在其前(before)或者后(after)插入一个新的元素value,例如下面操作会在列表的元素b前插入java。

    127.0.0.1:6379> linsert listkey before b java
    (integer) 4
    127.0.0.1:6379>
    

    返回的结果为4,代表当前列表的长度,当前列表包含以下元素

    127.0.0.1:6379>  lrange listkey 0 -1
    1) "c"
    2) "java"
    3) "b"
    4) "a"
    

# 常用命令-查找

  1. 获取指定范围内的元素列表

    lrange key start end
    

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

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

  • lrange命令中的end选项包含了自身,这个和很多编程语言不包含end不太相同,例如想获取列表的第2到第4个元素,可以执行如下操作:

    127.0.0.1:6379> lrange listkey 1 3
    1) "java"
    2) "b"
    3) "a"
    
  1. 获取列表指定索引下标的元素

     lindex key index
    

    例如当前列表最后一个元素为a,我们按照上面的命令格式获取最后一个元素。

       127.0.0.1:6379> lindex listkey -1
       "a"
    
  2. 获取列表长度

    llen key
    

    例如我们想获取当前列表的长度

    127.0.0.1:6379> llen listkey
    (integer) 4
    

# 常用命令-删除

  1. 从列表左侧弹出元素

    lpop key
    

    如下操作将列表最左侧的元素c弹出,弹出后列表变为java、b、a。

    127.0.0.1:6379> lpop listkey
    "c"
    127.0.0.1:6379> lrange listkey 0 -1
    1) "java"
    2) "b"
    3) "a"
    
  2. 从列表右侧弹出

    rpop key
    

    它的使用方法和lpop是一样的,只不过从列表右侧弹出。

  3. 删除指定元素

    lrem key count value
    

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

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

    例如向列表从左向右插入5个a,那么当前列表变为“a a a a a java b a”,下面操作将从列表左边开始删除4个为a的元素。

    127.0.0.1:6379> lpush listkey a a a a a
    (integer) 8
    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"
    
  4. 按照索引范围修剪列表

    ltrim key start end
    

    例如,下面操作会只保留列表listkey第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"
    
  5. 修改指定索引下标的元素

    lset key index newValue
    

    下面操作会将列表listkey中的第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"
    

# 常用命令-阻塞操作

阻塞式弹出命令格式如下:

blpop key [key ...] timeout
brpop key [key ...] timeout

blpopbrpoplpoprpop命令的阻塞版本,它们除了弹出方向不同,使用方法基本相同,所以下面以brpop命令进行说明,brpop命令包含两个参数。

  • key [key...]: 多个列表的键。
  • timeout: 阻塞时间(单位:秒)

下面分列表是否为空两种情况讨论。

  1. 若列表为空:如果timeout=3,那么客户端要等到3秒后返回,如果timeout=0,那么客户端一直阻塞等下去。

    127.0.0.1:6379> brpop list:test 3
    (nil)
    (3.03s)
    127.0.0.1:6379> brpop list:test 0
    ...一直阻塞...
    

    如果此期间添加了数据element1,客户端立即返回。

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

    127.0.0.1:6379> lpush list:test a b c d e f g
    (integer) 7
    

    再执行bprop命令

    127.0.0.1:6379> brpop list:test 0
    1) "list:test"
    2) "b"
    

    此时,客户端立即返回了。

在使用brpop时,我们需要注意以下两点:

  • 第一点,如果是多个键,那么brpop会从左至右遍历键,一旦有一个键,能弹出元素,客户端立即返回。

    127.0.0.1:6379> brpop list:1 list:2 list:3 0
    ...阻塞...
    

    此时另一个客户端分别向list:2和list:3插入元素。

    127.0.0.1:6379> lpush list:2 element2
    (integer) 1
    127.0.0.1:6379> lpush list:3 element3
    (integer) 1
    

    客户端会立即返回list:2中的element2,因为list:2最先有可以弹出的元素。

    127.0.0.1:6379> brpop list:1 list:2 list:3 0
    1) "list:2"
    2) "element2"
    (22.88s)
    
  • 第二点,如果多个客户端对同一个键执行brpop,那么最先执行brpop命令的客户端可以获取到弹出的值。

    客户端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最会获取到元素,因为客户端1最先执行brpop,而客户端2和客户端3继续阻塞。

# 列表命令时间复杂度

下面我们根据不同的命令类型分贝列出命令及其复杂度数据。

  • 添加命令时间复杂度

    命令 复杂度
    rpush key value [value...] O(k),k是元素个数
    lpush key value [value...] O(k),k是元素个数
    linsert key before | after pivot value O(n),n是距离列表头或尾的距离
  • 查找命令时间复杂度

    命令 复杂度
    lrange key start end O(s+n),s是start的偏移量,n是start到end的范围
    lindex key index O(n),n是索引的偏移量
    llen key O(1)
  • 删除命令时间复杂度

    命令 复杂度
    lpop key O(1)
    rpop key O(1)
    lrem key count value O(n),n是列表长度
    ltrim key start end O(n),n是要裁剪的元素总数
  • 修改命令时间复杂度

    命令 复杂度
    lset key index value O(n),n是索引的偏移量
  • 阻塞命令时间复杂度

    命令 复杂度
    brpop blpop O(1)

# 列表类型的内部编码

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

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

TIP

Redis3.2版本提供了quicklist内部编码,简单地说它是以一个ziplist为节点的linkedlist,它结合了ziplist和linkedlist两者的优势,为列表类型提供了一种更为优秀的内部编码实现。

# 列表的使用场景

  • 消息队列:Redis的lpush+brpop命令组合即可实现阻塞队列,生产者客户端使用lrpush从列表左侧插入元素,多个消费者客户端使用brpop命令阻塞式的“抢”列表尾部的元素,多个客户端保证了消费的负载均衡和高可用性。
  • 文章列表:每个用户有属于自己的文章列表,现需要分页展示文章列表。此时可以考虑使用列表,因为列表不但是有序的,同时支持按照索引范围获取元素。

TIP

实际上列表的使用场景很多,在选择时可以参考以下口诀:

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

# Redis数据结构-集合

集合(set)类型也是用来保存多个的字符串元素,但和列表类型不一样的是,集合中不允许有重复元素,并且集合中的元素是无序的,不能通过索引下标获取元素。Redis除了支持集合内的增删改查,同时还支持多个集合取交集、并集、差集,合理地使用好集合类型,能在实际开发中解决很多实际问题。

下面将按照集合内和集合间两个维度对集合的常用命令进行介绍。

# 集合内操作-添加元素

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

当我们重复添加重复元素时,无法再添加进集合里。

# 集合内操作-删除元素

srem key element [element ...]

返回结果为成功删除元素个数。

127.0.0.1:6379> srem myset a b
(integer) 2
127.0.0.1:6379> srem myset hello
(integer) 0

# 集合内操作-计算元素个数

scard key

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

127.0.0.1:6379> scard myset
(integer) 1

# 集合内操作-判断元素是否在集合中

sismember key element

如果给定元素element在集合内返回1,反之返回0,例如:

127.0.0.1:6379> sismember myset c
(integer) 1

# 集合内操作-随机从集合返回指定个数元素。

srandmember key [count]

count是可选参数,如果不写默认为1,例如:

127.0.0.1:6379> sadd myset a b
(integer) 2
127.0.0.1:6379> srandmember myset 2
1) "b"
2) "c"
127.0.0.1:6379> srandmember myset
"a"
127.0.0.1:6379>

# 集合内操作-从集合随机弹出元素

spop key

spop命令操作可以从集合中随机弹出一个元素。

127.0.0.1:6379> spop myset
"a"
127.0.0.1:6379> smembers myset
1) "b"
2) "c"

需要注意的是Redis从3.2版本开始,spop命令也支持[count]参数。其命令格式和前面的命令类似,若不写,则默认为弹出一个。

spop key [count]

TIP

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

# 集合内操作-获取所有元素

smembers key

下面代码获取集合myset所有元素,并且返回结果是无序的:

127.0.0.1:6379> smembers myset
1) "b"
2) "c"

WARNING

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

# 集合间操作-求多个集合的交集

现在有两个集合,它们分别是user:1:follow和user:2:follow。

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

若我们想求多个集合的交集,则可以使用下面形式的命令。

sinter key [key ...]

例如下面代码是求user:1:follow和user:2:follow两个集合的交集,返回结果是sports、it。

127.0.0.1:6379> sinter user:1:follow user:2:follow
1) "sports"
2) "it"

# 集合间操作-求多个集合的并集

suinon key [key ...]

例如下面代码是求user:1:follow和user:2:follow两个集合的并集,返回结果是sports、it、his、news、music、ent。

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

# 集合间操作-求多个集合的差集

sdiff key [key ...]

例如下面代码是求user:1:follow和user:2:follow两个集合的差集,返回结果是music和his。

127.0.0.1:6379> smembers user:1:follow
1) "music"
2) "sports"
3) "it"
4) "his"
127.0.0.1:6379> smembers user:2:follow
1) "ent"
2) "sports"
3) "it"
4) "news"
127.0.0.1:6379> sdiff user:1:follow user:2:follow
1) "music"
2) "his"

# 集合间操作-将交集、并集、差集的结果保存

sinterstore destination key [key ...]
suionstore destination key [key ...]
sdiffstore destination key [key ...]

集合间的运算在元素较多的情况下会比较耗时,所以Redis提供了上面三个命令(原命令+store)将集合间交集、并集、差集的结果保存在destination key中。

例如下面操作将user:1:follow和user:2:follow两个集合的交集结果保存在user:1_2:inter中,user:1_2:inter本身也是集合类型:

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"

# 集合常用命令时间复杂度

命令 时间复杂度
sadd key element [element...] O(k),k是元素个数
srem key elememt [element...] O(k),k是元素个数
scard key O(1)
sismember key element O(1)
srandmember key [count] O(count)
spop key O(1)
smembers key O(n),n是元素总数
sinter key [key....]或 O(m*k),k是多个集合中元素最少的集合的元素个数,m是键个数
sunion key [key...]或 O(k),k是多个集合元素个数和
sdiff key [key...] O(k),k是多个集合元素个数和

# 集合类型的内部编码

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

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

# 集合的使用场景

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

# Redis数据结构-有序集合

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

有序集合提供了获取指定分数和元素范围查询、计算成员排名等功能,合理的利用有序集合,能帮助我们在实际开发中解决很多问题。

TIP

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

# 列表、集合和有序集合三者的异同点

数据结构 是否允许重复元素 是否有序 有序实现方式 应用场景
列表 索引下标 时间轴、消息队列等
集合 标签、社交等
有序集合 分值 排行榜系统、社交等

# 集合内操作-添加成员

zadd key score member [score member ...]

下面操作向有序集合user:ranking添加用户tom和他的分数251。

127.0.0.1:6379> zadd user:ranking 251 tom
(integer) 1

返回结果代表成功添加成员的个数。

127.0.0.1:6379> zadd user:ranking 1 kris 91 mike 200 frank 220 tim 250 martin
(integer) 5

Redis3.2为zadd命令添加了nx、xx、ch、incr四个选项

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

:: : tip

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

:::

# 集合内操作-计算成员个数

zcard key

例如下面操作返回有序集合user:ranking的成员数为5,和集合类型的scard命令一样,zcard的时间复杂度为O(1)。

127.0.0.1:6379> zcard user:ranking
(integer) 6

# 集合内操作-计算某个成员的分数

zscore key member

tom的分数为251,如果成员不存在则返回nil:

127.0.0.1:6379> zscore user:ranking tom
"251"
127.0.0.1:6379> zscore user test
(nil)

# 集合内操作-计算成员的排名

zrank key member
zrevrank key member

zrank是从分数从低到高返回排名,zrevrank反之。例如下面操作中,tom在zrank和zrevrank分别排名第5和第0(排名从0开始计算)。

127.0.0.1:6379> zrank user:ranking tom
(integer) 5
127.0.0.1:6379> zrevrank user:ranking tom
(integer) 0

# 集合内操作-删除成员

zrem key member [member ...]

下面操作将成员mike从有序集合user:ranking中删除。

127.0.0.1:6379> zrem user:ranking mike
(integer) 1

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

# 集合内操作-增加成员分数

zincrby key increment member

下面操作给tom增加了9分,分数变为了260分。

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

# 集合内操作-返回指定排名范围的成员

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) "tim"
6) "220"
127.0.0.1:6379> zrevrange user:ranking 0 2 withscores
1) "tom"
2) "260"
3) "martin"
4) "250"
5) "tim"
6) "220"

# 集合内操作-返回指定分数范围的成员

zrangebyscore key min max [withscores] [limit offset count]
zrevrangebyscore key max min [withscores] [limit offset count]

其中zrangebyscore按照分数从低到高返回,zrevrangebyscore反之。例如下面操作从低到高返回200到221分的成员,withscores选项会同时返回每个成员的分数。[limit offset count]选项可以限制输出的起始位置和个数:

127.0.0.1:6379> zrangebyscore user:ranking 200 221 withscores
1) "frank"
2) "200"
3) "tim"
4) "220"
127.0.0.1:6379> zrevrangebyscore user:ranking 221 200 withscores
1) "tim"
2) "220"
3) "frank"
4) "200"

同时min和max还支持开区间(小括号)和闭区间(中括号),-inf+inf分别代表无限小和无限大。

127.0.0.1:6379> zrangebyscore user:ranking (200 +inf withscores
1) "tim"
2) "220"
3) "martin"
4) "250"
5) "tom"
6) "260"

# 集合内操作-返回指定分数范围成员个数

zcount key min max

下面的命令返回200到221分的成员的个数。

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

# 集合内操作-删除指定排名内的升序元素

zremrangebyrank key start end

下面操作删除第start到第end名的成员:

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

# 集合内操作-删除指定分数范围的成员

zremrangebyscore key min max

下面操作将250分以上的成员全部删除,返回结果为成功删除的个数:

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

现在,我们先添加两个集合user:ranking:1和user:ranking:2。

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

# 集合间操作-求多个集合的交集

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

这个命令参数较多,下面分别进行说明:

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

下面操作对user:ranking:1和user:ranking:2做交集,weights和aggregate使用了默认配置,可以看到目标键user:ranking:1_inter_2对分值做了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"

如果想让user:ranking:2的权重变为0.5,并且聚合效果使用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"

# 集合间操作-求多个集合的并集

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

该命令的所有参数和zinterstore是一致的,只不过是做并集计算,例如下面操作是计算user:ranking:1和user:ranking:2的并集,weights和aggregate使用了默认配置,可以看到目标键user:ranking:1_union_2对分值做了sum操作。

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_union_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"

开发人员在使用对应的命令进行开发时,不仅要考虑功能性,还要了解相应的时间复杂度,防止由于使用不当造成应用方效率下降以及Redis阻塞。

# 有序集合命令的时间复杂度

有序集合复杂度

# 有序集合内部编码

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

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

# 有序集合的使用场景

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

# Redis键管理

本节将按照单个键、遍历键、数据库管理三个维度对一些通用命令进行介绍。

# 单个键管理-键重命名管理

针对单个键的命令,前面几节已经介绍过一部分了,例如typedelobjectexistsexpire等,下面将介绍剩余的几个重要命令。

键重命名管理

rename key newkey

例如现有一个键值对,键为python,值为jedis,下面将键python重命名为java:

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"

此时原来,键为c,值为d的字符串类型数据被覆盖。

为了防止被强行rename,Redis提供了renamenx命令,确保只有newKey不存在时候才被覆盖,例如下面操作renamenx时,newkey=python已经存在,返回结果是0代表没有完成重命名,所以键java和python的值没变:

127.0.0.1:6379> get java
"jedis"
127.0.0.1:6379> set python redis-py
OK
127.0.0.1:6379> renamenx java python
(integer) 0
127.0.0.1:6379> get java
"jedis"
127.0.0.1:6379> get python
"redis-py"

在使用重命名命令时,有两点需要注意:

  • 由于重命名键期间会执行del命令删除旧的键,如果键对应的值比较大,会存在阻塞Redis的可能性,这点不要忽视。
  • 如果renamerenamenx中的key和newkey如果是相同的,在Redis3.2和之前版本返回结果略有不同。Redis3.2中会返回OK,Redis3.2之前的版本会提示错误。

# 单个键管理-随机返回一个键

randomkey

下面,我们随机返回一个键。

127.0.0.1:6379> randomkey
"listkey"
127.0.0.1:6379> randomkey
"user:ranking:1_inter_2"

# 单个键管理-键过期

前面简单介绍键过期功能,它可以自动将带有过期时间的键删除,在许多应用场景都非常有帮助。除了expirettl命令以外,Redis还提供了expireatpexpirepexpireatpttlpersist等一系列命令,下面分别进行说明。

  • expire key seconds:键在seconds秒后过期
  • expireat key timestamp:键在秒级时间戳timestamp后过期

下面为键hello设置了10秒的过期时间,然后通过ttl观察它的过期剩余时间(单位:秒),随着时间的推移,ttl逐渐变小,最终变为-2:

127.0.0.1:6379> expire hello 10
(integer) 1
127.0.0.1:6379> ttl hello
(integer) 6
127.0.0.1:6379> ttl hello
(integer) 1
127.0.0.1:6379> ttl hello
(integer) -2

ttl命令和pttl都可以查询键的剩余过期时间,但是pttl精度更高可以达到毫秒级别,有3种返回值:

  • 大于等于0的整数:键剩余的过期时间(ttl是秒,pttl是毫秒)
  • -1:键没有设置过期时间
  • -2:键不存在

expireat命令可以设置键的秒级过期时间戳,除此之外,Redis2.6版本后提供了毫秒级的过期方案:

  • pexpire key milliseconds:键在milliseconds毫秒后过期。
  • pexpireat key milliseconds-timestamp:键在毫秒级时间戳timestamp后过期。

在使用Redis相关过期命令时,需要注意以下几点。

  • 如果expire key的键不存在,返回结果为0

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

    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)
    
  • persist命令可以将键的过期时间清除:

    127.0.0.1:6379> hset fv1 f1 v1
    (integer) 1
    127.0.0.1:6379> expire fv1 50
    (integer) 1
    127.0.0.1:6379> ttl fv1
    (integer) 43
    127.0.0.1:6379> persist fv1
    (integer) 1
    127.0.0.1:6379> ttl key
    (integer) -1
    
  • 对于字符串类型键,执行set命令会去掉过期时间,这个问题很容易在开发中被忽视。

    下面的例子证实了set会导致过期时间失效,因为ttl变为-1:

    127.0.0.1:6379> set hello world
    OK
    127.0.0.1:6379> expire hello 50
    (integer) 1
    127.0.0.1:6379> ttl hello
    (integer) 41
    127.0.0.1:6379> set hello world
    OK
    127.0.0.1:6379> ttl hello
    (integer) -1
    
  • Redis不支持二级数据结构(例如哈希、列表)内部元素的过期功能,例如不能对列表类型的一个元素做过期时间设置。

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

# 单个键管理-move-Redis内部键迁移

迁移键功能非常重要,因为有时候我们只想把部分数据由一个Redis迁移到另一个Redis(例如从生产环境迁移到测试环境),Redis发展历程中提供了movedump+restoremigrate三组迁移键的方法,它们的实现方式以及使用的场景不太相同。

move`-Redis内部进行数据迁移

move key db

move命令用于在Redis内部进行数据迁移,Redis内部可以有多个数据库,彼此在数据上是相互隔离的,move key db就是把指定的键从源数据库移动到目标数据库中,但不建议在生产环境使用,所以这个命令读者知道即可。

# 单个键管理-dump+restore-Redis实例间键迁移

dump+restore-在不同的Redis实例之间进行数据迁移

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

  • 在源Redis上,dump命令会将键值序列化,格式采用的是RDB格式。

    dump key
    
  • 在目标Redis上,restore命令将上面序列化的值进行复原,其中ttl参数代表过期时间,如果ttl=0代表没有过期时间。

    restore key ttl value
    

有关dump+restore有两点需要注意:

  • 整个迁移过程并非原子性的,而是通过客户端分步完成的。
  • 迁移过程是开启了两个客户端连接,所以dump的结果不是在源Redis和目标Redis之间进行传输。

# 单个键管理-migrate-Redis实例间键迁移

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

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

migrate实现过程和dump+restore基本类似,但是有3点不太相同:

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

下面对migrate的参数进行逐个说明:

  • host:目标Redis的IP地址。
  • port:目标Redis的端口。
  • key|"":在Redis3.0.6版本之前,migrate只支持迁移一个键,所以此处是要迁移的键,但Redis3.0.6版本之后支持迁移多个键,如果当前需要迁移多个键,此处为空字符串""。
  • destination-db:目标Redis的数据库索引,例如要迁移到0号数据库,这里就写0。
  • timeout:迁移的超时时间(单位为毫秒)
  • [replace]:如果添加此选项,migrate不管目标Redis是否存在该键都会正常迁移进行数据覆盖。
  • [keys key[key...]]:迁移多个键,例如要迁移key1、key2、key3,此处填写“keys key1 key2 key3”。

# 单个键管理-movedump+restoremigrate三个命令比较

命令 作用域 原子性 支持多个键
move Redis实例内部
dump+restore Redis实例之间
migrate Redis实例之间

# 遍历键-全量遍历键

Redis提供了两个命令遍历所有的键,分别是keys和scan,本节将对它们介绍并简要分析。

keys pattern

事实上,keys命令支持glob风格的通配符:

  • *****:代表匹配任意字符。
  • ?:匹配一个字符。
  • []:代表匹配部分字符,例如[1,3]代表匹配1,3,[1-10]代表匹配1到10的任意数字。
  • \x:用来做转义,例如要匹配星号、问号需要进行转义。

下面操作匹配以j,r开头,紧跟edis字符串的所有键:

127.0.0.1:6380> keys [j,r]edis
1) "redis"

WARNING

当需要遍历所有键时(例如检测过期或闲置时间、寻找大对象等),keys是一个很有帮助的命令。但是如果考虑到Redis的单线程架构就不那么美妙了,如果Redis包含了大量的键,执行keys命令很可能会造成Redis阻塞,所以一般建议不要在生产环境下使用keys命令。

若实在有遍历键的需求,可以在以下三种情况使用:

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

# 遍历键-渐进式遍历

Redis从2.8版本后,提供了一个新的命令scan,它能有效的解决keys命令存在的问题。和keys命令执行时会遍历所有键不同,scan采用渐进式遍历的方式来解决keys命令可能带来的阻塞问题,每次scan命令的时间复杂度是O(1),但是要真正实现keys的功能,需要执行多次scan

scan的使用方法如下:

scan cursor [match pattern] [count number]

其参数含义如下:

  • cursor是必需参数,实际上cursor是一个游标,第一次遍历从0开始,每次scan遍历完都会返回当前游标的值,直到游标值为0,表示遍历结束。

  • match pattern是可选参数,它的作用的是做模式的匹配,这点和keys的模式匹配很像。

  • count number是可选参数,它的作用是表明每次要遍历的键个数,默认值是10,此参数可以适当增大。

现有一个Redis有41个键,现在要遍历所有的键,使用scan命令效果的操作如下。第一次执行scan 0,返回结果分为两个部分:第一个部分30,就是下次scan需要的cursor,第二个部分是这次遍历的key。

127.0.0.1:6380> scan 0
1) "30"
2)  1) "user:1:follow"
    2) "c"
    3) "b"
    4) "user:ranking:1_union_2"
    5) "fv1"
    6) "user:ranking:1_inter_2"
    7) "python"
    8) "hello"
    9) "list:3"
   10) "user:ranking"
   11) "myset"

此时,使用新的cursor=“30”,执行scan 30命令。

 scan 30
1) "27"
2)  1) "mylist"
    2) "listkey"
    3) "user:2:follow"
    4) "java"
    5) "d"
    6) "list:test"
    7) "key"
    8) "redis"
    9) "user:1"
   10) "user:ranking:2"

这次得到cursor=“27”,继续执行scan 27命令。

scan 27
1) "0"
2) 1) "user:1_2:inter"
   2) "user:ranking:1"

此时,cursor=“0”,表示所有的键已经被遍历过了。

除了scan以外,Redis提供了面向哈希类型、集合类型、有序集合的扫描遍历命令,解决诸如hgetallsmemberszrange可能产生的阻塞问题,对应的命令分别是hscansscanzscan,它们的用法和scan基本类似。

渐进式遍历的并非没有缺点:渐进式遍历可以有效的解决keys命令可能产生的阻塞问题,但是scan并非完美无瑕,如果在scan的过程中如果有键的变化(增加、删除、修改),那么遍历效果可能会碰到如下问题:新增的键可能没有遍历到,遍历出了重复的键等情况,也就是说scan并不能保证完整的遍历出来所有的键,这些是我们在开发时需要考虑的。

# 数据库管理之切换数据库

Redis提供了几个面向Redis数据库的操作,它们分别是dbsizeselectflushdb/flushall命令,本节将通过具体的使用场景介绍这些命令。

select dbIndex

TIP

许多关系型数据库,例如MySQL支持在一个实例下有多个数据库存在的,但是与关系型数据库用字符来区分不同数据库名不同,Redis只是用数字作为多个数据库的实现。Redis默认配置中是有16个数据库,各个数据库之间数据是相互隔离的,没有任何关联。

当使用rediscli-h{ip}-p{port}连接Redis时,默认使用的就是0号数据库,当选择其他数据库时,会有[index]的前缀标识,其中index就是数据库的索引下标。

127.0.0.1:6379> keys hell*
1) "hello"
127.0.0.1:6379> get hello
"world"
127.0.0.1:6379> select 15
OK
127.0.0.1:6379[15]> get hello
(nil)

Redis3.0中已经逐渐弱化这个功能,例如Redis的分布式实现Redis Cluster只允许使用0号数据库,只不过为了向下兼容老版本的数据库功能,该功能没有完全废弃掉。究其原因无非以下三点:

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

如果要使用多个数据库功能,完全可以在一台机器上部署多个Redis实例,彼此用端口来做区分,因为现代计算机或者服务器通常是有多个CPU的。这样既保证了业务之间不会受到影响,又合理地使用了CPU资源。

# 数据库管理之清除数据库

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

flushdb/flushall命令可以非常方便的清理数据,但是也带来两个问题:

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

总而言之,慎重使用flushdb/flushall

LastUpdated: 10/4/2019, 12:10:46 AM