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

本文涉及的产品
云数据库 Redis 版,社区版 2GB
推荐场景:
搭建游戏排行榜
简介:

本节书摘来异步社区《Redis入门指南》一书中的第4章,第4.4节,作者: 李子骅 责编: 杨海玲,更多章节内容可以访问云栖社区“异步社区”公众号查看。

4.4 消息通知

Redis入门指南
凭着小白的用心经营,博客的访问量逐渐增多,甚至有了小白自己的粉丝。这不,小白刚收到一封来自粉丝的邮件,在邮件中那个粉丝强烈建议小白给博客加入邮件订阅功能,这样当小白发布新文章后订阅小白博客的用户就可以收到通知邮件了。在信的末尾,那个粉丝还着重强调了一下:“这个功能对不习惯使用RSS的用户很重要,希望能够加上!”

看过信后,小白心想:“是个好建议!不过话说回来,似乎他还没发现其实我的博客连RSS功能都没有。”

邮件订阅功能太好实现了,无非是在博客首页放一个文本框供访客输入自己的邮箱地址,提交后博客会将该地址存入Redis的一个集合类型键中(使用集合类型是为了保证同一邮箱地址不会存储多个)。每当发布新文章时,就向收集到的邮箱地址发送通知邮件。

想的简单,可是做出来后小白却发现了一个问题:输入邮箱地址提交后,页面需要很久时间才能载入完。

原来小白为了确保用户没有输入他人的邮箱,在提交之后程序会向用户输入的邮箱发送一封包含确认链接的邮件,只有用户单击这个链接后对应的邮箱地址才会被程序记录。可是由于发送邮件需要连接到一个远程的邮件发送服务器,网络好的情况下也得花上2秒左右的时间,赶上网络不好10秒都未必能发完。所以每次用户提交邮箱后页面都要等待程序发送完邮件才能加载出来,而加载出来的页面上显示的内容只是提示用户查看自己的邮箱单击确认链接。“完全可以等页面加载出来后再发送邮件,这样用户就不需要等了。”小白喃喃道。

按照惯例,有问题问宋老师,小白给宋老师发了一封邮件,不久就收到了答复。

4.4.1 任务队列

小白的问题在网站开发中十分常见,当页面需要进行如发送邮件、复杂数据运算等耗时较长的操作时会阻塞页面的渲染。为了避免用户等待太久,应该使用独立的线程来完成这类操作。不过一些编程语言或框架不易实现多线程,这时很容易就会想到通过其他进程来实现。就小白的例子来说,设想有一个进程能够完成发邮件的功能,那么在页面中只需要想办法通知这个进程向指定的地址发送邮件就可以了。

通知的过程可以借助任务队列来实现。任务队列顾名思义,就是“传递任务的队列”。与任务队列进行交互的实体有两类,一类是生产者(producer),一类是消费者(consumer)。生产者会将需要处理的任务放入任务队列中,而消费者则不断地从任务队列中读入任务信息并执行。

对于发邮件这个操作来说页面程序就是生产者,而发邮件的进程就是消费者。当需要发送邮件时,页面程序会将收件地址、邮件主题和邮件正文组装成一个任务后存入任务队列中。同时发邮件的进程会不断检查任务队列,一旦发现有新的任务便会将其从队列中取出并执行。由此实现了进程间的通信。

使用任务队列有如下好处。

(1)松耦合。生产者和消费者无需知道彼此的实现细节,只需要约定好任务的描述格式。这使得生产者和消费者可以由不同的团队使用不同的编程语言编写。

(2)易于扩展消费者可以有多个,而且可以分布在不同的服务器中,如图4-1所示。借此可以轻易地降低单台服务器的负载。


4_1

4.4.2 使用Redis实现任务队列

说到队列很自然就能想到Redis的列表类型,3.4.2节介绍了使用LPUSH和RPOP命令实现队列的概念。如果要实现任务队列,只需要让生产者将任务使用LPUSH命令加入到某个键中,另一边让消费者不断地使用RPOP命令从该键中取出任务即可。

在小白的例子中,完成发邮件的任务需要知道收件地址、邮件主题和邮件正文。所以生产者需要将这三个信息组成对象并序列化成字符串,然后将其加入到任务队列中。而消费者则循环从队列中拉取任务,就像如下伪代码:

# 无限循环读取任务队列中的内容
loop
  $task = RPOR queue
  if $task
     # 如果任务队列中有任务则执行它
    execute($task)
  else
     # 如果没有则等待1秒以免过于频繁地请求数据
    wait 1 second

