背景

相信我们或多或少的会遇到类似下面这样的需求:

第三方给了一批数据给我们处理,我们处理好之后就通知他们处理结果。

大概就是下面这个图说的。

本来在处理完数据之后,我们就会马上把处理结果返回给对方,但是对方要求我们处理速度不能过快,要有一种人为处理的效果。

换句话就是说,就算是处理好了,也要晚一点再执行通知操作。

这就是一个典型的延时任务。

延时,那还不简单,执行完之后,让它Sleep一下就好了,这样就达到目标了。

Sleep一下确定是最容易实现的一种方案,但是试想一下,数据的数量不断的增加,这样Sleep真的好吗?答案是否定的。

延时队列,是处理这个场景最为妥当的方案。

RabbitMQ,RocketMQ,Cmq等都可以直接或间接的达到相应的效果。

如果不具备队列条件,又要怎么处理呢?还可以借助Redis来完成这项工作。

MQ不一定每个公司都会用,但Redis应该80%以上的都会用吧。

处理方案

Redis这边,可用的方案有两种,下面分别来介绍一下。

#1 键的过期时间

在设置缓存的时候,我们比较多情况下都会设置一个缓存的过期时间,这个时间过期后,会重新去数据源拿数据回来。

可以基于这个过期时间结合Redis的keyspace notifications共同完成。

keyspace notifications里面包含了非常多的事件,这里只关注EXPIRE,这个是和过期有关的。

只要订阅了__keyevent@0__:expired这个主题,当有key过期的时候,就会收到对应的信息。

注:主题@后面的0,指的是db 0.

要想使用这个特性,必不可少的一步是修改Redis默认的配置,把notify-keyspace-events设置成Ex

############################# Event notification ##############################  

# Redis can notify Pub/Sub clients about events happening in the key space.
# This feature is documented at http://redis.io/topics/notifications
#
# .........
#
# By default all notifications are disabled because most users don't need
# this feature and the feature has some overhead. Note that if you don't
# specify at least one of K or E, no events will be delivered.
notify-keyspace-events "Ex"

其中 E 指的是键事件通知,x 指的是过期事件。

根据这个特性,重新调整一下流程图:

应该也比较好懂,下面通过简单的代码来实现一下这种方案。

首先是处理完数据及往Redis写数据。

public async Task DoTaskAsync()
{
// 数据处理
// ... // 后续操作要延时,把Id记录下来
var taskId = new Random().Next(1, 10000);
// 要延迟的时间
int sec = new Random().Next(1, 5); // 可以加个重试机制,预防单次执行失败。
await RedisHelper.SetAsync($"task:{taskId}", "1", sec);
}

还需要回传结果的后台任务,这个任务就是去订阅上面说的键过期事件,然后回传结果。

这里可以借助BackgroundService来订阅处理。

public class SubscribeTaskBgTask : BackgroundService
{
protected override Task ExecuteAsync(CancellationToken stoppingToken)
{
stoppingToken.ThrowIfCancellationRequested();
var keyPrefix = "task:";
RedisHelper.Subscribe(
("__keyevent@0__:expired", arg =>
{
var msg = arg.Body;
Console.WriteLine($"recive {msg}");
if (msg.StartsWith(keyPrefix))
{
// 取到任务Id
var val = msg.Substring(keyPrefix.Length);
Console.WriteLine($"{DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss")} begin to do task {val}"); // 回传处理结果给第三方,这里可以考虑这个并发锁,避免多实例都处理了这个任务。
// ....
}
}
)); return Task.CompletedTask;
}
}

这里有一个要注意的地方,要在key里面包含任务的Id,因为订阅处理的时候,只能拿到一个key,后续能做的操作也只是基于这个key。

上面的例子,是用了task:任务Id的形式,所以在订阅处理的时候,只处理以task:开头的那些key。

效果如下:

这种方案,直观上是非常简单的,不过这种方案会遇到一个小问题。

当一个key过期后,并不一定会马上收到通知,这个也是会有一定的延时的,取决于Redis的内部机制。

Redis Keyspace Notifications文档的最后一段也提到了这个问题。

所以用这种方案的时候,要考虑一下,你的延时是不是要及时~~

#2 有序集合

