UDN-企业互联网技术人气社区

板块导航

浏览  : 1168
回复  : 0

[讨论交流] 你不可不知的Redis常用命令(下)

[复制链接]
白青青的头像 楼主
  3.5 有序集合

  和散列存储着键与值之间的映射类似,有序集合也存储着成员与分值之间的映射,并且提供了分值处理命令,以及根据分值大小有序地获取(fetch)或扫描(scan)成员和分值的命令。本书曾在第1章使用有序集合实现过基于发表时间排序的文章列表和基于投票数量排序的文章列表,还在第2章使用有序集合存储过cookie的过期时间。

  本节将对操作有序集合的命令进行介绍,其中包括向有序集合添加新元素的命令、更新已有元素的命令,以及对有序集合进行交集运算和并集运算的命令。阅读本节可以加深读者对有序集合的认识,从而帮助读者更好地理解本书在第1章、第5章、第6章和第7章展示的有序集合示例。

  表3-9展示了一部分常用的有序集合命令。

  表3-9 一些常用的有序集合命令
命令
用例和描述
ZADD
ZADD key-name score member [score member ...]—将带有给定分值的成员添加到有序集合里面
ZREM
ZREM key-name member [member ...]—从有序集合里面移除给定的成员,并返回被移除成员的数量
ZCARD
ZCARD key-name—返回有序集合包含的成员数量
ZINCRBY
ZINCRBY key-name increment member—将member成员的分值加上increment
ZCOUNT
ZCOUNT key-name min max—返回分值介于min和max之间的成员数量
ZRANK
ZRANK key-name member—返回成员member在有序集合中的排名
ZSCORE
ZSCORE key-name member—返回成员member的分值
ZRANGE
ZRANGE key-name start stop [WITHSCORES]—返回有序集合中排名介于start和stop之间的成员,如果给定了可选的WITHSCORES选项,那么命令会将成员的分值也一并返回

  在上面列出的命令当中,有一部分命令已经在第1章和第2章使用过了,所以读者应该不会对它们感到陌生,代码清单3-9回顾了这些命令的用法。

  代码清单3-9 这个交互示例展示了Redis中的一些常用的有序集合命令
2.png

2.png

  因为ZADD、ZREM、ZINCRBY、ZSCORE和ZRANGE都已经在第1章和第2章介绍过了,所以读者应该不会对它们感到陌生。ZCOUNT命令和其他命令不太相同,它主要用于计算分值在给定范围内的成员数量。

  表3-10展示了另外一些非常有用的有序集合命令。

  表3-10 有序集合的范围型数据获取命令和范围型数据删除命令,以及并集命令和交集命令
1.jpg

  在表3-10展示的命令里面,有几个是之前没介绍过的新命令。除了使用逆序来处理有序集合之外,ZREV*命令的工作方式和相对应的非逆序命令的工作方式完全一样(逆序就是指元素按照分值从大到小地排列)。代码清单3-10展示了ZINTERSTORE和ZUNIONSTORE的用法。

  代码清单3-10 这个交互示例展示了ZINTERSTORE命令和ZUNIONSTORE命令的用法
2.png

2.png

  有序集合的并集运算和交集运算在刚开始接触时可能会比较难懂,所以本节将使用图片来展示交集运算和并集运算的执行过程。图3-1展示了对两个输入有序集合执行交集运算并得到输出有序集合的过程,这次交集运算使用的是默认的聚合函数sum,所以输出有序集合成员的分值都是通过加法计算得出的。
2.png

  图3-1 执行conn.zinterstore('zset-i', ['zset-1', 'zset-2'])

  将使得同时存在于zset-1和zset-2里面的元素被添加到zset-i里面

  并集运算和交集运算不同,只要某个成员存在于至少一个输入有序集合里面,那么这个成员就会被包含在输出有序集合里面。图3-2展示了使用聚合函数min执行并集运算的过程,min函数在多个输入有序集合都包含同一个成员的情况下,会将最小的那个分值设置为这个成员在输出有序集合的分值。
