《65哥,如果你有一个漂亮的小姐姐当女朋友,你会怎么把消息传给你的微信好友?》那就别拍什么女朋友美照+亲密照了,做个九宫格图文发朋友圈宣传一下,打击单身狗。”像这样的65哥通过朋友圈发布消息,关注65哥的朋友都可以收到通知的场景称为“发布/订阅机制”。今天先不说小姐姐,我们来详细了解一下“Redis发布/订阅机制”。原理和实际应用。Redis通过SUBSCRIBE、UNSUBSCRIBE和PUBLISH来实现发布-订阅消息模式。Redis提供了两种实现模式,分别是“发布/订阅通道”和“发布/订阅模式”。[toc]Redis发布订阅简介Redis发布订阅(Pus/Sub)是一种消息通信方式:发送者通过PUBLISH发布消息,订阅者通过SUBSCRIBE订阅接收消息或通过UNSUBSCRIBE取消订阅。它主要由“Publisher”、“Subscriber”、“Channel”三部分组成。发布者和订阅者属于客户端,Channel是Redis服务器。发布者向频道发布消息,订阅频道的订阅者接收消息。如下图,三个“订阅者”订阅了“ChannelA”频道:此时,组长向“ChannelA”发布消息,该消息的订阅者会收到消息“关注码哥字节,提高技术》:Pub/Sub在实战中废话不多说。了解了基本概念后,学习一门技术的第一步就是先跑起来,再去摸索原理,做到“知其然,知其所以然”的境界。“发布\订阅”的实现有两种模式:使用频道(Channel)发布订阅;发布-订阅使用模式(Pattern)。需要注意的是,发布-订阅机制与db空间无关。比如在db10发布,db0的订阅者也会收到消息。通过频道(Channel)实现三步:订阅者订阅频道;发布者向“频道”发布消息;所有订阅“频道”的订阅者都会收到消息。订阅者订阅频道使用SUBSCRIBEchannel[channel...]订阅一个或多个频道,O(n)时间复杂度,n=订阅频道的数量。SUBSCRIBEdevelopReadingmessages...(pressCtrl-Ctoquit)1)"subscribe"//messagetype2)"develop"//channel3)(integer)1//messagecontent执行这条命令后,客户端进入订阅状态下,订阅者只能使用四种“发布/订阅”命令:subscribe、unsubscribe、psubscribe、punsubscribe。客户端“小菜鸡”订阅了“开发”频道,接收组长的消息。消息响应体分别表示:消息类型:订阅、消息、退订频道消息内容:不同的消息类型代表不同的含义。进入订阅后,客户端可以收到3种消息回复:subscribe:订阅成功的反馈消息,第二个值是订阅成功的频道名称,第三个是当前客户端订阅的频道数。message:客户端收到消息,第二个值表示产生消息的通道名称,第三个值是消息的内容。unsubscribe:表示频道已成功退订。第二个值是对应的频道名称,第三个值是当前客户端订阅的频道数。当该值为0时,客户端将退出订阅状态,然后可以执行其他非“发布/订阅”模式的命令。向上。发布者发布消息组长使用PUBLISH通道消息将消息发布到指定的“develop”通道。PUBLISHdevelop'dojob'(integer)1需要注意的是,发布的消息不会被持久化。消息发布后,如果有新的“开发”帅哥订阅,则只能收到该频道发布的后续消息。“不问往事,只求今朝”的好榜样。订阅者接受消息关注“开发”频道的订阅者将收到“做工作”消息。//订阅develop频道SUBSCRIBEdevelopReadingmessages...(pressCtrl-Ctoquit)1)"subscribe"//订阅频道成功2)"develop"//Channel3)(integer)1//当发布者发布消息,订阅者读取的消息如下1)"message"//收到消息2)"develop"//通道名称3)"dojob"//的逆向操作消息内容退订频道订阅,“65哥”天天发朋友圈秀恩爱,受不了了,退订他的朋友圈。使用UNSUBSCRIBE命令取消订阅指定的“模式”不会影响通过`subscribe命令订阅的频道。同样,unsubscribe命令不会影响psubscribe命令订阅的规则。通过模式实现接下来,我们再看看另一种实现发布和订阅的方式。如下图所示,当“匹配模式”匹配到这个频道时,当一条消息发布到该频道时,该消息也会发布到匹配到这个频道的“匹配模式”。模式”,订阅该模式的客户端也会收到该消息。模式smile.girl.*表示“你微笑时真美”模式,匹配该模式的两个频道是smile.girls.Tina和smile。girls.maggi,分别表示她们喜欢“微笑的蒂娜”和“微笑的玛吉”粉丝。如下图所示:现在Tina发布消息,向smile.girls.Tina频道发送消息时,除了会收到订阅smile.girls.Tina频道的粉丝的消息外,消息还会被发送给订阅的smile.girl.*模式关注者(因为频道与模式匹配)。这些粉丝比较贪心,“一笑起来就很美的妹子”都跟了上去,LSP~~,马哥不是这样的人。使用匹配模式,使用PUBLISH将消息发布到订阅的smile.girls.Tina客户端,并将“频道”与“发布/订阅模式”中的模式进行比较。如果Channel匹配某种模式,也将此消息发布给订阅此模式的客户端。订阅模式订阅模式命令为PSUBSCRIBE,表示LSP订阅"smile.girl.*"模式如下:PSUBSCRIBEsmile.girls.*Readingmessages...(pressCtrl-Ctoquit)1)"psubscribe"//messagetype2)"smile.girls.*"//Mode3)(integer)1//反向模式下对应订阅数退订的命令为PUNSUBSCRIBEsmile.girl.*。订阅“smile.girls.Tina”频道:SUBSCRIBEsmile.girls.TinaReadingmessages...(pressCtrl-Ctoquit)1)"subscribe"2)"smile.girls.Tina"3)(integer)1subscribe"smile.girls.maggi”频道:SUBSCRIBEsmile.girls.maggi正在阅读消息...(按Ctrl-C退出)1)“订阅”2)“smile.girls.maggi”3)(整数)1Tina发布消息,关注“smile.girls.Tina”的关注者和订阅与该频道匹配的模式“smile.girls.*”的粉丝会收到消息。遵循“smile.girls.*”模式的粉丝会收到消息PSUBSCRIBEsmile.girls.*Readingmessages...(按Ctrl-C退出)1)"psubscribe"2)"smile.girls.*"3)(integer)1//进入订阅状态,接收消息1)"pmessage"消息类型2)"smile.girls.*"3)"smile.girls.Tina"4)"loveu"//消息内容重点“smile.girls.Tina”粉丝收到消息127.0.0.1:6379>SUBSCRIBEsmile.girls.TinaReadingmessages...(pressCtrl-Ctoquit)//SUBSCRIBESUCCESSFUL1)"subscribe"2)"smile.girls.tina"3)(integer)1//接收消息1)"message"2)"smile.girls.Tina"3)"loveu"pattern,则客户端将收到多条消息。例如,65哥订阅了“smile.girls.Tina”频道和“smile.girls.*”模式,那么当Tina向该频道发布消息时,65哥会收到两条工单消息,一条消息类型为message,一种是pmessage。Redisson与SpringBoot实战官方文档:https://github.com/redisson/r...producer代码/***发布消息到Topic*@parammessagemessage*@return接收消息的客户端数量*/publiclongsendMessage(字符串消息){RTopictopic=redissonClient.getTopic(CHANNEL);longpublish=topic.publish(消息);log.info("生产者发送消息成功,msg={}",message);returnpublish;}消费者代码publicvoidonMessage(){//在其他线程或JVMRTopictopic=redissonClient.getTopic(CHANNEL);topic.addListener(String.class,(channel,msg)->{log.info("channel:{}接收消息{}.",channel,msg);});}需要注意的是发布消息和收听消息应该在不同的JVM中运行。如果使用同一个redissonClient发布,则不会监听到自己的消息。原理分析通过上面我们知道了发布和订阅的概念,而实现发布和订阅有两种模式。并使用原生指令和Redisson进行实战。接下来,我们需要深入了解Redis是如何实现发布订阅机制的,从而知其然,知其所以然。频道(Channel)发布/订阅是如何实现的?65大哥,如果是的话,你会用什么数据结构来根据频道定位到对应的客户端呢?码哥,我觉得可以用字典来实现。字典的key对应订阅的频道,字典的value可以用一个链表,里面存储了所有订阅这个频道的client。数据结构很聪明。Redis在redis.h中使用了一个redisServer结构来维护每个服务器进程来代表服务器状态。pubsub_channels属性是保存订阅频道信息的字典。structredisServer{.../*Pubsub*/dict*pubsub_channels;...}如下图,“麻哥”和“亮子”订阅了“redis-channel”,“宅男”和“LSP”订阅了“知~vine¥yu*xiang-li”:发送向频道生产者发送消息并调用PUBLISH频道消息发送消息。程序首先根据channel从pubsub_channels中定位到字典的key所在的bucket,然后将消息发送到key对应的value列表allclients。退订频道UNSUBSCRIBE命令可以退订指定频道:在字典操作方面,根据key找到follow列表,遍历列表,删除客户端,这样消息就不会再发给客户端了。模式(Pattern)发布/订阅是如何实现的?接下来我们继续看基于模式的发布-订阅原理……当使用PUBLISH向频道发布消息时,不仅所有订阅频道的客户端都会收到消息,匹配模式的客户端也会收到将收到消息。到消息。源代码在server.h文件的redisServer.pubsub_patterns属性中定义。structredisServer{.../*pubsub_patterns字典*/dict*pubsub_patterns;...}也是一个dict字典类型,key对应“pattern”模式,value是一个链表类型结构:list*clients包含匹配模式的客户端列表。当执行PSUBSCRIBEsmile.girls.*命令时,会执行pubsubSubscribePattern方法。这里分享下如何定位关键源码,发布订阅,大家可以凭经验搜索pubsub获取pubsub.c:码哥用CLion调试的Redis源码和我们IDEAforJava是同一家公司的开发,所以快捷键都是一样的,然后用Command+F12弹出方法搜索,找到pubsubSubscribePattern订阅模式的方法。方法参数分别表示关注模式的客户端clientc,和客户端想要关注的模式。方法主要逻辑如下:listSearchKey(c->pubsub_patterns,pattern):根据pattern,从redisServer.pubsub_patterns字典Modekey中检查pattern是否已经存在,如果存在则调用addReplyPubsubPatSubscribed通知客户端表示已经订阅,否则继续执行下面的逻辑。dictFind(server.pubsub_patterns,pattern):根据patternpattern从字典server.pubsub_patterns中找到dictEntry哈希桶,如果为空则调用listCreate()创建客户端链表list*clients,放入字典中,key=pattern,value=list*clients链表。如果哈希桶不为空,则将当前客户端client*c添加到list*clients列表的尾节点。因此,模式实现的发布订阅也使用字典来保存模式与客户端的关系,如下图所示:当使用PUBLISH发布消息时,除了发布到客户端订阅channel,也会在pubsub_patterns字典中查找channel,在modekey对应的value中匹配client链表,执行消息发送。取消订阅模式使用PUNSUBSCRIBE命令取消订阅指定模式。该命令执行订阅模式的逆向操作:根据模式从pubsub_patterns字典中找到client链表,遍历链表删除当前client。总结一下Redis的发布订阅功能,主要是通过以下命令实现的:subscribechannel[channel...]:订阅一个或多个频道;unsubscribechannel取消订阅指定频道;发布频道消息,向指定频道发送消息;psubscribepattern订阅指定的模式;punsubscribepattern取消订阅指定的模式。Pub/Sub与数据库无关。例如,如果它发布在DB0上,那么DB1的订阅者也会收到它。基于频道的发布订阅信息由服务器进程的redisServer.pubsub_channels字典保存,key=订阅频道,value为所有订阅频道的客户端链表。当消息发布到通道时,遍历字典得到所有客户端,将消息发送到通道的客户端。基于模式的发布订阅信息保存在字典pubsub_patterns中,key=pattern,value为客户端链表。当一条消息发布到一个频道时,除了订阅该频道的客户端之外,所有订阅了与该频道匹配的模式的客户端也会收到该消息。使用场景说了这么多,那么Redis的发布订阅可以在哪些场景下发挥作用呢?哨兵间通信在哨兵集群中,各个哨兵节点使用Pub/Sub发布和订阅,实现哨兵之间的相互发现,找到Slave。详情请点击->《哨兵集群原理那些事》。sentinel与master建立通信后,使用master提供的发布/订阅机制,在__sentinel__:hello中发布自己的信息,比如身高体重,是否单身,IP,端口...,并订阅到这个通道获取其他sentinel的信息,就是它实现了sentry间的通信。在消息队列之前,“码哥”给大家分享了如何使用RedisList和Stream实现消息队列。我们也可以使用Redis发布订阅实现轻量简单的MQ功能,实现上下游解耦。需要注意的是,Redis的发布订阅消息是不会持久化的,所以新订阅的客户端是收不到历史消息的。也不支持ACK机制,所以现在的业务不能容忍这些缺点,所以使用专业的消息队列。如果能忍,就可以快速享受Redis带来的优势。最后,大家可以在评论区叫我“美少年”吗?马哥为了写这篇文章,看了很多笑起来很美的女孩子才写的。原创并不容易。朋友喜欢,分享,收藏和支持我。参考资料1.Redis设计与实现2.https://redisbook.readthedocs...
