开发提示
有序集合中的元素不能重复,但是score可以重复,就和一个班里的同
学学号不能重复,但是考试成绩可以相同。
(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命令有两点需要注意:
(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-25的两个有序集合导入到Redis中。
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]
这个命令参数较多,下面分别进行说明:
下面操作对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阻塞。
有序集合类型的内部编码有两种:
127.0.0.1:6379> zadd zsetkey 50 e1 60 e2 30 e3 (integer) 3 127.0.0.1:6379> object encoding zsetkey "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"
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"
按照时间、按照播放数量、按照获得的赞数。本节使用赞数这个维度,记录每天用户上传视频的排行榜。主要需要实现以下4个功能。
例如用户mike上传了一个视频,并获得了3个赞,可以使用有序集合的zadd和zincrby功能:
zadd user:ranking:2016_03_15 mike 3
如果之后再获得一个赞,可以使用zincrby:
zincrby user:ranking:2016_03_15 mike 1
由于各种原因(例如用户注销、用户作弊)需要将用户删除,此时需要将用户从榜单中删除掉,可以使用zrem。例如删除成员tom:
zrem user:ranking:2016_03_15 mike
此功能使用zrevrange命令实现:
zrevrangebyrank user:ranking:2016_03_15 0 9
此功能将用户名作为键后缀,将用户信息保存在哈希类型中,至于用户的分数和排名可以使用zscore和zrank两个功能:
hgetall user:info:tom zscore user:ranking:2016_03_15 mike zrank user:ranking:2016_03_15 mike
本节将按照单个键、遍历键、数据库管理三个维度对一些通用命令进行介绍。
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"
在使用重命名命令时,有两点需要注意:
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
randomkey
下面示例中,当前数据库有1000个键值对,randomkey命令会随机从中挑选一个键:
127.0.0.1:6379> dbsize 1000 127.0.0.1:6379> randomkey "hello" 127.0.0.1:6379> randomkey "jedis"
2.1节简单介绍键过期功能,它可以自动将带有过期时间的键删除,在许多应用场景都非常有帮助。
除了expire、ttl命令以外,Redis还提供了expireat、pexpire、pexpireat、pttl、persist等一系列命令,下面分别进行说明:
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种返回值:
127.0.0.1:6379> expireat hello 1469980800 (integer) 1
除此之外,Redis2.6版本后提供了毫秒级的过期方案:
127.0.0.1:6379> expire not_exist_key 30 (integer) 0
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)
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
如下是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
迁移键功能非常重要,因为有时候我们只想把部分数据由一个Redis迁移到另一个Redis(例如从生产环境迁移到测试环境),
Redis发展历程中提供了move、dump+restore、migrate三组迁移键的方法,它们的实现方式以及使用的场景不太相同,下面分别介绍。
(1)move
move key db
如图2-26所示,move命令用于在Redis内部进行数据迁移,Redis内部可以有多个数据库,由于多个数据库功能后面会进行介绍,这里只需要知道Redis内部可以有多个数据库,彼此在数据上是相互隔离的,move key db就
是把指定的键从源数据库移动到目标数据库中,但笔者认为多数据库功能不建议在生产环境使用,所以这个命令读者知道即可
(2)dump+restore
dump key restore key ttl value
dump+restore可以实现在不同的Redis实例之间进行数据迁移的功能,整 个迁移的过程分为两步:
有关dump+restore有两点需要注意:
下面用一个例子演示完整过程。
redis-source> set hello world OK redis-source> dump hello "\x00\x05world\x06\x00\x8f<T\x04%\xfcNQ"
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点不太相同:
下面对migrate的参数进行逐个说明:
下面用示例演示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版本以后迁移多个键的功能。
127.0.0.1:6379> mset key1 value1 key2 value2 key3 value3 OK
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命令进行键值迁移。
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风格的通配符:
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从2.8版本后,提供了一个新的命令scan,它能有效的解决keys命令存在的问题。
和keys命令执行时会遍历所有键不同,scan采用渐进式遍历的方式来解决keys命令可能带来的阻塞问题,每次scan命令的时间复杂度是O(1),
但是要真正实现keys的功能,需要执行多次scan。Redis存储键值对实际使用的是hashtable的数据结构,其简化模型如图2-29所示。
那么每次执行scan,可以想象成只扫描一个字典中的一部分键,直到将字典中的所有键遍历完毕。scan的使用方法如下:
scan cursor [match pattern] [count number]
现有一个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并不能保证完整的遍历出来所有的键,这些是我们在开发时需要考虑的。
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就是数据库的索引下标。
那么能不能像使用测试数据库和正式数据库一样,把正式的数据放在0号数据库,测试的数据库放在1号数据库,那么两者在数据上就不会彼此受影响了。
事实真有那么好吗?
Redis3.0中已经逐渐弱化这个功能,例如Redis的分布式实现RedisCluster只允许使用0号数据库,只不过为了向下兼容老版本的数据库功能,
该功能没有完全废弃掉,下面分析一下为什么要废弃掉这个“优秀”的功能呢?总结起来有三点:
Redis是单线程的。如果使用多个数据库,那么这些数据库仍然是使用一个CPU,彼此之间还是会受到影响的。
多数据库的使用方式,会让调试和运维不同业务的数据库变的困难,假如有一个慢查询存在,依然会影响其他数据库,这样会使得别的业务方定位问题非常的困难。
部分Redis的客户端根本就不支持这种方式。即使支持,在开发的时候来回切换数字形式的数据库,很容易弄乱。
笔者建议如果要使用多个数据库功能,完全可以在一台机器上部署多个Redis实例,彼此用端口来做区分,因为现代计算机或者服务器通常是有多个CPU的。这样既保证了业务之间不会受到影响,又合理地使用了CPU资源。
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命令可以非常方便的清理数据,但是也带来两个问题:
本文作者:Eric
本文链接:
版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!