编辑
2024-06-06
💌中间键
00
请注意,本文编写于 378 天前,最后修改于 225 天前,其中某些信息可能已经过时。

目录

2.6 有序集合
2.6.1 命令
1.集合内
2.集合间的操作
2.6.2 内部编码
2.6.3 使用场景
(1)添加用户赞数
(2)取消用户赞数
(3)展示获取赞数最多的十个用户
(4)展示用户信息以及用户分数
2.7 键管理
2.7.1 单个键管理
1.键重命名
2.随机返回一个键
3.键过期
4.迁移键
2.7.2 遍历键
1.全量遍历键
2.渐进式遍历
2.7.3 数据库管理
1.切换数据库
2.flushdb/flushall

2.6 有序集合

image.png

开发提示

有序集合中的元素不能重复,但是score可以重复,就和一个班里的同

学学号不能重复,但是考试成绩可以相同。

image.png

2.6.1 命令

1.集合内

(1)添加成员

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

有关zadd命令有两点需要注意:

  • Redis3.2为zadd命令添加了nx、xx、ch、incr四个选项:
  • nx:member必须不存在,才可以设置成功,用于添加。
  • xx:member必须存在,才可以设置成功,用于更新。
  • ch:返回此次操作后,有序集合元素和分数发生变化的个数
  • incr:对score做增加,相当于后面介绍的zincrby。
  • 有序集合相比集合提供了排序字段,但是也产生了代价,zadd的时间复杂度为O(log(n)),sadd的时间复杂度为O(1)。

(2)计算成员个数

zcard key

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

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

(3)计算某个成员的分数

zscore key member

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

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

(4)计算成员的排名

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

(5)删除成员

zrem key member [member ...]

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

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

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

(6)增加成员的分数

zincrby key increment member

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

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

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

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"

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

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

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

zcount key min max

下面操作返回200到221分的成员的个数:

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

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

zremrangebyrank key start end

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

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

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

zremrangebyscore key min max

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

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

2.集合间的操作

将图2-25的两个有序集合导入到Redis中。

image.png

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

(1)交集

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"

(2)并集

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"

至此有序集合的命令基本介绍完了,表2-8是这些命令的时间复杂度,

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

image.png

2.6.2 内部编码

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

  • ziplist(压缩列表):当有序集合的元素个数小于zset-max-ziplist￾entries配置(默认128个),同时每个元素的值都小于zset-max-ziplist-value配置(默认64字节)时,Redis会用ziplist来作为有序集合的内部实现,ziplist可以有效减少内存的使用。
  • skiplist(跳跃表):当ziplist条件不满足时,有序集合会使用skiplist作为内部实现,因为此时ziplist的读写效率会下降。 下面用示例来说明:
  1. 当元素个数较少且每个元素较小时,内部编码为skiplist:
127.0.0.1:6379> zadd zsetkey 50 e1 60 e2 30 e3 (integer) 3 127.0.0.1:6379> object encoding zsetkey "ziplist"
  1. 当元素个数超过128个,内部编码变为ziplist:
127.0.0.1:6379> zadd zsetkey 50 e1 60 e2 30 e3 12 e4 ...忽略... 84 e129 (integer) 129 127.0.0.1:6379> object encoding zsetkey "skiplist"
  1. 当某个元素大于64字节时,内部编码也会变为hashtable:
127.0.0.1:6379> zadd zsetkey 20 "one string is bigger than 64 byte............. ..................." (integer) 1 127.0.0.1:6379> object encoding zsetkey "skiplist"

2.6.3 使用场景

按照时间、按照播放数量、按照获得的赞数。本节使用赞数这个维度,记录每天用户上传视频的排行榜。主要需要实现以下4个功能。

(1)添加用户赞数

例如用户mike上传了一个视频,并获得了3个赞,可以使用有序集合的zadd和zincrby功能:

zadd user:ranking:2016_03_15 mike 3

如果之后再获得一个赞,可以使用zincrby:

zincrby user:ranking:2016_03_15 mike 1

(2)取消用户赞数

由于各种原因(例如用户注销、用户作弊)需要将用户删除,此时需要将用户从榜单中删除掉,可以使用zrem。例如删除成员tom:

zrem user:ranking:2016_03_15 mike

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

此功能使用zrevrange命令实现:

zrevrangebyrank user:ranking:2016_03_15 0 9

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

此功能将用户名作为键后缀,将用户信息保存在哈希类型中,至于用户的分数和排名可以使用zscore和zrank两个功能:

hgetall user:info:tom zscore user:ranking:2016_03_15 mike zrank user:ranking:2016_03_15 mike

2.7 键管理

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

2.7.1 单个键管理

1.键重命名

rename key newkey

例如现有一个键值对,键为python,值为jedis:

127.0.0.1:6379> get 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"

为了防止被强行rename,Redis提供了renamenx命令,确保只有newKey不存在时候才被覆盖,

例如下面操作renamenx时,newkey=python已经存在,返回结果是0代表没有完成重命名,所以键java和python的值没变:

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 java python (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和之前版本返回结果略有不同。Redis3.2中会返回OK:
127.0.0.1:6379> rename key key OK

Redis3.2之前的版本会提示错误:

127.0.0.1:6379> rename key key (error) ERR source and destination objects are the same

2.随机返回一个键

randomkey

下面示例中,当前数据库有1000个键值对,randomkey命令会随机从中挑选一个键:

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

3.键过期

2.1节简单介绍键过期功能,它可以自动将带有过期时间的键删除,在许多应用场景都非常有帮助。

除了expire、ttl命令以外,Redis还提供了expireat、pexpire、pexpireat、pttl、persist等一系列命令,下面分别进行说明:

  • expire key seconds:键在seconds秒后过期。
  • expireat key timestamp:键在秒级时间戳timestamp后过期。 下面为键hello设置了10秒的过期时间,然后通过ttl观察它的过期剩余时间(单位:秒),随着时间的推移,ttl逐渐变小,最终变为-2:
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 ... #还剩0秒 127.0.0.1:6379> ttl hello (integer) 0 #返回结果为-2,说明键hello已经被删除 127.0.0.1:6379> ttl hello (integer) -2

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

  • 大于等于0的整数:键剩余的过期时间(ttl是秒,pttl是毫秒)。
  • -1:键没有设置过期时间。
  • -2:键不存在。 expireat命令可以设置键的秒级过期时间戳,例如如果需要将键hello在2016-08-0100:00:00(秒级时间戳为1469980800)过期,可以执行如下操作:
127.0.0.1:6379> expireat hello 1469980800 (integer) 1

除此之外,Redis2.6版本后提供了毫秒级的过期方案:

  • pexpire key milliseconds:键在milliseconds毫秒后过期。
  • pexpireat key milliseconds-timestamp键在毫秒级时间戳timestamp后过期。 但无论是使用过期时间还是时间戳,秒级还是毫秒级,在Redis内部最终使用的都是pexpireat。 在使用Redis相关过期命令时,需要注意以下几点。
  1. 如果expire key的键不存在,返回结果为0:
127.0.0.1:6379> expire not_exist_key 30 (integer) 0
  1. 如果过期时间为负值,键会立即被删除,犹如使用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)
  1. persist命令可以将键的过期时间清除:
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
  1. 对于字符串类型键,执行set命令会去掉过期时间,这个问题很容易在开发中被忽视。

如下是Redis源码中,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); }

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

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
  1. Redis不支持二级数据结构(例如哈希、列表)内部元素的过期功能,例如不能对列表类型的一个元素做过期时间设置。
  2. setex命令作为set+expire的组合,不但是原子执行,同时减少了一次网络通讯的时间。有关Redis键过期的详细原理,8.2节会深入剖析。

4.迁移键

迁移键功能非常重要,因为有时候我们只想把部分数据由一个Redis迁移到另一个Redis(例如从生产环境迁移到测试环境),

Redis发展历程中提供了move、dump+restore、migrate三组迁移键的方法,它们的实现方式以及使用的场景不太相同,下面分别介绍。

(1)move

move key db

如图2-26所示,move命令用于在Redis内部进行数据迁移,Redis内部可以有多个数据库,由于多个数据库功能后面会进行介绍,这里只需要知道Redis内部可以有多个数据库,彼此在数据上是相互隔离的,move key db就

是把指定的键从源数据库移动到目标数据库中,但笔者认为多数据库功能不建议在生产环境使用,所以这个命令读者知道即可

image.png

(2)dump+restore

dump key restore key ttl value

dump+restore可以实现在不同的Redis实例之间进行数据迁移的功能,整 个迁移的过程分为两步:

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

image.png

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

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

下面用一个例子演示完整过程。

  1. 在源Redis上执行dump:
redis-source> set hello world OK redis-source> dump hello "\x00\x05world\x06\x00\x8f<T\x04%\xfcNQ"
  1. 在目标Redis上执行restore:
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步对应的伪代码如下:

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