有序集合是Redis中一种十分有用的数据结构,它的本质其实就是集合加了一个排序的功能,每个集合里面的元素还会有一个分值的属性。

它提供了一个可以获取指定分值范围内的元素,这个也就是我们的出发点。

在这个场景下,什么东西可能作为这个分值呢?现在只有一个处理任务的Id还有一个延迟的时间,Id肯定不行,那么也只能是延迟时间来作这个分值了。

延迟1秒,5秒,1分钟,这个都是比较大粒度的时间,这里要转化一下,用时间戳来代替这些延迟的时间。

假设现在的时间戳是 1584171520, 要延迟5秒执行,那么执行任务的时间就是 1584171525,在当前时间戳的基础上加个5秒,就是最终要执行的了。

到时有序集合中存的元素就会是这样的

任务Id-1 1584171525
任务Id-2 1584171528
任务Id-3 1584171530

接下来就是要怎么取出这些任务的问题了!

把当前时间戳当成是取数的最大分值,0作为最小分值,这个时候取出的元素就是应该要执行回传的任务了。

根据这个方案,重新调整一下流程图:

交代清楚了思路,再来点代码,加深一下理解。

首先还是处理完数据后往Redis写数据。

public async Task DoTaskAsync()
{
// 数据处理
// ... // 后续操作要延时,把Id记录下来
var taskId = new Random().Next(1, 10000); var cacheKey = "task:delay";
int sec = new Random().Next(1, 5); // 要执行这个任务的时间戳
var time = DateTimeOffset.Now.AddSeconds(sec).ToUnixTimeSeconds(); await RedisHelper.ZAddAsync(cacheKey, (time, taskId));
Console.WriteLine($"{DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss")} done {taskId} here - {sec}");
}

后面就是轮训有序集合里面的元素了,这里同样是借助BackgroundService来处理。

public class SubscribeTaskBgTask : BackgroundService
{
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
stoppingToken.ThrowIfCancellationRequested();
var cacheKey = "task:delay";
while (true)
{
// 先取,后删,不具备原子性,可考虑用lua脚本来保证原子性。
var vals = await RedisHelper.ZRangeByScoreAsync(cacheKey, -1, DateTimeOffset.Now.ToUnixTimeSeconds(), 1, 0); if (vals != null && vals.Length > 0)
{
var val = vals[0]; var rmCount = await RedisHelper.ZRemAsync(cacheKey, vals); if (rmCount > 0)
{
// 要把这个元素先删除成功了,再执行任务,不然会重复
Console.WriteLine($"{DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss")} begin to do task {val}"); // 回传处理结果给第三方,这里可以考虑这个并发锁,避免多实例都处理了这个任务。
// ....
}
}
else
{
// 没有数据,休眠500ms,避免CPU空转
await Task.Delay(500);
}
}
}
}

效果如下:

参考文章

https://redis.io/topics/notifications

https://zhuanlan.zhihu.com/p/87113913

本文首发于我的个人公众号,欢迎大家关注

借助Redis完成延时任务

