《Redis入门指南》一4.3 排序

4.3 排序

Redis入门指南
午后,宋老师正在批改学生们提交的程序,再过几天就会迎来第一次计算机全市联考。他在每个学生的程序代码末尾都用注释详细地做了批注——严谨的治学态度让他备受学生们的爱戴。

一个电话打来。“小白的?”宋老师拿出手机,“博客最近怎么样了?”未及小白开口,他就抢先问道。

特别好!现在平均每天都有50多人访问我的博客。不过昨天我收到一个访客的邮件,他向我反映了一个问题:查看一个标签下的文章列表时文章不是按照时间顺序排列的,找起来很麻烦。我看了一下代码,发现程序中是使用SMEMBERS命令获取标签下的文章列表,因为集合类型是无序的,所以不能实现按照文章的发布时间排列。我考虑过使用有序集合类型存储标签,但是有序集合类型的集合操作不如集合类型强大。您有什么好方法来解决这个问题吗?

方法有很多,我推荐使用SORT命令,你先挂了电话,我写好后发邮件给你吧。

4.3.1 有序集合的集合操作

集合类型提供了强大的集合操作命令,但是如果需要排序就要用到有序集合类型。Redis的作者在设计Redis的命令时考虑到了不同数据类型的使用场景,对于不常用到的或者在不损失过多性能的前提下可以使用现有命令来实现的功能,Redis就不会单独提供命令来实现。这一原则使得Redis在拥有强大功能的同时保持着相对精简的命令。

有序集合常见的使用场景是大数据排序,如游戏的玩家排行榜,所以很少会需要获得键中的全部数据。同样Redis认为开发者在做完交集、并集运算后不需要直接获得全部结果,而是会希望将结果存入新的键中以便后续处理。这解释了为什么有序集合只有ZINTERSTORE和ZUNIONSTORE命令而没有ZINTER和ZUNION命令。

当然实际使用中确实会遇到像小白那样需要直接获得集合运算结果的情况,除了等待Redis加入相关命令,我们还可以使用MULTI, ZINTERSTORE, ZRANGE, DEL和EXEC这5个命令自己实现ZINTER:

MULTI
ZINTERSTORE tempKey ...
ZRANGE tempKey ...
DEL tempKey
EXEC

4.3.2 SORT命令

除了使用有序集合外,我们还可以借助Redis提供的SORT命令来解决小白的问题。SORT命令可以对列表类型、集合类型和有序集合类型键进行排序,并且可以完成与关系数据库中的连接查询相类似的任务。

小白的博客中标有“ruby”标签的文章的ID分别是:“2”,“6”,“12”,“26”。由于在集合类型中所有元素是无序的,所以使用SMEMBERS命令并不能获得有序的结果1。为了能够让博客的标签页面下的文章也能按照发布的时间顺序排列(如果不考虑发布后再修改文章发布时间,就是按照文章ID的顺序排列),可以借助SORT命令实现,方法如下所示:

redis> SORT tag:ruby:posts
1) "2"
2) "6"
3) "12"
4) "26"

是不是十分简单?除了集合类型,SORT 命令还可以对列表类型和有序集合类型进行排序:

redis> LPUSH mylist 4 2 6 1 3 7
(integer) 6
redis> SORT mylist
1) "1"
2) "2"
3) "3"
4) "4"
5) "6"
6) "7"

在对有序集合类型排序时会忽略元素的分数,只针对元素自身的值进行排序。例如:

redis> ZADD myzset 50 2 40 3 20 1 60 5
(integer) 4
redis> SORT myzset
1) "1"
2) "2"
3) "3"
4) "5"

除了可以排列数字外,SORT命令还可以通过ALPHA参数实现按照字典顺序排列非数字元素,就像这样:

redis> LPUSH mylistalpha a c e d B C A
(integer) 7
redis> SORT mylistalpha
(error) ERR One or more scores can't be converted into double
redis> SORT mylistalpha ALPHA
1) "A"
2) "B"
3) "C"
4) "a"
5) "c"
6) "d"
7) "e"

从这段示例中可以看到如果没有加ALPHA参数的话,SORT命令会尝试将所有元素转换成双精度浮点数来比较,如果无法转换则会提示错误。

回到小白的问题,SORT 命令默认是按照从小到大的顺序排列,而一般博客中显示文章的顺序都是按照时间倒序的,即最新的文章显示在最前面。SORT命令的DESC参数可以实现将元素按照从大到小的顺序排列:

redis> SORT tag:ruby:posts DESC
1) "26"
2) "12"
3) "6"
4) "2"

那么如果文章数量过多需要分页显示呢?SORT命令还支持LIMIT参数来返回指定范围的结果。用法和SQL语句一样,LIMIT offset count,表示跳过前offset个元素并获取之后的count个元素。