2.png

  图3-2 执行conn.zunionstore('zset-u', ['zset-1', 'zset-2'], aggregate='min')

  会将存在于zset-1或者zset-2里面的元素通过min函数组合到zset-u里面

  在第1章中,我们就基于“集合可以作为ZUNIONSTORE操作和ZINTERSTORE操作的输入”这个事实,在没有使用额外的有序集合来存储群组文章的评分和发布时间的情况下,实现了群组文章的添加和删除操作。图3-3展示了如何使用ZUNIONSTORE命令来将两个有序集合和一个集合组合成一个有序集合。
2.png

  图3-3 执行conn.zunionstore('zset-u2', ['zset-1', 'zset-2', 'set-1'])

  将使得所有存在于zset-1、zset-2或者set-1里面的元素都被添加到zset-u2里面

  第7章将使用ZINTERSTORE和ZUNIONSTORE来构建几个不同类型的搜索系统,并说明如何通过可选的WEIGHTS参数来以几种不同的方式组合有序集合的分值,从而使得集合和有序集合可以用于解决更多问题。

  读者在开发应用的过程中,也许曾经听说过发布与订阅(publish/subscribe)模式,又称pub/sub模式,Redis也实现了这种模式,接下来的一节将对其进行介绍。

  3.6 发布与订阅

  如果你因为想不起来本书在前面的哪个章节里面介绍过发布与订阅而困惑,那么大可不必—这是本书目前为止第一次介绍发布与订阅。一般来说,发布与订阅(又称pub/sub)的特点是订阅者(listener)负责订阅频道(channel),发送者(publisher)负责向频道发送二进制字符串消息(binary string message)。每当有消息被发送至给定频道时,频道的所有订阅者都会收到消息。我们也可以把频道看作是电台,其中订阅者可以同时收听多个电台,而发送者则可以在任何电台发送消息。

  本节将对发布与订阅的相关操作进行介绍,阅读这一节可以让读者学会怎样使用发布与订阅的相关命令,并了解到为什么本书在之后的章节里面会使用其他相似的解决方案来代替Redis提供的发布与订阅。

  表3-11展示了Redis提供的5个发布与订阅命令。

  表3-11 Redis提供的发布与订阅命令
命令
用例和描述
SUBSCRIBE
SUBSCRIBE channel [channel ...]—订阅给定的一个或多个频道
UNSUBSCRIBE
UNSUBSCRIBE [channel [channel ...]]—退订给定的一个或多个频道,如果执行时没有给定任何频道,那么退订所有频道
PUBLISH
PUBLISH channel message—向给定频道发送消息
PSUBSCRIBE
PSUBSCRIBE pattern [pattern ...]—订阅与给定模式相匹配的所有频道
PUNSUBSCRIBE
PUNSUBSCRIBE [pattern [pattern ...]]—退订给定的模式,如果执行时没有给定任何模式,那么退订所有模式

  考虑到PUBLISH命令和SUBSCRIBE命令在Python客户端的实现方式,一个比较简单的演示发布与订阅的方法,就是像代码清单3-11那样使用辅助线程(helper thread)来执行PUBLISH命令。

  代码清单3-11 这个交互示例展示了如何使用Redis中的PUBLISH命令以及SUBSCRIBE命令
2.png

