最近遇到了这样的场景:每隔一段时间,需要在后台使用队列对一批数据进行业务处理。

Quartz.NET是一种选择,在 .NET Core中,可以使用IHostedService执行后台定时任务。在本篇中,首先尝试把队列还原到最简单、原始的状态,然后给出以上场景问题的具体解决方案。

假设一个队列有8个元素。现在abcd依次进入队列。

0 1 2 3 4 5 6 7
a b c d
head tail

ab依次出队列。

0 1 2 3 4 5 6 7
c d
head tail

可以想象,随着不断地入列出列,head和tail的位置不断往后,当tail在7号位的时候,虽然队列里还有空间,但此时数据就无法入队列了。

如何才可以继续入队列呢

首先想到的是数据搬移。当数据无法进入队列,首先让队列项出列,进入到另外一个新队列,这个新队列就可以再次接收数据入队列了。但是,搬移整个队列中的数据的时间复杂度为O(n),而原先出队列的时间复杂度是O(1),这种方式不够理想。

还有一种思路是使用循环队列。当tail指向最后一个位置,此时有新的数据进入队列,tail就来到头部指向0号位置,这样这个队列就可以循环使用了。

0 1 2 3 4 5 6 7
h i j
head tail

现在a入栈。

0 1 2 3 4 5 6 7
h i j a
tail head

队列有很多种实现

比如在生产消费模型中可以用阻塞队列。当生产队列为空的时候,为了不让消费者取数据,生产队列的Dequeue行为会被阻塞;而当生产队列满的时候,为了不让更多的数据进来,生产队列的Enqueue行为被阻塞。

线程安全的队列叫并发队列,如C#中的ConcurrentQueue

线程池内部也使用了队列机制。因为CPU的资源是有限的,过多的线程会导致CPU频繁地在线程之间切换。线程池内通过维护一定数量的线程来减轻CPU的负担。当线程池没有多余的线程可供使用,请求过来,一种方式是拒绝请求,另外一种方式是让请求队列阻塞,等到线程池内有线程可供使用,请求队列出列执行。用链表实现的队列是无界队列(unbounded queue),这种做法可能会导致过多的请求在排队,等待响应时间过长。用数组实现的队列是有界队列(bounded queue),当线程池已满,请求过来就会被拒绝。对有界队列来说数组的大小设置很讲究。

来模拟一个数组队列。

public class ArrayQueue
{
private string[] items;
private int n = 0; //数组长度
private int head = 0;
private int tail = 0; public ArrayQueue(int capacity)
{
n = capacity;
items = new string[capacity];
} public bool Enqueue(string item)
{
if(tail==n){
return false;
}
items[tail] = item;
++tail;
return true;
} public string Dequeue()
{
if(head==null){
return null;
}
string ret = items[head];
++head;
return ret;
}
}

以上就是一个最简单的、用数组实现的队列。

再次回到要解决的场景问题。解决思路大致是:实现IHostedService接口,在其中执行定时任务,每次把队列项放到队列中,并定义出队列的方法,在其中执行业务逻辑。

关于队列,通过以下的步骤使其在后台运行。

  • 队列项(MessageQueueItem):具备唯一标识、委托、添加到队列中的时间等属性
  • 队列(MessageQueue):维护着Dictionary<string, MessageQueueItem>静态字典集合
  • MessageQueueUtility类:决定着如何运行,比如队列执行的间隔时间、垃圾回收
  • MessageQueueThreadUtility类:维护队列线程,提供队列在后台运行的方法
  • Startup.cs中的Configure中调用MessageQueueThreadUtility中的方法使队列在后台运行

队列项(MessageQueueItem)

public class MessageQueueItem
{
public MessageQueueItem(string key, Action action, string description=null)
{
Key = key;
Action = action;
Description = description;
AddTime = DateTime.Now;
} public string Key{get;set;}
public Actioin Action{get;set;}
public DateTime AddTime{get;set;}
public string Description{get;set;}
}

队列(MessageQueue),维护着针对队列项的一个静态字典集合。

