背景

  工作中经常会有定时任务的需求,常见的做法可以使用Timer、Quartz、Hangfire等组件,这次想尝试下新的思路,使用RabbitMQ死信队列的机制来实现定时任务,同时帮助再次了解RabbitMQ的死信队列。

交互流程

  

  1. 用户创建定时任务

  2. 往死信队列插入一条消息,并设置过期时间为首个任务执行时间

  3. 死信队列中的消息过期后,消息流向工作队列

  4. 任务执行消费者监听工作队列,工作队列向消费者推送消息

  5. 消费者查询数据库,读取任务信息

  6. 消费者确认任务有效(未被撤销),执行任务

  7. 消费者确认有下个任务,再往死信队列插入一条消息,并设置过期时间为任务执行时间

  8. 重复2-7的步骤,直到所有任务执行完成或任务撤销

环境准备

  请自行完成MongoDB和RabbitMQ的安装,Windows、Linux、Docker皆可,以下提供Windows的安装方法:

  MongoDB:https://docs.mongodb.com/manual/tutorial/install-mongodb-on-windows/

  RabbitMQ:https://www.rabbitmq.com/install-windows.html

核心代码

  1. (WebApi)创建任务,并根据设置创建子任务,把任务数据写入数据库

    var task = new Task
{
Name = form.Name,
StartTime = form.StartTime,
EndTime = form.EndTime,
Interval = form.Interval,
SubTasks = new List<SubTask>()
}; var startTime = task.StartTime;
var endTime = task.EndTime; while ((endTime - startTime).TotalMinutes >= )
{
var sendTime = startTime;
if (sendTime <= endTime && sendTime > DateTime.UtcNow)
{
task.SubTasks.Add(new SubTask { Id = ObjectId.GenerateNewId(), SendTime = sendTime });
} startTime = startTime.AddMinutes(task.Interval);
} await _mongoDbContext.Collection<Task>().InsertOneAsync(task);

  2. (WebApi)往死信队列中写入消息

    var timeFlag = task.SubTasks[].SendTime.ToString("yyyy-MM-dd HH:mm:ssZ");
var exchange = "Task";
var queue = "Task"; var index = ;
var pendingExchange = "PendingTask";
var pendingQueue = $"PendingTask|Task:{task.Id}_{index}_{timeFlag}"; using (var channel = _rabbitConnection.CreateModel())
{
channel.ExchangeDeclare(exchange, "direct", true);
channel.QueueDeclare(queue, true, false, false);
channel.QueueBind(queue, exchange, queue); var retryDic = new Dictionary<string, object>
{
{"x-dead-letter-exchange", exchange},
{"x-dead-letter-routing-key", queue}
}; channel.ExchangeDeclare(pendingExchange, "direct", true);
channel.QueueDeclare(pendingQueue, true, false, false, retryDic);
channel.QueueBind(pendingQueue, pendingExchange, pendingQueue); var properties = channel.CreateBasicProperties();
properties.Headers = new Dictionary<string, object>
{
["index"] = index,
["id"] = task.Id.ToString(),
["sendtime"] = timeFlag
}; properties.Expiration = ((int)(task.SubTasks[].SendTime - DateTime.UtcNow).TotalMilliseconds).ToString(CultureInfo.InvariantCulture);
channel.BasicPublish(pendingExchange, pendingQueue, properties, Encoding.UTF8.GetBytes(string.Empty));
}

  其中:

  PendingTask为死信队列Exchange,死信队列的队列名(Queue Name)会包含Task、index、timeFlag的信息,帮助跟踪队列和子任务,同时也起到唯一标识的作用。

  task.id为任务Id

  index为子任务下标

  timeFlag为子任务执行时间

  3. (消费者)处理消息

    var exchange = "Task";
var queue = "Task"; _channel.ExchangeDeclare(exchange, "direct", true);
_channel.QueueDeclare(queue, true, false, false);
_channel.QueueBind(queue, exchange, queue); var consumer = new EventingBasicConsumer(_channel);
   //监听处理