(3)migrate

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

migrate命令也是用于在Redis实例间进行数据迁移的,实际上migrate命令就是将dump、restore、del三个命令进行组合,从而简化了操作流程。

migrate命令具有原子性,而且从Redis3.0.6版本以后已经支持迁移多个键的功能,有效地提高了迁移效率,migrate在10.4节水平扩容中起到重要作用。

整个过程如图2-28所示,实现过程和dump+restore基本类似,但是有3点不太相同:

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

image.png

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

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

下面用示例演示migrate命令,为了方便演示源Redis使用6379端口,目标Redis使用6380端口,现要将源Redis的键hello迁移到目标Redis中,会分为如下几种情况:

情况1:源Redis有键hello,目标Redis没有: 127.0.0.1:6379> migrate 127.0.0.1 6380 hello 0 1000 OK 情况2:源Redis和目标Redis都有键hello: 127.0.0.1:6379> get hello "world" 127.0.0.1:6380> get hello "redis"

如果migrate命令没有加replace选项会收到错误提示,如果加了replace会返回OK表明迁移成功:

127.0.0.1:6379> migrate 127.0.0.1 6379 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 6379 hello 0 1000 replace OK 情况3:源Redis没有键hello。如下所示,此种情况会收到nokey的提 示: 127.0.0.1:6379> migrate 127.0.0.1 6380 hello 0 1000 NOKEY

下面演示一下Redis3.0.6版本以后迁移多个键的功能。

  • 源Redis批量添加多个键:
127.0.0.1:6379> mset key1 value1 key2 value2 key3 value3 OK
  • 源Redis执行如下命令完成多个键的迁移:
127.0.0.1:6379> migrate 127.0.0.1 6380 "" 0 5000 keys key1 key2 key3 OK

至此有关Redis数据迁移的命令介绍完了,最后使用表2-9总结一下move、dump+restore、migrate三种迁移方式的异同点,笔者建议使用migrate命令进行键值迁移。

image.png

2.7.2 遍历键

1.全量遍历键

keys pattern

本章开头介绍keys命令的简单使用,实际上keys命令是支持pattern匹配的,例如向一个空的Redis插入4个字符串类型的键值对。

127.0.0.1:6379> dbsize (integer) 0 127.0.0.1:6379> mset hello world redis best jedis best hill high OK

如果要获取所有的键,可以使用keys pattern命令:

127.0.0.1:6379> keys * 1) "hill" 2) "jedis" 3) "redis" 4) "hello"

上面为了遍历所有的键,pattern直接使用星号,这是因为pattern使用的是glob风格的通配符:

  • *代表匹配任意字符。
  • 代表匹配一个字符。
  • []代表匹配部分字符,例如[1,3]代表匹配1,3,[1-10]代表匹配1到10的任意数字。
  • \x用来做转义,例如要匹配星号、问号需要进行转义。 下面操作匹配以j,r开头,紧跟edis字符串的所有键:
127.0.0.1:6379> keys [j,r]edis 1) "jedis" 2) "redis"

例如下面操作会匹配到hello和hill这两个键:

127.0.0.1:6379> keys hll* 1) "hill" 2) "hello"

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

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

但是如果考虑到Redis的单线程架构就不那么美妙了,如果Redis包含了大量的键,执行keys命令很可能会造成Redis阻塞,所以一般建议不要在生产环境下使用keys命令。

但有时候确实有遍历键的需求该怎么办,可以在以下三种情况使用:

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

2.渐进式遍历

Redis从2.8版本后,提供了一个新的命令scan,它能有效的解决keys命令存在的问题。

和keys命令执行时会遍历所有键不同,scan采用渐进式遍历的方式来解决keys命令可能带来的阻塞问题,每次scan命令的时间复杂度是O(1),

但是要真正实现keys的功能,需要执行多次scan。Redis存储键值对实际使用的是hashtable的数据结构,其简化模型如图2-29所示。

image.png

那么每次执行scan,可以想象成只扫描一个字典中的一部分键,直到将字典中的所有键遍历完毕。scan的使用方法如下:

scan cursor [match pattern] [count number]
  • cursor是必需参数,实际上cursor是一个游标,第一次遍历从0开始,每次scan遍历完都会返回当前游标的值,直到游标值为0,表示遍历结束。
  • match pattern是可选参数,它的作用的是做模式的匹配,这点和keys的模式匹配很像。
  • count number是可选参数,它的作用是表明每次要遍历的键个数,默认值是10,此参数可以适当增大。