SORT命令的参数可以组合使用,像这样:

redis> SORT tag:ruby:posts DESC LIMIT 1 2
1) "12"
2) "6"

4.3.3 BY参数

很多情况下列表(或集合、有序集合)中存储的元素值代表的是对象的ID(如标签集合中存储的是文章对象的ID),单纯对这些ID自身排序有时意义并不大。更多的时候我们希望根据ID对应的对象的某个属性进行排序。回想3.6节,我们通过使用有序集合键来存储文章ID列表,使得小白的博客能够支持修改文章时间,所以文章ID的顺序和文章的发布时间的顺序并不完全一致,因此4.3.2节介绍的对文章ID本身排序就变得没有意义了。小白的博客是使用散列类型键存储文章对象的,其中time字段存储的就是文章的发布时间。现在我们知道ID为“2”,“6”,“12”和“26”的四篇文章的time字段的值分别为“1352619200”,“1352619600”,“1352620100”和“1352620000”(Unix时间)。如果要按照文章的发布时间递减排列结果应为“12”,“26”,“6”,“2”。为了获得这样的结果,需要使用SORT命令的另一个强大的参数——BY。

BY参数的语法为“BY参考键”。其中参考键可以是字符串类型键或者是散列类型键的某个字段(表示为键名->字段名)。如果提供了BY参数,SORT命令将不再依据元素自身的值进行排序,而是对每个元素使用元素的值替换参考键中的第一个“*”并获取其值,然后依据该值对元素排序。就像这样:

redis> SORT tag:ruby:posts BY post:*->time DESC
1) "12"
2) "26"
3) "6"
4) "2"

在上例中SORT命令会读取post:2、post:6、post:12、post:26几个散列键中的time字段的值并以此决定tag:ruby:posts键中各个文章ID的顺序。

除了散列类型之外,参考键还可以是字符串类型,比如:

redis> LPUSH sortbylist 2 1 3
(integer) 3
redis> SET itemscore:1 50
OK
redis> SET itemscore:2 100
OK
redis> SET itemscore:3 -10
OK
redis> SORT sortbylist BY itemscore:* DESC
1) "2"
2) "1"
3) "3"

当参考键名不包含“*”时(即常量键名,与元素值无关),SORT 命令将不会执行排序操作,因为Redis认为这种情况是没有意义的(因为所有要比较的值都一样)。例如:

redis> SORT sortbylist BY anytext
1) "3"
2) "1"
3) "2"

例子中anytext是常量键名(甚至anytext键可以不存在),此时SORT的结果与LRANGE的结果相同,没有执行排序操作。在不需要排序但需要借助SORT命令获得与元素相关联的数据时(见4.3.4节),常量键名是很有用的。

如果几个元素的参考键值相同,则SORT命令会再比较元素本身的值来决定元素的顺序。像这样:

redis> LPUSH sortbylist 4
(integer) 4
redis> SET itemscore:4 50
OK
redis> SORT sortbylist BY itemscore:* DESC
1) "2"
2) "4"
3) "1"
4) "3"

示例中元素“4”的参考键itemscore:4的值和元素“1”的参考键itemscore:1的值都是50,所以SORT命令会再比较“4”和“1”元素本身的大小来决定两者的顺序。

当某个元素的参考键不存在时,会默认参考键的值为0:

redis> LPUSH sortbylist 5
(integer) 5
redis> SORT sortbylist BY itemscore:* DESC
1) "2"
2) "4"
3) "1"
4) "5"
5) "3"

上例中“5”排在了“3”的前面,是因为“5”的参考键不存在,所以默认为0,而“3”的参考键值为−10。

补充知识

参考键虽然支持散列类型,但是“*”只能在“ ->”符号前面(即键名部分)才有用,在“ ->”后(即字段名部分)会被当成字段名本身而不会作为占位符被元素的值替换,即常量键名。但是实际运行时会发现一个有趣的结果:

redis> SORT sortbylist BY somekey->somefield:*
   1) "1"
   2) "2"
   3) "3"
   4) "4"
   5) "5"

上面提到了当参考键名是常量键名时SORT命令将不会执行排序操作,然而上例中确进行了排序,而且只是对元素本身进行排序。这是因为Redis判断参考键名是不是常量键名的方式是判断参考键名中是否包含“”,而 somekey->somefield: 中包含“”所以不是常量键名。所以在排序的时候Redis对每个元素都会读取键somekey中的somefield:字段(“*”不会被替换),无论能否获得其值,每个元素的参考键值是相同的,所以Redis会按照元素本身的大小排列。

4.3.4 GET参数