2.png

  虽然Redis的发布与订阅模式非常有用,但本书只在这一节和8.5节中使用了这个模式,这样做的原因有以下两个。

  第一个原因和Redis系统的稳定性有关。对于旧版Redis来说,如果一个客户端订阅了某个或某些频道,但它读取消息的速度却不够快的话,那么不断积压的消息就会使得Redis输出缓冲区的体积变得越来越大,这可能会导致Redis的速度变慢,甚至直接崩溃。也可能会导致Redis被操作系统强制杀死,甚至导致操作系统本身不可用。新版的Redis不会出现这种问题,因为它会自动断开不符合client-output-buffer-limit pubsub配置选项要求的订阅客户端(本书第8章将对这个选项做更详细的介绍)。

  第二个原因和数据传输的可靠性有关。任何网络系统在执行操作时都可能会遇上断线情况,而断线产生的连接错误通常会使得网络连接两端中的其中一端进行重新连接。本书使用的Python语言的Redis客户端会在连接失效时自动进行重新连接,也会自动处理连接池(connection pool,具体信息将在第4章介绍),诸如此类。但是,如果客户端在执行订阅操作的过程中断线,那么客户端将丢失在断线期间发送的所有消息,因此依靠频道来接收消息的用户可能会对Redis提供的PUBLISH命令和SUBSCRIBE命令的语义感到失望。

  基于以上两个原因,本书在第6章编写了两个不同的方法来实现可靠的消息传递操作,这两个方法除了可以处理网络断线之外,还可以防止Redis因为消息积压而耗费过多内存(这个方法即使对于旧版Redis也是有效的)。

  如果你喜欢简单易用的PUBLISH命令和SUBSCRIBE命令,并且能够承担可能会丢失一小部分数据的风险,那么你也可以继续使用Redis提供的发布与订阅特性,而不是8.5节中提供的实现,只要记得先把client-output-buffer-limit pubsub选项设置好就行了。

  到目前为止,本书介绍的大多数命令都是与特定数据类型相关的。接下来的一节要介绍的命令你可能也会用到,但它们既不属于Redis提供的5种数据结构,也不属于发布与订阅特性。

  3.7 其他命令

  到目前为止,本章介绍了Redis提供的5种结构以及Redis的发布与订阅模式。本节将要介绍的命令则可以用于处理多种类型的数据:首先要介绍的是可以同时处理字符串、集合、列表和散列的SORT命令;之后要介绍是用于实现基本事务特性的MULTI命令和EXEC命令,这两个命令可以让用户将多个命令当作一个命令来执行;最后要介绍的是几个不同的自动过期命令,它们可以自动删除无用数据。

  阅读本节有助于读者更好地理解如何同时组合和操作多种数据类型。

  3.7.1 排序

  Redis的排序操作和其他编程语言的排序操作一样,都可以根据某种比较规则对一系列元素进行有序的排列。负责执行排序操作的SORT命令可以根据字符串、列表、集合、有序集合、散列这5种键里面存储着的数据,对列表、集合以及有序集合进行排序。如果读者之前曾经使用过关系数据库的话,那么可以将SORT命令看作是SQL语言里的order by子句。表3-12展示了SORT命令的定义。

  表3-12 SORT命令的定义
1.jpg

  使用SORT命令提供的选项可以实现以下功能:根据降序而不是默认的升序来排序元素;将元素看作是数字来进行排序,或者将元素看作是二进制字符串来进行排序(比如排序字符串'110'和'12'的结果就跟排序数字110和12的结果不一样);使用被排序元素之外的其他值作为权重来进行排序,甚至还可以从输入的列表、集合、有序集合以外的其他地方进行取值。

  代码清单3-12展示了一些SORT命令的使用示例。其中,最开头的几行代码设置了一些初始数据,然后对这些数据进行了数值排序和字符串排序,最后的代码演示了如何通过SORT命令的特殊语法来将散列存储的数据作为权重进行排序,以及怎样获取并返回散列存储的数据。

  代码清单3-12 这个交互示例展示了SORT命令的一些简单的用法