到此一个使用Redis实现的简单的任务队列就写好了。不过还有一点不完美的地方:当任务队列中没有任务时消费者每秒都会调用一次RPOP命令查看是否有新任务。如果可以实现一旦有新任务加入任务队列就通知消费者就好了。其实借助BRPOP命令就可以实现这样的需求。

BRPOP命令和RPOP命令相似,唯一的区别是当列表中没有元素时BRPOP命令会一直阻塞住连接,直到有新元素加入。如上段代码可改写为:

loop
  # 如果任务队列中没有新任务,BRPOP命令会一直阻塞,不会执行execute()。
  $task = BRPOP queue, 0
  # 返回值是一个数组(见下介绍),数组第二个元素是我们需要的任务。
execute($task[1])

BRPOP命令接收两个参数,第一个是键名,第二个是超时时间,单位是秒。当超过了此时间仍然没有获得新元素的话就会返回nil。上例中超时时间为“0”,表示不限制等待的时间,即如果没有新元素加入列表就会永远阻塞下去。

当获得一个元素后BRPOP命令返回两个值,分别是键名和元素值。为了测试BRPOP命令,我们可以打开两个redis-cli实例,在实例A中:

redis A> BRPOP queue 0

键入回车后实例1会处于阻塞状态,这时在实例B中向queue中加入一个元素:

redis B> LPUSH queue task
(integer) 1

在LPUSH命令执行后实例A马上就返回了结果:

1) "queue"
2) "task"

同时会发现queue中的元素已经被取走:

redis> LLEN queue
(integer) 0

除了BRPOP命令外,Redis还提供了BLPOP,和BRPOP的区别在与从队列取元素时BLPOP会从队列左边取。具体可以参照LPOP理解,这里不再赘述。

4.4.3 优先级队列

前面说到了小白博客需要在发布文章的时候向每个订阅者发送邮件,这一步骤同样可以使用任务队列实现。由于要执行的任务和发送确认邮件一样,所以二者可以共用一个消费者。然而设想这样的情况:假设订阅小白博客的用户有1000人,那么当发布一篇新文章后博客就会向任务队列中添加1000个发送通知邮件的任务。如果每发一封邮件需要10秒,全部完成这1000个任务就需要近3个小时。问题来了,假如这期间有新的用户想要订阅小白博客,当他提交完自己的邮箱并看到网页提示他查收确认邮件时,他并不知道向自己发送确认邮件的任务被加入到了已经有1000个任务的队列中。要收到确认邮件,他不得不等待近3个小时。多么糟糕的用户体验!而另一方面发布新文章后通知订阅用户的任务并不是很紧急,大多数用户并不要求有新文章后马上就能收到通知邮件,甚至延迟一天的时间在很多情况下也是可以接受的。

所以可以得出结论当发送确认邮件和发送通知邮件两种任务同时存在时,应该优先执行前者。为了实现这一目的,我们需要实现一个优先级队列。

BRPOP命令可以同时接收多个键,其完整的命令格式为BLPOPkey[key…] timeout,如BLPOP queue:1 queue:2 0。意义是同时检测多个键,如果所有键都没有元素则阻塞,如果其中有一个键有元素则会从该键中弹出元素。例如,打开两个redis-cli实例,在实例A中:

redis A> BLPOP queue:1 queue:2 queue:3 0

在实例B中:

redis B> LPUSH queue:2 task
(integer) 1

则实例A中会返回:

1) "queue:2"
2) "task"

如果多个键都有元素则按照从左到右的顺序取第一个键中的一个元素。我们先在queue:2和queue:3中各加入一个元素:

redis> LPUSH queue:2 task1
1) (integer) 1
redis> LPUSH queue:3 task2
2) (integer) 1

然后执行BRPOP命令:

redis> BRPOP queue:1 queue:2 queue:3 0
1) "queue:2"
2) "task1"

借此特性可以实现区分优先级的任务队列。我们分别使用queue:confirmation.email和queue:notification.email两个键存储发送确认邮件和发送通知邮件两种任务,然后将消费者的代码改为:

loop
  $task =
    BRPOP queue:confirmation.email,
        queue:notification.email,
       0
  execute($task[1])

这时一旦发送确认邮件的任务被加入到queue:confirmation.email队列中,无论queue:notification.email还有多少任务,消费者都会优先完成发送确认邮件的任务。

4.4.4 “发布/订阅”模式

除了实现任务队列外,Redis 还提供了一组命令可以让开发者实现“发布/订阅” (publish/subscribe)模式。“发布/订阅”模式同样可以实现进程间的消息传递,其原理是这样的:

“发布/订阅”模式中包含两种角色,分别是发布者和订阅者。订阅者可以订阅一个或若干个频道(channel),而发布者可以向指定的频道发送消息,所有订阅此频道的订阅者都会收到此消息。

发布者发布消息的命令是 PUBLISH,用法是 PUBLISH channel message,如向channel.1说一声“hi”:

redis> PUBLISH channel.1 hi
(integer) 0

这样消息就发出去了。PUBLISH命令的返回值表示接收到这条消息的订阅者数量。因为此时没有客户端订阅channel.1,所以返回0。发出去的消息不会被持久化,也就是说当有客户端订阅channel.1后只能收到后续发布到该频道的消息,之前发送的就收不到了。

订阅频道的命令是 SUBSCRIBE,可以同时订阅多个频道,用法是 SUBSCRIBE channel [channel …]。现在新开一个redis-cli实例A,用它来订阅channel.1:

redis A> SUBSCRIBE channel.1
Reading messages... (press Ctrl-C to quit)
1) "subscribe"
2) "channel.1"
3) (integer) 1

执行 SUBSCRIBE 命令后客户端会进入订阅状态,处于此状态下客户端不能使用除SUBSCRIBE/UNSUBSCRIBE/PSUBSCRIBE/PUNSUBSCRIBE这4个属于“发布/订阅”模式的命令之外的命令(后面3个命令会在下面介绍),否则会报错。

进入订阅状态后客户端可能收到三种类型的回复。每种类型的回复都包含 3 个值,第一个值是消息的类型,根据消息类型的不同,第二、三个值的含义也不同。消息类型可能的取值有:

(1)Subscribe。表示订阅成功的反馈信息。第二个值是订阅成功的频道名称,第三个值是当前客户端订阅的频道数量。

(2)message。这个类型的回复是我们最关心的,它表示接收到的消息。第二个值表示产生消息的频道名称,第三个值是消息的内容。

(3)unsubscribe。表示成功取消订阅某个频道。第二个值是对应的频道名称,第三个值是当前客户端订阅的频道数量,当此值为0时客户端会退出订阅状态,之后就可以执行其他非“发布/订阅”模式的命令了。

上例中当实例A订阅了channel.1进入订阅状态后收到了一条subscribe类型的回复,这时我们打开另一个redis-cli实例B,并向channel.1发送一条消息:

redis B> PUBLISH channel.1 hi!
(integer) 1

返回值为1表示有一个客户端订阅了channel.1,此时实例A收到了类型为message的回复:

1) "message"
2) "channel.1"
3) "hi!"

使用 UNSUBSCRIBE 命令可以取消订阅指定的频道,用法为 UNSUBSCRIBE [channel [channel …]],如果不指定频道则会取消订阅所有频道1。

4.4.5 按照规则订阅

除了可以使用SUBSCRIBE命令订阅指定名称的频道外,还可以使用PSUBSCRIBE命令订阅指定的规则。规则支持glob风格通配符格式(见3.1节),下面我们新打开一个redis-cli实例C进行演示:

redis C> PSUBSCRIBE channel.?*
Reading messages... (press Ctrl-C to quit)
1) "psubscribe"
2) "channel.?*"
3) (integer) 1

规则channel.?*可以匹配channel.1和channel.10,但不会匹配channel.。这时在实例B中发布消息:

redis B> PUBLISH channel.1 hi!
(integer) 2

返回结果为2是因为实例A和实例C两个客户端都订阅了channel.1频道。实例C接收到的回复是:

1) "pmessage"
2) "channel.?*"
3) "channel.1"
4) "hi!"

第一个值表示这条消息是通过PSUBSCRIBE命令订阅频道而收到的,第二个值表示订阅时使用的通配符,第三个值表示实际收到消息的频道命令,第四个值则是消息内容。

提示

使用PSUBSCRIBE命令可以重复订阅一个频道,如某客户端执行了PSUBSCRIBE channel.? channel.?,这时向channel.2发布消息后该客户端会收到两条消息,而同时PUBLISH命令返回的值也是2而不是1。同样的,如果有另一个客户端执行了SUBSCRIBE channel.10,和PSUBSCRIBE channel.?的话,向channel.10发送命令该客户端也会收到两条消息(但是是两种类型,message和pmessage),同时PUBLISH命令会返回2。

PUNSUBSCRIBE命令可以退订指定的规则,用法是 PUNSUBSCRIBE [pattern [pattern …]],如果没有参数则会退订所有规则。

