learn.lianglianglee.com/专栏/Redis 核心原理与实战/24 消息队列——发布订阅模式.md.html
2022-08-14 03:40:33 +08:00

467 lines
23 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!DOCTYPE html>
<!-- saved from url=(0046)https://kaiiiz.github.io/hexo-theme-book-demo/ -->
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1.0, user-scalable=no">
<link rel="icon" href="/static/favicon.png">
<title>24 消息队列——发布订阅模式.md.html</title>
<!-- Spectre.css framework -->
<link rel="stylesheet" href="/static/index.css">
<!-- theme css & js -->
<meta name="generator" content="Hexo 4.2.0">
</head>
<body>
<div class="book-container">
<div class="book-sidebar">
<div class="book-brand">
<a href="/">
<img src="/static/favicon.png">
<span>技术文章摘抄</span>
</a>
</div>
<div class="book-menu uncollapsible">
<ul class="uncollapsible">
<li><a href="/" class="current-tab">首页</a></li>
</ul>
<ul class="uncollapsible">
<li><a href="../">上一级</a></li>
</ul>
<ul class="uncollapsible">
<li>
<a href="/专栏/Redis 核心原理与实战/01 Redis 是如何执行的.md.html">01 Redis 是如何执行的</a>
</li>
<li>
<a href="/专栏/Redis 核心原理与实战/02 Redis 快速搭建与使用.md.html">02 Redis 快速搭建与使用</a>
</li>
<li>
<a href="/专栏/Redis 核心原理与实战/03 Redis 持久化——RDB.md.html">03 Redis 持久化——RDB</a>
</li>
<li>
<a href="/专栏/Redis 核心原理与实战/04 Redis 持久化——AOF.md.html">04 Redis 持久化——AOF</a>
</li>
<li>
<a href="/专栏/Redis 核心原理与实战/05 Redis 持久化——混合持久化.md.html">05 Redis 持久化——混合持久化</a>
</li>
<li>
<a href="/专栏/Redis 核心原理与实战/06 字符串使用与内部实现原理.md.html">06 字符串使用与内部实现原理</a>
</li>
<li>
<a href="/专栏/Redis 核心原理与实战/07 附录:更多字符串操作命令.md.html">07 附录:更多字符串操作命令</a>
</li>
<li>
<a href="/专栏/Redis 核心原理与实战/08 字典使用与内部实现原理.md.html">08 字典使用与内部实现原理</a>
</li>
<li>
<a href="/专栏/Redis 核心原理与实战/09 附录:更多字典操作命令.md.html">09 附录:更多字典操作命令</a>
</li>
<li>
<a href="/专栏/Redis 核心原理与实战/10 列表使用与内部实现原理.md.html">10 列表使用与内部实现原理</a>
</li>
<li>
<a href="/专栏/Redis 核心原理与实战/11 附录:更多列表操作命令.md.html">11 附录:更多列表操作命令</a>
</li>
<li>
<a href="/专栏/Redis 核心原理与实战/12 集合使用与内部实现原理.md.html">12 集合使用与内部实现原理</a>
</li>
<li>
<a href="/专栏/Redis 核心原理与实战/13 附录:更多集合操作命令.md.html">13 附录:更多集合操作命令</a>
</li>
<li>
<a href="/专栏/Redis 核心原理与实战/14 有序集合使用与内部实现原理.md.html">14 有序集合使用与内部实现原理</a>
</li>
<li>
<a href="/专栏/Redis 核心原理与实战/15 附录:更多有序集合操作命令.md.html">15 附录:更多有序集合操作命令</a>
</li>
<li>
<a href="/专栏/Redis 核心原理与实战/16 Redis 事务深入解析.md.html">16 Redis 事务深入解析</a>
</li>
<li>
<a href="/专栏/Redis 核心原理与实战/17 Redis 键值过期操作.md.html">17 Redis 键值过期操作</a>
</li>
<li>
<a href="/专栏/Redis 核心原理与实战/18 Redis 过期策略与源码分析.md.html">18 Redis 过期策略与源码分析</a>
</li>
<li>
<a href="/专栏/Redis 核心原理与实战/19 Redis 管道技术——Pipeline.md.html">19 Redis 管道技术——Pipeline</a>
</li>
<li>
<a href="/专栏/Redis 核心原理与实战/20 查询附近的人——GEO.md.html">20 查询附近的人——GEO</a>
</li>
<li>
<a href="/专栏/Redis 核心原理与实战/21 游标迭代器过滤器——Scan.md.html">21 游标迭代器过滤器——Scan</a>
</li>
<li>
<a href="/专栏/Redis 核心原理与实战/22 优秀的基数统计算法——HyperLogLog.md.html">22 优秀的基数统计算法——HyperLogLog</a>
</li>
<li>
<a href="/专栏/Redis 核心原理与实战/23 内存淘汰机制与算法.md.html">23 内存淘汰机制与算法</a>
</li>
<li>
<a class="current-tab" href="/专栏/Redis 核心原理与实战/24 消息队列——发布订阅模式.md.html">24 消息队列——发布订阅模式</a>
</li>
<li>
<a href="/专栏/Redis 核心原理与实战/25 消息队列的其他实现方式.md.html">25 消息队列的其他实现方式</a>
</li>
<li>
<a href="/专栏/Redis 核心原理与实战/26 消息队列终极解决方案——Stream.md.html">26 消息队列终极解决方案——Stream</a>
</li>
<li>
<a href="/专栏/Redis 核心原理与实战/27 消息队列终极解决方案——Stream.md.html">27 消息队列终极解决方案——Stream</a>
</li>
<li>
<a href="/专栏/Redis 核心原理与实战/28 实战:分布式锁详解与代码.md.html">28 实战:分布式锁详解与代码</a>
</li>
<li>
<a href="/专栏/Redis 核心原理与实战/29 实战:布隆过滤器安装与使用及原理分析.md.html">29 实战:布隆过滤器安装与使用及原理分析</a>
</li>
<li>
<a href="/专栏/Redis 核心原理与实战/30 完整案例:实现延迟队列的两种方法.md.html">30 完整案例:实现延迟队列的两种方法</a>
</li>
<li>
<a href="/专栏/Redis 核心原理与实战/31 实战:定时任务案例.md.html">31 实战:定时任务案例</a>
</li>
<li>
<a href="/专栏/Redis 核心原理与实战/32 实战RediSearch 高性能的全文搜索引擎.md.html">32 实战RediSearch 高性能的全文搜索引擎</a>
</li>
<li>
<a href="/专栏/Redis 核心原理与实战/33 实战Redis 性能测试.md.html">33 实战Redis 性能测试</a>
</li>
<li>
<a href="/专栏/Redis 核心原理与实战/34 实战Redis 慢查询.md.html">34 实战Redis 慢查询</a>
</li>
<li>
<a href="/专栏/Redis 核心原理与实战/35 实战Redis 性能优化方案.md.html">35 实战Redis 性能优化方案</a>
</li>
<li>
<a href="/专栏/Redis 核心原理与实战/36 实战Redis 主从同步.md.html">36 实战Redis 主从同步</a>
</li>
<li>
<a href="/专栏/Redis 核心原理与实战/37 实战Redis哨兵模式.md.html">37 实战Redis哨兵模式</a>
</li>
<li>
<a href="/专栏/Redis 核心原理与实战/38 实战Redis 哨兵模式(下).md.html">38 实战Redis 哨兵模式(下)</a>
</li>
<li>
<a href="/专栏/Redis 核心原理与实战/39 实战Redis 集群模式(上).md.html">39 实战Redis 集群模式(上)</a>
</li>
<li>
<a href="/专栏/Redis 核心原理与实战/40 实战Redis 集群模式(下).md.html">40 实战Redis 集群模式(下)</a>
</li>
<li>
<a href="/专栏/Redis 核心原理与实战/41 案例Redis 问题汇总和相关解决方案.md.html">41 案例Redis 问题汇总和相关解决方案</a>
</li>
<li>
<a href="/专栏/Redis 核心原理与实战/42 技能学习指南.md.html">42 技能学习指南</a>
</li>
<li>
<a href="/专栏/Redis 核心原理与实战/43 加餐Redis 的可视化管理工具.md.html">43 加餐Redis 的可视化管理工具</a>
</li>
</ul>
</div>
</div>
<div class="sidebar-toggle" onclick="sidebar_toggle()" onmouseover="add_inner()" onmouseleave="remove_inner()">
<div class="sidebar-toggle-inner"></div>
</div>
<script>
function add_inner() {
let inner = document.querySelector('.sidebar-toggle-inner')
inner.classList.add('show')
}
function remove_inner() {
let inner = document.querySelector('.sidebar-toggle-inner')
inner.classList.remove('show')
}
function sidebar_toggle() {
let sidebar_toggle = document.querySelector('.sidebar-toggle')
let sidebar = document.querySelector('.book-sidebar')
let content = document.querySelector('.off-canvas-content')
if (sidebar_toggle.classList.contains('extend')) { // show
sidebar_toggle.classList.remove('extend')
sidebar.classList.remove('hide')
content.classList.remove('extend')
} else { // hide
sidebar_toggle.classList.add('extend')
sidebar.classList.add('hide')
content.classList.add('extend')
}
}
function open_sidebar() {
let sidebar = document.querySelector('.book-sidebar')
let overlay = document.querySelector('.off-canvas-overlay')
sidebar.classList.add('show')
overlay.classList.add('show')
}
function hide_canvas() {
let sidebar = document.querySelector('.book-sidebar')
let overlay = document.querySelector('.off-canvas-overlay')
sidebar.classList.remove('show')
overlay.classList.remove('show')
}
</script>
<div class="off-canvas-content">
<div class="columns">
<div class="column col-12 col-lg-12">
<div class="book-navbar">
<!-- For Responsive Layout -->
<header class="navbar">
<section class="navbar-section">
<a onclick="open_sidebar()">
<i class="icon icon-menu"></i>
</a>
</section>
</header>
</div>
<div class="book-content" style="max-width: 960px; margin: 0 auto;
overflow-x: auto;
overflow-y: hidden;">
<div class="book-post">
<p id="tip" align="center"></p>
<div><h1>24 消息队列——发布订阅模式</h1>
<p>在 Redis 中提供了专门的类型Publisher发布者和 Subscriber订阅者来实现消息队列。</p>
<p>在文章开始之前,先来介绍消息队列中有几个基础概念,以便大家更好的理解本文的内容。</p>
<p>首先,发布消息的叫做发布方或发布者,也就是消息的生产者,而接收消息的叫做消息的订阅方或订阅者,也就是消费者,用来处理生产者发布的消息。</p>
<p><img src="assets/33a25110-6905-11ea-a947-1f4a9107200f" alt="生产者-消费者基础概念-2.png" /></p>
<p>除了发布和和订阅者在消息队列中还有一个重要的概念channel 意为频道或通道,可以理解为某个消息队列的名称,首先消费者先要订阅某个 channel然后当生产者把消息发送到这个 channel 中时,消费者就可以正常接收到消息了,如下图所示:</p>
<p><img src="assets/643cf190-6905-11ea-9247-611fe26c272c" alt="频道channel.png" /></p>
<h3>普通订阅与发布</h3>
<p>消息队列有两个重要的角色,一个是发送者,另一个就是订阅者,对应的命令如下:</p>
<ul>
<li>发布消息publish channel &quot;message&quot;</li>
<li>订阅消息subscribe channel</li>
</ul>
<p>下面我们来看具体的命令实现。</p>
<h4><strong>订阅消息</strong></h4>
<pre><code class="language-shell">127.0.0.1:6379&gt; subscribe channel #订阅消息channel
Reading messages...
1) &quot;subscribe&quot;
2) &quot;channel&quot;
3) (integer) 1
</code></pre>
<p>相关语法:</p>
<pre><code>subscribe channel [channel ...]
</code></pre>
<p>此命令支持订阅一个或多个频道的命令,也就是说一个订阅者可以订阅多个频道。例如,某个客户端订阅了两个频道 channel 和 channel2当两个发布者分别推送消息后订阅者的信息输出如下</p>
<pre><code class="language-shell">127.0.0.1:6379&gt; subscribe channel channel2 #订阅 channel 和 channel2
Reading messages... (press Ctrl-C to quit)
1) &quot;subscribe&quot;
2) &quot;channel&quot;
3) (integer) 1
1) &quot;subscribe&quot;
2) &quot;channel2&quot;
3) (integer) 2
1) &quot;message&quot;
2) &quot;channel&quot; # 收到 channel 消息
3) &quot;message 1.&quot;
1) &quot;message&quot;
2) &quot;channel2&quot; # 收到 channel2 消息
3) &quot;message 2.&quot;
</code></pre>
<p>可以看出此订阅者可以收到来自两个频道的消息推送。</p>
<h4><strong>发送消息</strong></h4>
<pre><code class="language-shell">127.0.0.1:6379&gt; publish channel &quot;hello,redis.&quot; #发布消息
(integer) 1
</code></pre>
<p>相关语法:</p>
<pre><code>publish channel message
</code></pre>
<p>最后的返回值表示成功发送给几个订阅方1 表示成功发给了一个订阅者,这个数字可以是 0~n这是由订阅者的数量决定的。</p>
<p>例如,当有两个订阅者时,推送的结果为 2如下代码所示。</p>
<p>订阅者一:</p>
<pre><code class="language-shell">127.0.0.1:6379&gt; subscribe channel
Reading messages... (press Ctrl-C to quit)
1) &quot;subscribe&quot;
2) &quot;channel&quot;
3) (integer) 1
</code></pre>
<p>订阅者二:</p>
<pre><code class="language-shell">127.0.0.1:6379&gt; subscribe channel
Reading messages... (press Ctrl-C to quit)
1) &quot;subscribe&quot;
2) &quot;channel&quot;
3) (integer) 1
</code></pre>
<p>发送消息:</p>
<pre><code class="language-shell">127.0.0.1:6379&gt; publish channel &quot;message&quot;
(integer) 2
</code></pre>
<p>可以看出,此消息已成功发给两个订阅者,结果也变成 2 了。</p>
<h3>主题订阅</h3>
<p>上面介绍了普通的订阅与发布模式,但如果我要订阅某一个类型的消息就不适用了,例如我要订阅日志类的消息队列,它们的命名都是 logXXX这个时候就需要使用 Redis 提供的另一个功能 Pattern Subscribe 主题订阅,这种方式可以使用 <code>*</code> 来匹配多个频道,如下图所示:</p>
<p><img src="assets/c071d520-6905-11ea-bfcb-156eb66fb883" alt="主题订阅2.png" /></p>
<p>主题模式的具体实现代码如下,订阅者:</p>
<pre><code class="language-shell">127.0.0.1:6379&gt; psubscribe log_* #主题订阅 log_*
1) &quot;psubscribe&quot;
2) &quot;log_*&quot;
3) (integer) 1
1) &quot;pmessage&quot;
2) &quot;log_*&quot;
3) &quot;log_user&quot; #接收到频道 log_user 的消息推送
4) &quot;user message.&quot;
1) &quot;pmessage&quot;
2) &quot;log_*&quot;
3) &quot;log_sys&quot; #接收到频道 log_sys 的消息推送
4) &quot;sys message.&quot;
1) &quot;pmessage&quot;
2) &quot;log_*&quot;
3) &quot;log_db&quot; #接收到频道 log_db 的消息推送
4) &quot;db message&quot;
</code></pre>
<p>从上面的运行结果,可以看出使用命令 <code>psubscribe log_*</code> 可以接收到所有频道包含 log_XXX 的消息。</p>
<p>相关语法:</p>
<pre><code>psubscribe pattern [pattern ...]
</code></pre>
<p>生产者的代码如下:</p>
<pre><code class="language-shell">127.0.0.1:6379&gt; publish log_user &quot;user message.&quot;
(integer) 1
127.0.0.1:6379&gt; publish log_sys &quot;sys message.&quot;
(integer) 1
127.0.0.1:6379&gt; publish log_db &quot;db message&quot;
(integer) 1
</code></pre>
<h3>代码实战</h3>
<p>下面我们使用 Jedis 实现普通的发布订阅模式和主题订阅的功能。</p>
<h4><strong>普通模式</strong></h4>
<p>消费者代码如下:</p>
<pre><code class="language-java">/**
* 消费者
*/
public static void consumer() {
Jedis jedis = new Jedis(&quot;127.0.0.1&quot;, 6379);
// 接收并处理消息
jedis.subscribe(new JedisPubSub() {
@Override
public void onMessage(String channel, String message) {
// 接收消息,业务处理
System.out.println(&quot;频道 &quot; + channel + &quot; 收到消息:&quot; + message);
}
}, &quot;channel&quot;);
}
</code></pre>
<p>生产者代码如下:</p>
<pre><code class="language-java">/**
* 生产者
*/
public static void producer() {
Jedis jedis = new Jedis(&quot;127.0.0.1&quot;, 6379);
// 推送消息
jedis.publish(&quot;channel&quot;, &quot;Hello, channel.&quot;);
}
</code></pre>
<p>发布者和订阅者模式运行:</p>
<pre><code class="language-java">public static void main(String[] args) throws InterruptedException {
// 创建一个新线程作为消费者
new Thread(() -&gt; consumer()).start();
// 暂停 0.5s 等待消费者初始化
Thread.sleep(500);
// 生产者发送消息
producer();
}
</code></pre>
<p>以上代码运行结果如下:</p>
<pre><code>频道 channel 收到消息Hello, channel.
</code></pre>
<h4><strong>主题订阅模式</strong></h4>
<p>主题订阅模式的生产者的代码是一样,只有消费者的代码是不同的,如下所示:</p>
<pre><code class="language-java">/**
* 主题订阅
*/
public static void pConsumer() {
Jedis jedis = new Jedis(&quot;127.0.0.1&quot;, 6379);
// 主题订阅
jedis.psubscribe(new JedisPubSub() {
@Override
public void onPMessage(String pattern, String channel, String message) {
// 接收消息,业务处理
System.out.println(pattern + &quot; 主题 | 频道 &quot; + channel + &quot; 收到消息:&quot; + message);
}
}, &quot;channel*&quot;);
}
</code></pre>
<p>主题模式运行代码如下:</p>
<pre><code class="language-java">public static void main(String[] args) throws InterruptedException {
// 主题订阅
new Thread(() -&gt; pConsumer()).start();
// 暂停 0.5s 等待消费者初始化
Thread.sleep(500);
// 生产者发送消息
producer();
}
</code></pre>
<p>以上代码运行结果如下:</p>
<pre><code>channel* 主题 | 频道 channel 收到消息Hello, channel.
</code></pre>
<h3>注意事项</h3>
<p>发布订阅模式存在以下两个缺点:</p>
<ol>
<li>无法持久化保存消息,如果 Redis 服务器宕机或重启,那么所有的消息将会丢失;</li>
<li>发布订阅模式是“发后既忘”的工作模式,如果有订阅者离线重连之后不能消费之前的历史消息。</li>
</ol>
<p>然而这些缺点在 Redis 5.0 添加了 Stream 类型之后会被彻底的解决。</p>
<p>除了以上缺点外,发布订阅模式还有另一个需要注意问题:当消费端有一定的消息积压时,也就是生产者发送的消息,消费者消费不过来时,如果超过 32M 或者是 60s 内持续保持在 8M 以上,消费端会被强行断开,这个参数是在配置文件中设置的,默认值是 <code>client-output-buffer-limit pubsub 32mb 8mb 60</code></p>
<h3>小结</h3>
<p>本文介绍了消息队列的几个名词,生产者、消费者对应的就是消息的发送者和接收者,也介绍了发布订阅模式的三个命令:</p>
<ul>
<li>subscribe channel 普通订阅</li>
<li>publish channel message 消息推送</li>
<li>psubscribe pattern 主题订阅</li>
</ul>
<p>使用它们之后就可以完成单个频道和多个频道的消息收发,但发送与订阅模式也有一些缺点,比如“发后既忘”和不能持久化等问题,然而这些问题会等到 Stream 类型的出现而得到解决,关于更多 Stream 的内容后面文章会详细介绍。</p>
</div>
</div>
<div>
<div style="float: left">
<a href="/专栏/Redis 核心原理与实战/23 内存淘汰机制与算法.md.html">上一页</a>
</div>
<div style="float: right">
<a href="/专栏/Redis 核心原理与实战/25 消息队列的其他实现方式.md.html">下一页</a>
</div>
</div>
</div>
</div>
</div>
</div>
<a class="off-canvas-overlay" onclick="hide_canvas()"></a>
</div>
<script defer src="https://static.cloudflareinsights.com/beacon.min.js/v652eace1692a40cfa3763df669d7439c1639079717194" integrity="sha512-Gi7xpJR8tSkrpF7aordPZQlW2DLtzUlZcumS8dMQjwDHEnw9I7ZLyiOj/6tZStRBGtGgN6ceN6cMH8z7etPGlw==" data-cf-beacon='{"rayId":"709973e3ec403d60","version":"2021.12.0","r":1,"token":"1f5d475227ce4f0089a7cff1ab17c0f5","si":100}' crossorigin="anonymous"></script>
</body>
<!-- Global site tag (gtag.js) - Google Analytics -->
<script async src="https://www.googletagmanager.com/gtag/js?id=G-NPSEEVD756"></script>
<script>
window.dataLayer = window.dataLayer || [];
function gtag() {
dataLayer.push(arguments);
}
gtag('js', new Date());
gtag('config', 'G-NPSEEVD756');
var path = window.location.pathname
var cookie = getCookie("lastPath");
console.log(path)
if (path.replace("/", "") === "") {
if (cookie.replace("/", "") !== "") {
console.log(cookie)
document.getElementById("tip").innerHTML = "<a href='" + cookie + "'>跳转到上次进度</a>"
}
} else {
setCookie("lastPath", path)
}
function setCookie(cname, cvalue) {
var d = new Date();
d.setTime(d.getTime() + (180 * 24 * 60 * 60 * 1000));
var expires = "expires=" + d.toGMTString();
document.cookie = cname + "=" + cvalue + "; " + expires + ";path = /";
}
function getCookie(cname) {
var name = cname + "=";
var ca = document.cookie.split(';');
for (var i = 0; i < ca.length; i++) {
var c = ca[i].trim();
if (c.indexOf(name) === 0) return c.substring(name.length, c.length);
}
return "";
}
</script>
</html>