2.png

  SORT命令不仅可以对列表进行排序,还可以对集合进行排序,然后返回一个列表形式的排序结果。代码清单3-12除了展示如何使用alpha关键字参数对元素进行字符串排序之外,还展示了如何基于外部数据对元素进行排序,以及如何获取并返回外部数据。第7章将介绍如何组合使用集合操作和SORT命令:当集合结构计算交集、并集和差集的能力,与SORT命令获取散列存储的外部数据的能力相结合时,SORT命令将变得非常强大。

  尽管SORT是Redis中唯一一个可以同时处理3种不同类型的数据的命令,但基本的Redis事务同样可以让我们在一连串不间断执行的命令里面操作多种不同类型的数据。

  3.7.2 基本的Redis事务

  有时候为了同时处理多个结构,我们需要向Redis发送多个命令。尽管Redis有几个可以在两个键之间复制或者移动元素的命令,但却没有那种可以在两个不同类型之间移动元素的命令(虽然可以使用ZUNIONSTORE命令将元素从一个集合复制到一个有序集合)。为了对相同或者不同类型的多个键执行操作,Redis有5个命令可以让用户在不被打断(interruption)的情况下对多个键执行操作,它们分别是WATCH、MULTI、EXEC、UNWATCH和DISCARD。

  这一节只介绍最基本的Redis事务用法,即使用MULTI命令和EXEC命令。如果读者想看看使用WATCH、MULTI、EXEC和UNWATCH等多个命令的事务是什么样子的,可以阅读4.4节,其中解释了为什么需要在使用MULTI和EXEC的同时使用WATCH和UNWATCH。

  什么是Redis的基本事务

  Redis的基本事务(basic transaction)需要用到MULTI命令和EXEC命令,这种事务可以让一个客户端在不被其他客户端打断的情况下执行多个命令。和关系数据库那种可以在执行的过程中进行回滚(rollback)的事务不同,在Redis里面,被MULTI命令和EXEC命令包围的所有命令会一个接一个地执行,直到所有命令都执行完毕为止。当一个事务执行完毕之后,Redis才会处理其他客户端的命令。

  要在Redis里面执行事务,我们首先需要执行MULTI命令,然后输入那些我们想要在事务里面执行的命令,最后再执行EXEC命令。当Redis从一个客户端那里接收到MULTI命令时,Redis会将这个客户端之后发送的所有命令都放入到一个队列里面,直到这个客户端发送EXEC命令为止,然后Redis就会在不被打断的情况下,一个接一个地执行存储在队列里面的命令。从语义上来说,Redis事务在Python客户端上面是由流水线(pipeline)实现的:对连接对象调用piepline()方法将创建一个事务,在一切正常的情况下,客户端会自动地使用MULTI和EXEC包裹起用户输入的多个命令。此外,为了减少Redis与客户端之间的通信往返次数,提升执行多个命令时的性能,Python的Redis客户端会存储起事务包含的多个命令,然后在事务执行时一次性地将所有命令都发送给Redis。

  跟介绍PUBLISH命令和SUBSCRIBE命令时的情况一样,要展示事务执行结果,最简单的方法就是将事务放到线程里面执行。代码清单3-13展示了在没有使用事务的情况下,执行并行(parallel)自增操作的结果。

  代码清单3-13 在并行执行命令时,缺少事务可能会引发的问题
2.png

  因为没有使用事务,所以3个线程都可以在执行自减操作之前,对notrans:计数器执行自增操作。虽然代码清单里面通过休眠100毫秒的方式来放大了潜在的问题,但如果我们确实需要在不受其他命令干扰的情况下,对计数器执行自增操作和自减操作,那么我们就不得不解决这个潜在的问题。代码清单3-14展示了如何使用事务来执行相同的操作。

  代码清单3-14 使用事务来处理命令的并行执行问题
2.png

