learn.lianglianglee.com/专栏/Redis 核心原理与实战/31 实战:定时任务案例.md.html
2022-05-11 18:57:05 +08:00

1095 lines
24 KiB
HTML
Raw Permalink 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>31 实战:定时任务案例.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 是如何执行的.md.html</a>
</li>
<li>
<a href="/专栏/Redis 核心原理与实战/02 Redis 快速搭建与使用.md.html">02 Redis 快速搭建与使用.md.html</a>
</li>
<li>
<a href="/专栏/Redis 核心原理与实战/03 Redis 持久化——RDB.md.html">03 Redis 持久化——RDB.md.html</a>
</li>
<li>
<a href="/专栏/Redis 核心原理与实战/04 Redis 持久化——AOF.md.html">04 Redis 持久化——AOF.md.html</a>
</li>
<li>
<a href="/专栏/Redis 核心原理与实战/05 Redis 持久化——混合持久化.md.html">05 Redis 持久化——混合持久化.md.html</a>
</li>
<li>
<a href="/专栏/Redis 核心原理与实战/06 字符串使用与内部实现原理.md.html">06 字符串使用与内部实现原理.md.html</a>
</li>
<li>
<a href="/专栏/Redis 核心原理与实战/07 附录:更多字符串操作命令.md.html">07 附录:更多字符串操作命令.md.html</a>
</li>
<li>
<a href="/专栏/Redis 核心原理与实战/08 字典使用与内部实现原理.md.html">08 字典使用与内部实现原理.md.html</a>
</li>
<li>
<a href="/专栏/Redis 核心原理与实战/09 附录:更多字典操作命令.md.html">09 附录:更多字典操作命令.md.html</a>
</li>
<li>
<a href="/专栏/Redis 核心原理与实战/10 列表使用与内部实现原理.md.html">10 列表使用与内部实现原理.md.html</a>
</li>
<li>
<a href="/专栏/Redis 核心原理与实战/11 附录:更多列表操作命令.md.html">11 附录:更多列表操作命令.md.html</a>
</li>
<li>
<a href="/专栏/Redis 核心原理与实战/12 集合使用与内部实现原理.md.html">12 集合使用与内部实现原理.md.html</a>
</li>
<li>
<a href="/专栏/Redis 核心原理与实战/13 附录:更多集合操作命令.md.html">13 附录:更多集合操作命令.md.html</a>
</li>
<li>
<a href="/专栏/Redis 核心原理与实战/14 有序集合使用与内部实现原理.md.html">14 有序集合使用与内部实现原理.md.html</a>
</li>
<li>
<a href="/专栏/Redis 核心原理与实战/15 附录:更多有序集合操作命令.md.html">15 附录:更多有序集合操作命令.md.html</a>
</li>
<li>
<a href="/专栏/Redis 核心原理与实战/16 Redis 事务深入解析.md.html">16 Redis 事务深入解析.md.html</a>
</li>
<li>
<a href="/专栏/Redis 核心原理与实战/17 Redis 键值过期操作.md.html">17 Redis 键值过期操作.md.html</a>
</li>
<li>
<a href="/专栏/Redis 核心原理与实战/18 Redis 过期策略与源码分析.md.html">18 Redis 过期策略与源码分析.md.html</a>
</li>
<li>
<a href="/专栏/Redis 核心原理与实战/19 Redis 管道技术——Pipeline.md.html">19 Redis 管道技术——Pipeline.md.html</a>
</li>
<li>
<a href="/专栏/Redis 核心原理与实战/20 查询附近的人——GEO.md.html">20 查询附近的人——GEO.md.html</a>
</li>
<li>
<a href="/专栏/Redis 核心原理与实战/21 游标迭代器过滤器——Scan.md.html">21 游标迭代器过滤器——Scan.md.html</a>
</li>
<li>
<a href="/专栏/Redis 核心原理与实战/22 优秀的基数统计算法——HyperLogLog.md.html">22 优秀的基数统计算法——HyperLogLog.md.html</a>
</li>
<li>
<a href="/专栏/Redis 核心原理与实战/23 内存淘汰机制与算法.md.html">23 内存淘汰机制与算法.md.html</a>
</li>
<li>
<a href="/专栏/Redis 核心原理与实战/24 消息队列——发布订阅模式.md.html">24 消息队列——发布订阅模式.md.html</a>
</li>
<li>
<a href="/专栏/Redis 核心原理与实战/25 消息队列的其他实现方式.md.html">25 消息队列的其他实现方式.md.html</a>
</li>
<li>
<a href="/专栏/Redis 核心原理与实战/26 消息队列终极解决方案——Stream.md.html">26 消息队列终极解决方案——Stream.md.html</a>
</li>
<li>
<a href="/专栏/Redis 核心原理与实战/27 消息队列终极解决方案——Stream.md.html">27 消息队列终极解决方案——Stream.md.html</a>
</li>
<li>
<a href="/专栏/Redis 核心原理与实战/28 实战:分布式锁详解与代码.md.html">28 实战:分布式锁详解与代码.md.html</a>
</li>
<li>
<a href="/专栏/Redis 核心原理与实战/29 实战:布隆过滤器安装与使用及原理分析.md.html">29 实战:布隆过滤器安装与使用及原理分析.md.html</a>
</li>
<li>
<a href="/专栏/Redis 核心原理与实战/30 完整案例:实现延迟队列的两种方法.md.html">30 完整案例:实现延迟队列的两种方法.md.html</a>
</li>
<li>
<a class="current-tab" href="/专栏/Redis 核心原理与实战/31 实战:定时任务案例.md.html">31 实战:定时任务案例.md.html</a>
</li>
<li>
<a href="/专栏/Redis 核心原理与实战/32 实战RediSearch 高性能的全文搜索引擎.md.html">32 实战RediSearch 高性能的全文搜索引擎.md.html</a>
</li>
<li>
<a href="/专栏/Redis 核心原理与实战/33 实战Redis 性能测试.md.html">33 实战Redis 性能测试.md.html</a>
</li>
<li>
<a href="/专栏/Redis 核心原理与实战/34 实战Redis 慢查询.md.html">34 实战Redis 慢查询.md.html</a>
</li>
<li>
<a href="/专栏/Redis 核心原理与实战/35 实战Redis 性能优化方案.md.html">35 实战Redis 性能优化方案.md.html</a>
</li>
<li>
<a href="/专栏/Redis 核心原理与实战/36 实战Redis 主从同步.md.html">36 实战Redis 主从同步.md.html</a>
</li>
<li>
<a href="/专栏/Redis 核心原理与实战/37 实战Redis哨兵模式.md.html">37 实战Redis哨兵模式.md.html</a>
</li>
<li>
<a href="/专栏/Redis 核心原理与实战/38 实战Redis 哨兵模式(下).md.html">38 实战Redis 哨兵模式(下).md.html</a>
</li>
<li>
<a href="/专栏/Redis 核心原理与实战/39 实战Redis 集群模式(上).md.html">39 实战Redis 集群模式(上).md.html</a>
</li>
<li>
<a href="/专栏/Redis 核心原理与实战/40 实战Redis 集群模式(下).md.html">40 实战Redis 集群模式(下).md.html</a>
</li>
<li>
<a href="/专栏/Redis 核心原理与实战/41 案例Redis 问题汇总和相关解决方案.md.html">41 案例Redis 问题汇总和相关解决方案.md.html</a>
</li>
<li>
<a href="/专栏/Redis 核心原理与实战/42 技能学习指南.md.html">42 技能学习指南.md.html</a>
</li>
<li>
<a href="/专栏/Redis 核心原理与实战/43 加餐Redis 的可视化管理工具.md.html">43 加餐Redis 的可视化管理工具.md.html</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>31 实战:定时任务案例</h1>
<p>我在开发的时候曾经遇到了这样一个问题,产品要求给每个在线预约看病的患者,距离预约时间的前一天发送一条提醒推送,以防止患者错过看病的时间。这个时候就要求我们给每个人设置一个定时任务,用前面文章说的延迟队列也可以实现,但延迟队列的实现方式需要开启一个无限循环任务,那有没有其他的实现方式呢?</p>
<p>答案是肯定的,接下来我们就用 Keyspace Notifications键空间通知来实现定时任务<strong>定时任务指的是指定一个时间来执行某个任务,就叫做定时任务</strong></p>
<h3>开启键空间通知</h3>
<p>默认情况下 Redis 服务器端是不开启键空间通知的,需要我们手动开启。</p>
<p>键空间开启分为两种方式:</p>
<ul>
<li>命令设置方式</li>
<li>配置文件设置方式</li>
</ul>
<p>接下来,我们分别来看。</p>
<h4><strong>命令设置方式</strong></h4>
<p>使用 redis-cli 连接到服务器端之后,输入 <code>config set notify-keyspace-events Ex</code> 命令可以直接开启键空间通知功能返回“OK”则表示开启成功如下命令所示</p>
<pre><code class="language-shell">127.0.0.1:6379&gt; config set notify-keyspace-events Ex
OK
</code></pre>
<p><strong>优点:</strong></p>
<ul>
<li>设置方便,无序启动 Redis 服务。</li>
</ul>
<p><strong>缺点:</strong></p>
<ul>
<li>这种方式设置的配置信息是存储在内存中的,重启 Redis 服务之后,配置项会丢失。</li>
</ul>
<h4><strong>配置文件设置方式</strong></h4>
<p>找到 Redis 的配置文件 redis.conf设置配置项 <code>notify-keyspace-events Ex</code>,然后重启 Redis 服务器。</p>
<p><strong>优点:</strong></p>
<ul>
<li>无论 Redis 服务器重启多少次,配置都不会丢失。</li>
</ul>
<p><strong>缺点:</strong></p>
<ul>
<li>需要重启 Redis 服务。</li>
</ul>
<h4><strong>配置说明</strong></h4>
<p>可以看出无论是那种方式,都是设置 notify-keyspace-events Ex其中 Ex 表示开启键事件通知里面的 key 过期事件。</p>
<p>更多配置项说明如下:</p>
<ul>
<li>K键空间通知所有通知以 <code><a href="/cdn-cgi/l/email-protection" class="__cf_email__" data-cfemail="d88787b3bda1aba8b9bbbd98">[email&#160;protected]</a>&lt;db&gt;__</code> 为前缀</li>
<li>E键事件通知所有通知以 <code><a href="/cdn-cgi/l/email-protection" class="__cf_email__" data-cfemail="97c8c8fcf2eef2e1f2f9e3d7">[email&#160;protected]</a>&lt;db&gt;__</code> 为前缀</li>
<li>gDEL、EXPIRE、RENAME 等类型无关的通用命令的通知</li>
<li>$:字符串命令的通知</li>
<li>l列表命令的通知</li>
<li>s集合命令的通知</li>
<li>h哈希命令的通知</li>
<li>z有序集合命令的通知</li>
<li>x过期事件每当有过期键被删除时发送</li>
<li>e驱逐evict事件每当有键因为 maxmemory 政策而被删除时发送</li>
<li>A参数 g$lshzxe 的别名</li>
</ul>
<p>以上配置项可以自由组合,例如我们订阅列表事件就是 El但需要注意的是<strong>如果 notify-keyspace-event 的值设置为空,则表示不开启任何通知,有值则表示开启通知</strong></p>
<h3>功能实现</h3>
<p>我们要实现定时任务需要使用 Pub/Sub 订阅者和发布者的功能,使用订阅者订阅元素的过期事件,然后再执行固定的任务,这就是定时任务的实现思路。</p>
<p>以本文开头的问题为例,我们是这样实现此定时任务的,首先根据每个患者预约的时间往前推一天,然后再计算出当前时间和目标时间(预约前一天的时间)的毫秒值,把这个值作为元素的过期时间设置到 Redis 中,当这个键过期的时候,我们使用订阅者模式就可以订阅到此信息,然后再发提醒消息给此用户,这样就实现了给每个患者开启一个单独的分布式定时任务的功能。</p>
<p>我们先用命令的模式来模拟一下此功能的实现,首先,我们使用 redis-cli 开启一个客户端,监听 <code><a href="/cdn-cgi/l/email-protection" class="__cf_email__" data-cfemail="247b7b4f415d4152414a506414">[email&#160;protected]</a>__:expired</code> 键过期事件,此监听值 <code><a href="/cdn-cgi/l/email-protection" class="__cf_email__" data-cfemail="7629291d130f13001318023646">[email&#160;protected]</a>__:expired</code> 为固定的写法,其中 0 表示第一个数据库,我们知道 Redis 中一共有 16 个数据,默认使用的是第 0 个,我们建议新开一个非 0 的数据库专门用来实现定时任务,这样就可以避免很多无效的事件监听。</p>
<p>命令监听如下:</p>
<pre><code class="language-shell">127.0.0.1:6379&gt; psubscribe <a href="/cdn-cgi/l/email-protection" class="__cf_email__" data-cfemail="4d1212262834283b2823390d7d">[email&#160;protected]</a>__:expired
1) &quot;psubscribe&quot;
2) &quot;<a href="/cdn-cgi/l/email-protection" class="__cf_email__" data-cfemail="93ccccf8f6eaf6e5f6fde7d3a3">[email&#160;protected]</a>__:expired&quot;
3) (integer) 1
</code></pre>
<p>此时我们开启另一个客户端,添加两条测试数据试试,命令如下:</p>
<pre><code class="language-shell">127.0.0.1:6379&gt; set key value ex 3
OK
127.0.0.1:6379&gt; set user xiaoming ex 3
OK
</code></pre>
<p>等过去 3 秒钟之后,我们去看监听结果如下:</p>
<pre><code class="language-shell">127.0.0.1:6379&gt; psubscribe <a href="/cdn-cgi/l/email-protection" class="__cf_email__" data-cfemail="104f4f7b75697566757e645020">[email&#160;protected]</a>__:expired
1) &quot;psubscribe&quot;
2) &quot;<a href="/cdn-cgi/l/email-protection" class="__cf_email__" data-cfemail="d18e8ebab4a8b4a7b4bfa591e1">[email&#160;protected]</a>__:expired&quot;
3) (integer) 1
1) &quot;pmessage&quot;
2) &quot;<a href="/cdn-cgi/l/email-protection" class="__cf_email__" data-cfemail="da8585b1bfa3bfacbfb4ae9aea">[email&#160;protected]</a>__:expired&quot;
3) &quot;<a href="/cdn-cgi/l/email-protection" class="__cf_email__" data-cfemail="cb9494a0aeb2aebdaea5bf8bfb">[email&#160;protected]</a>__:expired&quot;
4) &quot;key&quot; #接收到过期信息 key
1) &quot;pmessage&quot;
2) &quot;<a href="/cdn-cgi/l/email-protection" class="__cf_email__" data-cfemail="3f6060545a465a495a514b7f0f">[email&#160;protected]</a>__:expired&quot;
3) &quot;<a href="/cdn-cgi/l/email-protection" class="__cf_email__" data-cfemail="eab5b5818f938f9c8f849eaada">[email&#160;protected]</a>__:expired&quot;
4) &quot;user&quot; #接收到过期信息 user
</code></pre>
<p>已经成功的介绍到两条过期信息了。</p>
<h3>代码实战</h3>
<p>本文我们使用 Jedis 来实现定时任务,代码如下:</p>
<pre><code class="language-java">import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPubSub;
import utils.JedisUtils;
/**
* 定时任务
*/
public class TaskExample {
public static final String _TOPIC = &quot;<a href="/cdn-cgi/l/email-protection" class="__cf_email__" data-cfemail="e3bcbc88869a8695868d97a3d3">[email&#160;protected]</a>__:expired&quot;; // 订阅频道名称
public static void main(String[] args) {
Jedis jedis = JedisUtils.getJedis();
// 执行定时任务
doTask(jedis);
}
/**
* 订阅过期消息,执行定时任务
* @param jedis Redis 客户端
*/
public static void doTask(Jedis jedis) {
// 订阅过期消息
jedis.psubscribe(new JedisPubSub() {
@Override
public void onPMessage(String pattern, String channel, String message) {
// 接收到消息,执行定时任务
System.out.println(&quot;收到消息:&quot; + message);
}
}, _TOPIC);
}
}
</code></pre>
<h3>小结</h3>
<p>本文我们通过开启 Keyspace Notifications 和 Pub/Sub 消息订阅的方式,可以拿到每个键值过期的事件,我们利用这个机制实现了给每个人开启一个定时任务的功能,过期事件中我们可以获取到过期键的 key 值,在 key 值中我们可以存储每个用户的 id例如“user_1001”的方式其中数字部分表示用户的编号通过此编号就可以完成给对应人发送消息通知的功能。</p>
</div>
</div>
<div>
<div style="float: left">
<a href="/专栏/Redis 核心原理与实战/30 完整案例:实现延迟队列的两种方法.md.html">上一页</a>
</div>
<div style="float: right">
<a href="/专栏/Redis 核心原理与实战/32 实战RediSearch 高性能的全文搜索引擎.md.html">下一页</a>
</div>
</div>
</div>
</div>
</div>
</div>
<a class="off-canvas-overlay" onclick="hide_canvas()"></a>
</div>
<script data-cfasync="false" src="/cdn-cgi/scripts/5c5dd728/cloudflare-static/email-decode.min.js"></script><script defer src="https://static.cloudflareinsights.com/beacon.min.js/v652eace1692a40cfa3763df669d7439c1639079717194" integrity="sha512-Gi7xpJR8tSkrpF7aordPZQlW2DLtzUlZcumS8dMQjwDHEnw9I7ZLyiOj/6tZStRBGtGgN6ceN6cMH8z7etPGlw==" data-cf-beacon='{"rayId":"709973f3bfd33d60","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>