现在小白的博客已经可以按照文章的发布顺序获得一个标签下的文章 ID 列表了,接下来要做的事就是对每个ID都使用HGET命令获取文章的标题以显示在博客列表页中。有没有觉得很麻烦?不论你的答案如何,都有一种更简单的方式来完成这个操作,那就是借助SORT命令的GET参数。

GET参数不影响排序,它的作用是使SORT命令的返回结果不再是元素自身的值,而是GET参数中指定的键值。GET参数的规则和BY参数一样,GET参数也支持字符串类型和散列类型的键,并使用“*”作为占位符。要实现在排序后直接返回ID对应的文章标题,可以这样写:

redis> SORT tag:ruby:posts BY post:->time DESC GET post:->title
1) "Windows 8 app designs"
2) "RethinkDB - An open-source distributed database built with love"
3) "Uses for cURL"
4) "The Nature of Ruby"

在一个SORT命令中可以有多个GET参数(而BY参数只能有一个),所以还可以这样用:

redis> SORT tag:ruby:posts BY post:->time DESC GET post:->title GET post:*->time
1) "Windows 8 app designs"
2) "1352620100"
3) "RethinkDB - An open-source distributed database built with love"
4) "1352620000"
5) "Uses for cURL"
6) "1352619600"
7) "The Nature of Ruby"
8) "1352619200"

可见有N个GET参数,每个元素返回的结果就有N行。这时有个问题:如果还需要返回文章ID该怎么办?答案是使用GET #。就像下面这样:

redis> SORT tag:ruby:posts BY post:->time DESC GET post:->title GET post:*->time GET #
 1) "Windows 8 app designs"
 2) "1352620100"
 3) "12"
 4) "RethinkDB - An open-source distributed database built with love"
 5) "1352620000"
 6) "26"
 7) "Uses for cURL"
 8) "1352619600"
 9) "6"
10) "The Nature of Ruby"
11) "1352619200"
12) "2"

也就是说,GET #会返回元素本身的值。

4.3.5 STORE参数

默认情况下SORT会直接返回排序结果,如果希望保存排序结果,可以使用STORE参数。如希望把结果保存到sort.result键中:

redis> SORT tag:ruby:posts BY post:->time DESC GET post:->title GET post:*->time
GET # STORE sort.result
(integer) 12
redis> LRANGE sort.result 0 -1
 1) "Windows 8 app designs"
 2) "1352620100"
 3) "12"
 4) "RethinkDB - An open-source distributed database built with love"
 5) "1352620000"
 6) "26"
 7) "Uses for cURL"
 8) "1352619600"
 9) "6"
10) "The Nature of Ruby"
11) "1352619200"
12) "2"

保存后的键的类型为列表类型,如果键已经存在则会覆盖它。加上STORE参数后SORT命令的返回值为结果的个数。

STORE参数常用来结合EXPIRE命令缓存排序结果,如下面的伪代码:

# 判断是否存在之前排序结果的缓存
$isCacheExists = EXISTS cache.sort
if $isCacheExists is 1
  # 如果存在则直接返回
  return LRANGE cache.sort, 0, -1
else
  # 如果不存在,则使用SORT命令排序并将结果存入cache.sort键中作为缓存
  $sortResult = SORT some.list STORE cache.sort
  # 设置缓存的生存时间为10分钟
  EXPIRE cache.sort, 600
  # 返回排序结果
  return $sortResult

4.3.6 性能优化

SORT是Redis中最强大最复杂的命令之一,如果使用不好很容易成为性能瓶颈。SORT命令的时间复杂度是O(n+mlogm),其中n表示要排序的列表(集合或有序集合)中的元素个数,m表示要返回的元素个数。当n较大的时候SORT命令的性能相对较低,并且Redis在排序前会建立一个长度为n2的容器来存储待排序的元素,虽然是一个临时的过程,但如果同时进行较多的大数据量排序操作则会严重影响性能。

所以开发中使用SORT命令时需要注意以下几点。

(1)尽可能减少待排序键中元素的数量(使n尽可能小)。

(2)使用LIMIT参数只获取需要的数据(使m尽可能小)。

(3)如果要排序的数据数量较大,尽可能使用STORE参数将结果缓存。

时间: 2024-08-03 16:08:40

《Redis入门指南》一4.3 排序的相关文章

《Redis入门指南》一第4章  进阶

第4章 进阶 Redis入门指南 没过几天,小白就完成了博客的开发并将其部署上线.之后的一段时间,小白又使用Redis开发了几个程序,用得还算顺手,便没有继续向宋老师请教Redis的更多知识.直到一个月后的一天,宋老师偶然访问了小白的博客-- 本章将会带领读者继续探索Redis,了解Redis的事务.排序与管道等功能,并且还会详细地介绍如何优化Redis的存储空间.

《Redis入门指南》一导读