现有一个Redis有26个键(英文26个字母),现在要遍历所有的键,使用scan命令效果的操作如下。第一次执行scan0,返回结果分为两个部分:

第一个部分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",执行scan6:

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"

这次得到的cursor="11",继续执行scan11得到结果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,它们的用法和scan基本类似,

下面以sscan为例子进行说明,当前集合有两种类型的元素,例如分别以old:user和new:user开头,先需要将old:user开头的元素全部删除,可以参考如下伪代码:

String key = "myset"; // 定义pattern String pattern = "old:user*"; // 游标每次从0开始 String cursor = "0"; while (true) { // 获取扫描结果 ScanResult scanResult = redis.sscan(key, cursor, pattern); List elements = scanResult.getResult(); if (elements != null && elements.size() > 0) { // 批量删除 redis.srem(key, elements); } // 获取新的游标 cursor = scanResult.getStringCursor(); // 如果游标为0表示遍历结束 if ("0".equals(cursor)) { break; } }

渐进式遍历可以有效的解决keys命令可能产生的阻塞问题,但是scan并非完美无瑕,如果在scan的过程中如果有键的变化(增加、删除、修改),

那么遍历效果可能会碰到如下问题:新增的键可能没有遍历到,遍历出了重复的键等情况,也就是说scan并不能保证完整的遍历出来所有的键,这些是我们在开发时需要考虑的。

2.7.3 数据库管理

1.切换数据库

select dbIndex

许多关系型数据库,例如MySQL支持在一个实例下有多个数据库存在的,但是与关系型数据库用字符来区分不同数据库名不同,Redis只是用数字作为多个数据库的实现。Redis默认配置中是有16个数据库:

databases 16

假设databases=16,select0操作将切换到第一个数据库,select15选择最后一个数据库,但是0号数据库和15号数据库之间的数据没有任何关联,甚至可以存在相同的键:

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 #因为15号数据库和0号数据库是隔离的,所以get hello为空 (nil)

图2-30更加生动地表现出上述操作过程。同时可以看到,当使用rediscli-h{ip}-p{port}连接Redis时,默认使用的就是0号数据库,

当选择其他数据库时,会有[index]的前缀标识,其中index就是数据库的索引下标。

image.png

那么能不能像使用测试数据库和正式数据库一样,把正式的数据放在0号数据库,测试的数据库放在1号数据库,那么两者在数据上就不会彼此受影响了。

事实真有那么好吗?

Redis3.0中已经逐渐弱化这个功能,例如Redis的分布式实现RedisCluster只允许使用0号数据库,只不过为了向下兼容老版本的数据库功能,

该功能没有完全废弃掉,下面分析一下为什么要废弃掉这个“优秀”的功能呢?总结起来有三点:

  • Redis是单线程的。如果使用多个数据库,那么这些数据库仍然是使用一个CPU,彼此之间还是会受到影响的。

  • 多数据库的使用方式,会让调试和运维不同业务的数据库变的困难,假如有一个慢查询存在,依然会影响其他数据库,这样会使得别的业务方定位问题非常的困难。

  • 部分Redis的客户端根本就不支持这种方式。即使支持,在开发的时候来回切换数字形式的数据库,很容易弄乱。

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

2.flushdb/flushall

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

例如当前0号数据库有四个键值对、1号数据库有三个键值对:

127.0.0.1:6379> dbsize (integer) 4 127.0.0.1:6379> select 1 OK 127.0.0.1:6379[1]> dbsize (integer) 3

如果在0号数据库执行flushdb,1号数据库的数据依然还在:

127.0.0.1:6379> flushdb OK 127.0.0.1:6379> dbsize (integer) 0 127.0.0.1:6379> select 1 OK 127.0.0.1:6379[1]> dbsize (integer) 3

在任意数据库执行flushall会将所有数据库清除:

127.0.0.1:6379> flushall OK 127.0.0.1:6379> dbsize (integer) 0 127.0.0.1:6379> select 1 OK 127.0.0.1:6379[1]> dbsize (integer) 0

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

  • flushdb/flushall命令会将所有数据清除,一旦误操作后果不堪设想,第12章会介绍rename-command配置规避这个问题,以及如何在误操作后快速恢复数据。
  • 如果当前数据库键值数量比较多,flushdb/flushall存在阻塞Redis的可能性。所以在使用flushdb/flushall一定要小心谨慎。

本文作者:Eric

本文链接:

版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!