consumer.Received += (model, ea) =>
{
     //获取消息头信息
var index = (int)ea.BasicProperties.Headers["index"];
var id = (ea.BasicProperties.Headers["id"] as byte[]).BytesToString();
var timeFlag = (ea.BasicProperties.Headers["sendtime"] as byte[]).BytesToString();    //删除临时死信队列
_channel.QueueDelete($"PendingTask|Task:{id}_{index}_{timeFlag}", false, true); var taskId = new ObjectId(id);
var task = _mongoDbContext.Collection<Task>().Find(n => n.Id == taskId).SingleOrDefault();      //撤销或已完成的任务不执行
if (task == null || task.Status != TaskStatus.Normal)
{
_channel.BasicAck(ea.DeliveryTag, false);
return;
}

     //执行任务
_logger.LogInformation($"[{DateTime.UtcNow}]执行任务...");

//设置子任务已完成
task.SubTasks[index].IsSent = true; if (task.SubTasks.Count > index + ) //还有未完成的子任务,把下个子任务的信息写入死信队列
{
PublishPendingMsg(_channel, task, index + );
}
else
{
task.Status = TaskStatus.Finished; //所有子任务执行完毕,设置任务状态为完成
} _mongoDbContext.Collection<Task>().ReplaceOne(n => n.Id == taskId, task); //更新任务状态
_channel.BasicAck(ea.DeliveryTag, false);
};
_channel.BasicConsume(queue, false, consumer);

  4. (WebApi)撤销任务,更新任务状态即可

    var taskId = new ObjectId(id);
var task = await _mongoDbContext.Collection<Task>().Find(n => n.Id == taskId).SingleOrDefaultAsync();
if (task == null)
{
return NotFound(new { message = "任务不存在!" });
} task.Status = TaskStatus.Canceled;
await _mongoDbContext.Collection<Task>().FindOneAndReplaceAsync(n => n.Id == taskId, task);

效果展示

  1. 先使用控制台把消费者启动起来。

  

  2. 创建任务

  启动WebApi,创建一个任务,开始时间为2019-07-16T07:55:00.000Z,结束时间为2019-07-16T07:59:00.000Z,执行时间间隔1分钟:

  

  任务与相应的子任务也写入了MongoDB,这里假设子任务可能是邮件发送任务:

  

  

  创建了一个临时死信队列,队列名称包含任务Id,子任务下标、以及子任务执行时间,并往其写入一条消息:

  

  3. 执行(子)任务

  从日志内容可以看出,(子)任务正常执行:

  

  子任务状态也标注为已发送

  

  同时也往消息队列写入了下一个子任务的消息:

  

  

  4. 撤销任务

  

  任务状态被置为已撤销:

  

  任务没再继续往下执行:

  

  

  消息队列中的临时队列被删除,消息也被消费完

  

源码地址

  https://github.com/ErikXu/rabbit-scheduler