public class MessageQueue
{
public static Dictionary<string, MessageQueueItem> MessageQueueDictionary = new Dictionary<string, MessageQueueItem>(StringComparer.OrdinalIgnoreCase); public static object MessageQueueSyncLock = new object();
public static object OperateLock = new object(); public static void OperateQueue()
{
lock(OperateLock)
{
var mq = new MessageQueue();
var key = mq.GetCurrentKey();
while(!string.IsNullOrEmpty(key))
{
var mqItem = mq.GetItem(key);
mqItem.Action();
mq.Remove(key);
key = mq.GetCurrentKey();
}
}
} public string GetCurrentKey()
{
lock(MessageQueueSyncLock)
{
return MessageQueueDictionary.Keys.FirstOrDefault();
}
} public MessageQueueItem GetItem(string key)
{
lock(MessageQueueSyncLock)
{
if(MessageQueueDictionary.ContainsKey(key))
{
return MessageQueueDictionary[key];
}
return null;
}
} public void Remove(string key)
{
lock(MessageQueueSyncLock)
{
if(MessageQueueDictionary.ContainsKey(key))
{
MessageQueueDictionary.Remove(key);
}
}
} public MessageQueueItem Add(string key, Action actioin)
{
lock(MessageQueueSyncLock)
{
var mqItem = new MessageQueueItem(key, action);
MessageQueueDictionary[key] = mqItem;
return mqItem;
}
} public int GetCount()
{
lock(MessageQueueSyncLock)
{
return MessageQueueDictionary.Count;
}
}
}

MessageQueueUtility类, 决定着队列运行的节奏。

public class MessageQueueUtility
{
private readonly int _sleepMilliSeconds;
public MessageQueueUtility(int sleepMilliSeconds=1000)
{
_sleepMilliSeconds = sleepMilliSeoncds;
} ~MessageQueueUtility()
{
MessageQueue.OperateQueue();
} public void Run
{
do
{
MessageQueue.OperateQueue();
Thread.Sleep(_sleepMilliSeconds);
} while(true)
}
}

MessageQueueThreadUtility类,管理队列的线程,并让其在后台运行。

public static class MessageQueueThreadUtility
{
public static Dictionary<string, Thread> AsyncThreadCollection = new Dictioanry<string, Thread>();
public static void Register(string threadUniqueName)
{
{
MessageQueueUtility messageQueueUtility = new MessageQueueUtility();
Thread messageQueueThread = new Thread(messageQueueUtility.Run){
Name = threadUniqueName
};
AsyncThreadCollection.Add(messageQueueThread.Name, messageQueueThread);
} AsyncThreadCollection.Values.ToList().ForEach(z => {
z.IsBackground = true;
z.Start();
});
}
}

Startup.cs中注册。

public class Startup
{
public IServiceProvider ConfigureServices(IServiceCollection services)
{
...
} public void Configure(IApplicationBuilder app, IHostingEnvironment env...)
{
RegisterMessageQueueThreads();
} private void RegisterMessageQueueThreads()
{
MessageQueueThreadUtility.Register("");
}
}

最后在IHostedService的实现类中把队列项丢给队列。

public class MyBackgroundSerivce : IHostedService, IDisposable
{
private Timer _timer;
public IServiceProvider Services{get;} public MyBackgroundService(IServiceProvider services)
{
Serivces = services;
} public void Dispose()
{
_timer?.Dispose();
} public Task StartAsync(CancellationToken cancellationToken)
{
_timer = new Timer(DoWork, null, TimeSpan.Zero, TimeSpan.FromSeconds(10));
return Task.CompletedTask;
} public Task StopAsync(CancellationToken cancellationToken)
{
_timer?.Change(Timeout.Infinite,0);
return Task.CompletedTask;
} private void DoWork(object state)
{
using(var scope = Services.CreateScope())
{
using(var db = scope.ServiceProvider.GetRequiredService<MyDbContext>())
{
...
var mq = new MessageQueue();
mq.Add("somekey", DealQueueItem);
}
}
} private void DealQueueItem()
{
var mq = new MessageQueue();
var key = mq.GetCurrentKey();
var item = mq.GetItem(key);
if(item!=null)
{
using(var scope = Services.CreateScope())
{
using(var db = scope.ServiceProvider.GetRequiredService<MyDbContext>())
{
//执行业务逻辑
}
}
}
}
}

当需要使用上下文的时候,首先通过IServiceProviderCreateScope方法得到ISerivceScope,再通过它的ServiceProvider属性获取依赖倒置容器中的上下文服务。

以上,用IHostedService结合队列解决了开篇提到的场景问题,如果您有很好的想法,我们一起交流吧。文中的队列部分来自"盛派网络"的Senparc.Weixin SDK源码。