前 言 Redis入门指南Redis如今已经成为Web开发社区中最火热的内存数据库之一,而它的诞生距现在不过才4年.随着Web 2.0的蓬勃发展,网站数据快速增长,对高性能读写的需求也越来越多,再加上半结构化的数据比重逐渐变大,人们对早已被铺天盖地地运用着的关系数据库能否适应现今的存储需求产生了疑问.而Redis的迅猛发展,为这个领域注入了全新的思维. Redis凭借其全面的功能得到越来越多的公司的青睐,从初创企业到新浪微博这样拥有着几百台Redis服务器的大公司,都能看到Redis的身影.Re

《Redis入门指南》一5.2 Ruby与Redis

5.2 Ruby与Redis Redis入门指南Redis官方推荐的Ruby客户端是redis-rb1,也是各种语言的Redis客户端中最为稳定的一个.其主要代码贡献者就是Redis的开发者之一Pieter Noordhuis. 5.2.1 安装 使用gem install redis安装最新版本的redis-rb,目前的最新版本是3.0.2. 5.2.2 使用方法 创建到Redis的连接很简单: require 'redis' redis = Redis.new 该行代码会默认Redis的地址

《Redis入门指南》一4.6 节省空间

4.6 节省空间 Redis入门指南Jim Gray1曾经说过:"内存是新的硬盘,硬盘是新的磁带."内存的容量越来越大,价格也越来越便宜.2012年年底,亚马逊宣布即将发布一个拥有240GB内存的EC2实例,如果放到若干年前来看,这个容量就算是对于硬盘来说也是很大的了.即便如此,相比于硬盘而言,内存在今天仍然显得比较昂贵.而 Redis 是一个基于内存的数据库,所有的数据都存储在内存中,所以如何优化存储,减少内存空间占用对成本控制来说是一个非常重要的话题. 4.6.1 精简键名和键值

《Redis入门指南》一5.1 PHP与Redis

5.1 PHP与Redis Redis入门指南 Redis官方推荐的PHP客户端是Predis1和phpredis2.前者是完全使用PHP代码实现的原生客户端,而后者则是使用C语言编写的PHP扩展.在功能上两者区别并不大,就性能而言后者会更胜一筹.考虑到很多主机并未提供安装PHP扩展的权限,本节会以Predis为示例介绍如何在PHP中使用Redis. 虽然Predis的性能逊于phpredis,但是除非执行大量Redis命令,否则很难区分二者的性能.而且实际应用中执行Redis命令的开销更多在网

《Redis入门指南》一5.3 Python与Redis

5.3 Python与Redis Redis入门指南 Redis官方推荐的Python客户端是redis-py1. 5.3.1 安装 推荐使用pip install redis安装最新版本的redis-py,也可以使用easy_install:easy_install redis. 5.3.2 使用方法 首先需要引入redis-py: import redis 下面的代码将创建一个默认连接到地址127.0.0.1,端口6379的Redis连接: r = redis.StrictRedis() 也

《Redis入门指南》一4.1 事务

4.1 事务 Redis入门指南 傍晚时候,忙完了一天的教学工作,宋老师坐在办公室的电脑前开始为明天的课程做准备.尽管有着近5年的教学经验,可是宋老师依然习惯在备课时写一份简单的教案.正在网上查找资料时,在浏览器的历史记录里他突然看到了小白的博客.心想:不知道他的博客怎么样了? 于是宋老师点进了小白的博客,页面刚载入完他就被博客最下面的一行大得夸张的文字吸引了:"Powered by Redis".宋老师笑了笑,接着就看到了小白博客中最新的一篇文章: 标题: 使用Redis来存储微博中

《Redis入门指南》一4.4 消息通知

4.4 消息通知 Redis入门指南 凭着小白的用心经营,博客的访问量逐渐增多,甚至有了小白自己的粉丝.这不,小白刚收到一封来自粉丝的邮件,在邮件中那个粉丝强烈建议小白给博客加入邮件订阅功能,这样当小白发布新文章后订阅小白博客的用户就可以收到通知邮件了.在信的末尾,那个粉丝还着重强调了一下:"这个功能对不习惯使用RSS的用户很重要,希望能够加上!" 看过信后,小白心想:"是个好建议!不过话说回来,似乎他还没发现其实我的博客连RSS功能都没有." 邮件订阅功能太好实现

《Redis入门指南》一5.4 Node.js与Redis

5.4 Node.js与Redis Redis入门指南 Redis官方推荐的Node.js客户端是node_redis1. 5.4.1 安装 使用npm install redis命令安装最新版本的node_redis,目前版本是0.8.2. 5.4.2 使用方法 首先加载node_redis模块: var redis = require('redis'); 下面的代码将创建一个默认连接到地址127.0.0.1,端口6379的Redis连接: var client = redis.createC