借助Redis完成延时任务的更多相关文章

  1. 借助Redis做秒杀和限流的思考

    最近群里聊起秒杀和限流,我自己没有做过类似应用,但是工作中遇到过更大的数据和并发. 于是提出了一个简单的模型: var count = rds.inc(key); if(count > 1000 ...

  2. Redis简单延时队列

    Redis实现简单延队列, 利用zset有序的数据结构, score设置为延时的时间戳. 实现思路: 1.使用命令 [zrangebyscore keyName socreMin socreMax] ...

  3. 180713-Spring之借助Redis设计访问计数器之扩展篇

    之前写了一篇博文,简单的介绍了下如何利用Redis配合Spring搭建一个web的访问计数器,之前的内容比较初级,现在考虑对其进行扩展,新增访问者记录 记录当前站点的总访问人数(根据Ip或则设备号) ...

  4. 180626-Spring之借助Redis设计一个简单访问计数器

    文章链接:https://liuyueyi.github.io/hexblog/2018/06/26/180626-Spring之借助Redis设计一个简单访问计数器/ Spring之借助Redis设 ...

  5. 使用Redis实现延时任务(二)

    前提 前一篇文章通过Redis的有序集合Sorted Set和调度框架Quartz实例一版简单的延时任务,但是有两个相对重要的问题没有解决: 分片. 监控. 这篇文章的内容就是要完善这两个方面的功能. ...

  6. 【Visio流程图】借助redis来实现数据即时刷新

    [需求:]数据从竞品网站爬过来,经过分析处理之后,把结果通过网页实时反馈给业务人员. [应用:]2个应用: 一个是爬取数据的应用:不断从竞品网站爬数据,每次爬到的数据为一批.然后,对每一批爬到的数据进 ...

  7. 用 Redis 实现延时任务

    原文:https://cloud.tencent.com/developer/article/1358266 1.什么是延时任务 延时任务,顾名思义,就是延迟一段时间后才执行的任务.延时任务的使用还是 ...

  8. 使用Flask开发简单接口(4)--借助Redis实现token验证

    前言 在之前我们已开发了几个接口,并且可以正常使用,那么今天我们将继续完善一下.我们注意到之前的接口,都是不需要进行任何验证就可以使用的,其实我们可以使用 token ,比如设置在修改或删除用户信息的 ...

  9. 基于Redis实现延时队列服务

    背景 在业务发展过程中,会出现一些需要延时处理的场景,比如: a.订单下单之后超过30分钟用户未支付,需要取消订单 b.订单一些评论,如果48h用户未对商家评论,系统会自动产生一条默认评论 c.点我达 ...

随机推荐

  1. Java多线程基础详解

    基础概念进程进程是操作系统结构的基础:是一次程序的执行:是一个程序及其数据在处理机上顺序执行时所发生的活动.操作系统中,几乎所有运行中的任务对应一条进程(Process).一个程序进入内存运行,即变成 ...

  2. WIN10 蓝牙连接音箱之后,音量调节无效,音量从1-100,声音一样大,都是最大声,可以静音(解决方案)

    1.win+r,输入regedit,打开注册表2.进入路径:计算机\HKEY_LOCAL_MACHINE\SYSTEM\ControlSet001\Control\Bluetooth\Audio\AV ...

  3. mac osx 下 浏览器 开启 java

    工作环境mac osx 浏览器 chrome:63.0.3239.132 (Official Build) (64-bit)firefox: 57.0.4 (64 位)safari:Version 1 ...

  4. Java POI导出Excel不弹框选择下载路径(下载文件不选择下载路径,默认) Chrome

    在Chrome浏览器中,Java导出Excel文件时,浏览器弹出提示框,需要选择下载路径 在Chrome中的高级设置中,把“下载前询问每个文件的保存位置”去掉就解决了 DEEPLOVE(LC)

  5. 思科WLC5508上传定制Portal展示模版

    1. 登录Cisco设备,获取模板样例登录cisco WLC设备后点击help,打开帮助文档Wireless Tab-->Web Login Page-->External Web Aut ...

  6. Janet Wu price

    上次也是第一次参加百公里是2012的时候,那年的主题是一路有你,和一群同事从深圳湾走到福田,最后累了就回家了,那晚应该是睡得很好吧. 今年是提前报名了,虽然还不确定是否参加.因为说实话,我不喜欢拥堵的 ...

  7. Python 爬虫 selenium 笔记

    1. selenium 安装, 与文档 pip install selenium Selenium with Python中文翻译文档 selenium官网英文文档 2. selenium 的第一个示 ...

  8. Centos 7 中 部署 asp.net core 3.0 + nginx + mongodb 的一些新手简单入门,非docker

    目录 零.准备工作 一.部署Mongodb 1.安装Mongodb 2.创建mongodb的数据目录 3.设置目录权限 4.设置mongodb启动 5.修改mongodb的配置文件 6.启动Mongo ...

  9. p标签内不能嵌套块级标签

    今天突然发现一个问题,那就是p标签内不能嵌套块级标签 例如: <p><p></p></p> 会被浏览器解析成 我又把 div 嵌套在里面,发现还是这样 ...

  10. 课题:html5图像羽化(不规则区域羽化,feather,html5羽化)

    下午搜索了一堆相关文章,没有找到符合要求的. 对一张图片应用不规则区域的羽化,该怎么做呢? 首先去查了下 羽化的原理,然而没有什么用, 然后就开始从表现层去研究怎么模拟? idea 1: blur滤镜 ...