.NET Core中使用IHostedService结合队列执行定时任务的更多相关文章

  1. java中服务器启动时,执行定时任务

    package com.ripsoft.util; import java.util.Calendar; import java.util.Timer; import javax.servlet.Se ...

  2. .NET Core 中基于 IHostedService 实现后台定时任务

    .NET Core 2.0 引入了 IHostedService ,基于它可以很方便地执行后台任务,.NET Core 2.1 则锦上添花地提供了 IHostedService 的默认实现基类 Bac ...

  3. WSL2+Docker部署RabbitMQ以及在Asp.net core 中使用RabbitMQ示例(1)

    本文主要在于最近因疫情不能外出,在家研究的一些技术积累. 主要用到的技术以及知识点: WSL 2 WSL 2+Docker Docker+RabbitMQ 在ASP.NET Core中使用Rabbit ...

  4. .NET Core中使用RabbitMQ正确方式

    .NET Core中使用RabbitMQ正确方式 首先甩官网:http://www.rabbitmq.com/ 然后是.NET Client链接:http://www.rabbitmq.com/dot ...

  5. 在ASP.NET Core中创建基于Quartz.NET托管服务轻松实现作业调度

    在这篇文章中,我将介绍如何使用ASP.NET Core托管服务运行Quartz.NET作业.这样的好处是我们可以在应用程序启动和停止时很方便的来控制我们的Job的运行状态.接下来我将演示如何创建一个简 ...

  6. 如何在Spring Boot 中动态设定与执行定时任务

    本篇文章的目的是记录并实现在Spring Boot中,动态设定与执行定时任务. 我的开发项目是 Maven 项目,所以首先需要在 pom.xml 文件中加入相关的依赖.依赖代码如下所示: <de ...

  7. 『学了就忘』Linux系统定时任务 — 88、循环执行定时任务

    目录 1.crond服务管理与访问控制 2.crontab命令的访问控制 3.用户级别的crontab命令 4.crontab命令的注意事项 5.系统的crontab设置 (1)/etc/cronta ...

  8. 使用Redis Stream来做消息队列和在Asp.Net Core中的实现

    写在前面 我一直以来使用redis的时候,很多低烈度需求(并发要求不是很高)需要用到消息队列的时候,在项目本身已经使用了Redis的情况下都想直接用Redis来做消息队列,而不想引入新的服务,kafk ...

  9. 在 ASP.NET Core 中执行租户服务

    在 ASP.NET Core 中执行租户服务 不定时更新翻译系列,此系列更新毫无时间规律,文笔菜翻译菜求各位看官老爷们轻喷,如觉得我翻译有问题请挪步原博客地址 本博文翻译自: http://gunna ...

随机推荐

  1. 【前端基础系列】slice方法将类数组转换数组实现原理

    问题描述 在日常编码中会遇到将类数组对象转换为数组的问题,其中常用到的一种方式使用Array.prototype.slice()方法. 类数组对象 所谓的类数组对象,JavaScript对它们定义为: ...

  2. 【Android】ScaleAnimation 详解

    ScaleAnimation类是Android系统中的尺寸变化动画类,用于控制View对象的尺寸变化,该类继承于Animation类.ScaleAnimation类中的很多方法都与Animation类 ...

  3. 实际生产用法CMS和G1

    java -Xms100m -Xmx100m -Xmn50m -XX:MetaspaceSize=20m -XX:MaxMetaspaceSize=20m -XX:+UseConcMarkSweepG ...

  4. Python学习(八) —— 内置函数和匿名函数

    一.递归函数 定义:在一个函数里调用这个函数本身 递归的最大深度:997 def func(n): print(n) n += 1 func(n) func(1) 测试递归最大深度 import sy ...

  5. P1052 过河 线性dp 路径压缩

    题目描述 在河上有一座独木桥,一只青蛙想沿着独木桥从河的一侧跳到另一侧.在桥上有一些石子,青蛙很讨厌踩在这些石子上.由于桥的长度和青蛙一次跳过的距离都是正整数,我们可以把独木桥上青蛙可能到达的点看成数 ...

  6. 068 mapWithState函数的讲解

    1.问题 主要是updateStateByKey的问题 有的值不需要变化的时候,还会再打印出来. 每个批次的数据都会出现,如果向redis保存更新的时候,会把不需要变化的值也更新,这个不是我们需要的, ...

  7. Python 扫盲

    yield使用浅析 菜鸟教程:http://www.runoob.com/w3cnote/python-yield-used-analysis.html #!/usr/bin/python # -*- ...

  8. QT pyqt pyside2 QLabel 自动换行设置注意点

    QT pyqt pyside2 QLabel 自动换行设置注意点 先看效果: PySide2(QT5) 的 QT Designer (我在 QT4 的 designer 中不可以直接看效果,可能需要设 ...

  9. Codeforces 861D - Polycarp's phone book 【Trie树】

    <题目链接> 题目大意: 输入7e4个长度为9的字符串,每个字符串中只出现0~9这几种数字,现在需要你输出每个母串中最短的特有子串. 解题分析: 利用Trie树进行公共子串的判定,因为Tr ...

  10. Java NIO- 最好文档

    http://www.cnblogs.com/puyangsky/p/5840873.html 1 背景介绍 在上一篇文章中我们介绍了Java基本IO,也就是阻塞式IO(BIO),在JDK1.4版本后 ...