注意

使用PUNSUBSCRIBE命令只能退订通过PSUBSCRIBE命令订阅的规则,不会影响直接通过SUBSCRIBE命令订阅的频道;同样UNSUBSCRIBE命令也不会影响通过PSUBSCRIBE命令订阅的规则。另外容易出错的一点是使用PUNSUBSCRIBE命令退订某个规则时不会将其中的通配符展开,而是进行严格的字符串匹配,所以PUNSUBSCRIBE无法退订channel.规则,而是必须使用PUNSUBSCRIBE channel.*才能退订。

相关实践学习
基于Redis实现在线游戏积分排行榜
本场景将介绍如何基于Redis数据库实现在线游戏中的游戏玩家积分排行榜功能。
云数据库 Redis 版使用教程
云数据库Redis版是兼容Redis协议标准的、提供持久化的内存数据库服务,基于高可靠双机热备架构及可无缝扩展的集群架构,满足高读写性能场景及容量需弹性变配的业务需求。 产品详情:https://www.aliyun.com/product/kvstore     ------------------------------------------------------------------------- 阿里云数据库体验:数据库上云实战 开发者云会免费提供一台带自建MySQL的源数据库 ECS 实例和一台目标数据库 RDS实例。跟着指引,您可以一步步实现将ECS自建数据库迁移到目标数据库RDS。 点击下方链接,领取免费ECS&RDS资源,30分钟完成数据库上云实战!https://developer.aliyun.com/adc/scenario/51eefbd1894e42f6bb9acacadd3f9121?spm=a2c6h.13788135.J_3257954370.9.4ba85f24utseFl
相关文章
|
7月前
|
监控 NoSQL Redis
一探Redis究竟:超火爆入门指南,你竟然还没看?
Redis是由C语言编写的开源、基于内存、支持多种数据结构、高性能的Key-Value数据库。
226 2
一探Redis究竟:超火爆入门指南,你竟然还没看?
|
9月前
|
存储 消息中间件 缓存
Redis入门指南:深入了解这款高性能缓存数据库
1. 什么是Redis? Redis(Remote Dictionary Server)是一个开源的,基于内存的高性能键值存储系统。它可以作为数据库、缓存和消息中间件使用。Redis的数据存储在内存中,这使得数据的读写速度非常快,因此它被广泛应用于需要高并发、低延迟的场景中。
92 0
|
消息中间件 缓存 NoSQL
Redis 入门:pub,sub 模式消息通知的说明| 学习笔记
快速学习 Redis 入门:pub,sub 模式消息通知的说明。
143 0
Redis 入门:pub,sub 模式消息通知的说明| 学习笔记
|
存储 JSON NoSQL
Day2、我室友打了一把王者时间我学会了Redis的入门指南
Day2、我室友打了一把王者时间我学会了Redis的入门指南
119 0
Day2、我室友打了一把王者时间我学会了Redis的入门指南
|
存储 监控 NoSQL
Redis入门指南
随着互联网业务对性能需求日益强烈,作为Key/Value存储的Redis具有数据类型丰富和性能表现优异的特点。如果能够熟练地驾驭它,不管是把它用做缓存还是存储,对很多大型应用都很多帮助。新浪作为世界上最大的Redis使用者,体会到了Redis为高并发在线业务带来的好处,但同时也遇到了很多挑战,新浪为推动Redis这种NoSQL产品在中国互联网产品技术架构中的使用做出了卓越的贡献。
1346 0
|
消息中间件 NoSQL C#
Redis 小白指南(三)- 事务、过期、消息通知、管道和优化内存空间
原文:Redis 小白指南(三)- 事务、过期、消息通知、管道和优化内存空间 Redis 小白指南(三)- 事务、过期、消息通知、管道和优化内存空间 简介   《Redis 小白指南(一)- 简介、安装、GUI 和 C# 驱动介绍》 讲的是 Redis 的介绍,以及如何在 Windows 上安装并使用,一些 GUI 工具和自己简单封装的 RedisHelper。
1570 0
|
存储 NoSQL 数据库
《Redis入门指南》一导读
Redis如今已经成为Web开发社区中最火热的内存数据库之一,而它的诞生距现在不过才4年。随着Web 2.0的蓬勃发展,网站数据快速增长,对高性能读写的需求也越来越多,再加上半结构化的数据比重逐渐变大,人们对早已被铺天盖地地运用着的关系数据库能否适应现今的存储需求产生了疑问。
1770 0

热门文章

最新文章