2.png

  可以看到,尽管自增操作和自减操作之间有一段延迟时间,但通过使用事务,各个线程都可以在不被其他线程打断的情况下,执行各自队列里面的命令。记住,Redis要在接收到EXEC命令之后,才会执行那些位于MULTI和EXEC之间的入队命令。

  使用事务既有利也有弊,本书的4.4节将对这个问题进行讨论。

  练习:移除竞争条件 正如前面的代码清单3-13所示,MULTI和EXEC事务的一个主要作用是移除竞争条件。第1章展示的articlevote()函数包含一个竞争条件以及一个因为竞争条件而出现的bug。函数的竞争条件可能会造成内存泄漏,而函数的bug则可能会导致不正确的投票结果出现。尽管article vote()函数的竞争条件和bug出现的机会都非常少,但为了防范于未然,你能想个办法修复它们么?提示:如果你觉得很难理解竞争条件为什么会导致内存泄漏,那么可以在分析第1章的post_article()函数的同时,阅读一下6.2.5节。

  练习:提高性能 在Redis里面使用流水线的另一个目的是提高性能(详细的信息会在之后的4.4节至4.6节中介绍)。在执行一连串命令时,减少Redis与客户端之间的通信往返次数可以大幅降低客户端等待回复所需的时间。第1章的get_articles()函数在获取整个页面的文章时,需要在Redis与客户端之间进行26次通信往返,这种做法简直低效得令人发指,你能否想个办法将get_articles()函数的往返次数从26次降低为2次呢?

  在使用Redis存储数据的时候,有些数据仅在一段很短的时间内有用,虽然我们可以在数据的有效期过了之后手动删除无用的数据,但更好的办法是使用Redis提供的键过期操作来自动删除无用数据。

  3.7.3 键的过期时间

  在使用Redis存储数据的时候,有些数据可能在某个时间点之后就不再有用了,用户可以使用DEL命令显式地删除这些无用数据,也可以通过Redis的过期时间(expiration)特性来让一个键在给定的时限(timeout)之后自动被删除。当我们说一个键“带有生存时间(time to live)”或者一个键“会在特定时间之后过期(expire)”时,我们指的是Redis会在这个键的过期时间到达时自动删除该键。

  虽然过期时间特性对于清理缓存数据非常有用,不过如果读者翻一下本书的其他章节,就会发现除了6.2节、7.1节和7.2节之外,本书使用过期时间特性的情况并不多,这主要和本书使用的结构类型有关。在本书常用的命令当中,只有少数几个命令可以原子地为键设置过期时间,并且对于列表、集合、散列和有序集合这样的容器(container)来说,键过期命令只能为整个键设置过期时间,而没办法为键里面的单个元素设置过期时间(为了解决这个问题,本书在好几个地方都使用了存储时间戳的有序集合来实现针对单个元素的过期操作)。

  本节将对那些可以在给定时限之后或者给定时间之后自动删除过期键的Redis命令进行介绍,阅读本节可以让读者学习到使用过期操作来自动删除过期数据并降低Redis内存占用的方法。

  表3-13列出了Redis提供的用于为键设置过期时间的命令,以及查看键的过期时间的命令。

  表3-13 用于处理过期时间的Redis命令
命令
示例和描述
PERSIST
PERSIST key-name—移除键的过期时间
TTL
TTL key-name—查看给定键距离过期还有多少秒
EXPIRE
EXPIRE key-name seconds—让给定键在指定的秒数之后过期
EXPIREAT
EXPIREAT key-name timestamp—将给定键的过期时间设置为给定的UNIX时间戳
PTTL
PTTL key-name—查看给定键距离过期时间还有多少毫秒,这个命令在Redis 2.6或以上版本可用
PEXPIRE
PEXPIRE key-name milliseconds—让键给定键在指定的毫秒数之后过期,这个命令在Redis 2.6或以上版本可用
PEXPIREAT
PEXPIREAT key-name timestamp-milliseconds—将一个毫秒级精度的UNIX时间戳设置为给定键的过期时间,这个命令在Redis 2.6或以上版本可用

  代码清单3-15展示了几个对键执行过期时间操作的例子。

  代码清单3-15 展示Redis中过期时间相关的命令的使用方法
2.png

2.png

 练习:使用EXPIRE命令代替时间戳有序集合 2.1节、2.2节和2.5节中使用了一个根据时间戳进行排序、用于清除会话ID的有序集合,通过这个有序集合,程序可以在清理会话的时候,对用户浏览过的商品以及用户购物车里面的商品进行分析。但是,如果我们决定不对商品进行分析的话,那么就可以使用Redis提供的过期时间操作来自动清理过期的会话ID,而无须使用清理函数。那么,你能否想办法修改在第2章定义的update_token()函数和add_to_cart()函数,让它们使用过期时间操作来删除会话ID,从而代替目前使用有序集合来记录并清除会话ID的做法呢?

  扩展阅读:

  你不可不知的Redis常用命令(上)

相关帖子

您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

关于我们
联系我们
  • 电话:010-86393388
  • 邮件:udn@yonyou.com
  • 地址:北京市海淀区北清路68号
移动客户端下载
关注我们
  • 微信公众号:yonyouudn
  • 扫描右侧二维码关注我们
  • 专注企业互联网的技术社区
版权所有:用友网络科技股份有限公司82041 京ICP备05007539号-11 京公网网备安1101080209224 Powered by Discuz!
快速回复 返回列表 返回顶部