[奇思异想]使用RabbitMQ实现定时任务的更多相关文章

  1. [奇思异想]使用Zookeeper管理数据库连接串

    背景 有一套特定规格的应用(程序+数据库),当有业务需求时,就需要多部署应用,并且所有的应用都使用一个共同的后台来管理.应用新增后,如何通知后台更新连接串成了一个关键的问题.于是就产生了使用ZooKe ...

  2. c# 扩展方法奇思妙用基础篇八:Distinct 扩展(转载)

    转载地址:http://www.cnblogs.com/ldp615/archive/2011/08/01/distinct-entension.html 刚看了篇文章 <Linq的Distin ...

  3. c# 扩展方法奇思妙用

    # 扩展方法出来已久,介绍扩展方法的文章也很多,但都是笼统的.本人最近一直在思考扩展方法的应用,也悟出了一些,准备将这最近一段时间对扩展方法的思考,写成一个系列文章.每个文章只介绍一个应用方面,篇幅不 ...

  4. c# 扩展方法奇思妙用集锦

    本文转载:http://www.cnblogs.com/ldp615/archive/2009/08/07/1541404.html 其中本人觉得很经典的:c# 扩展方法奇思妙用基础篇五:Dictio ...

  5. c# 扩展方法奇思妙用基础篇八:Distinct 扩展

    刚看了篇文章 <Linq的Distinct太不给力了>,文中给出了一个解决办法,略显复杂. 试想如果能写成下面的样子,是不是更简单优雅 var p1 = products.Distinct ...

  6. C#通过rabbitmq实现定时任务(延时队列)

    本文主要讲解如何通过RabbitMQ实现定时任务(延时队列) 环境准备 需要在MQ中进行安装插件 地址链接 插件介绍地址:https://www.rabbitmq.com/blog/2015/04/1 ...

  7. 异想-天开 python---while、for、if-else 循环学习

    for循环: for i in range(10): # i循环10次 print('------',i) for j in range(10): print(j) if j > 2 : bre ...

  8. c# 扩展方法 奇思妙用 高级篇 九:OrderBy(string propertyName, bool desc)

    下面是 Queryable 类 中最常用的两个排序的扩展方法: 1 2 public static IOrderedQueryable<TSource> OrderBy<TSourc ...

  9. 奇淫异巧之 PHP 后门

    整理大部分来源信安之路 对于隐蔽来说,有以下几点要素: 1.熟悉环境,模拟环境,适应环境,像一只变色龙一样隐藏 2.清除痕迹,避免运维发现 3.避免后门特征值被 D 盾等工具检测到 姿势 一般过狗思路 ...

随机推荐

  1. PRML Chapter3

    曲线拟合的几种方法 最大似然估计MLE,最大后验概率MAP:MLE和MAP MLE 给定一堆数据,假如我们知道它是从某一种分布中随机取出来的,可是我们并不知道这个分布具体的参,即"模型已定, ...

  2. 解决xp越来越慢的办法(其中有些自动备份的功能)

    1.减少磁盘空间占用2.终止不常用的系统服务3.安全问题4.另外一些技巧 首先问一下,你是不是很想激活XP,不...准确的说你是不是想在ms的站上能够升级.如果答案是肯定的话,那我们就先来探讨一下安装 ...

  3. SharePoint js操作原生的New/Edit表单

    列表的表单,有个类似的需求:在New需隐藏特定字段,Edit时显示. 默认是New/Edit表单的字段是一样,就算在Content type 是隐藏也是同时影响两个表单.   如何做到仅仅在New时隐 ...

  4. JavaWeb实现上传文件

    需要 commons-io与commons-fileupload 首先在jsp中创建一下布局 <%@ page contentType="text/html;charset=UTF-8 ...

  5. 使用C++还是QML(QML容易使用和维护,效果好)

    本质上,Qt 是一个C++类库.在引入 QML 以前,所有的开发都是基于 C++ 的,但到了 Qt 5,QML 和 Qt Quick 成为了 Qt 的核心之一,导致很多初学者在犹豫是否还需要学习 C+ ...

  6. Gradle添加外部项目代码

    为了测试一些功能,我想在公司的项目中引进外部项目的子模块代码进来调试,试验了好半天终于成功了... 原来不需要导入代码,只要在settings.gradle中这样就好了: 然后就可以和原项目中的代码一 ...

  7. Delphi各种从文件里读取内容的方法

    Hi I am having a problem running a function to read a text file the problem seems to be that my anti ...

  8. 安装Cloudera Impala

    安装Cloudera Impala Cloudera Impala是Cloudera Enterprise Core的开源扩展,用于快速返回查询结果. Impala作为你环境的插件,与其他组件的安装独 ...

  9. Unity Shader 菲涅尔环境反射

    菲涅尔反射描述了一种光学现象,当光照到物体表面时,一部分发生反射,另一部分则进入物体内部,发生折射或散射:相比直接的反射和折射计算,菲涅尔反射更接近真实情况. 可用下面的等式近似计算这种反射效果: F ...

  10. Angular4.0从入门到实战打造在线竞拍网站学习笔记之一--组件

    Angular4.0基础知识之组件 Angular4.0基础知识之路由 Angular4.0依赖注入 Angular4.0数据绑定&管道 最近搞到手了一部Angular4的视频教程,这几天正好 ...