有段日子没有更新,写点东西冒个泡 。这篇文章过来讲个小东西,也是大家在日常开发中也经常需要面临的问题:后台定时任务处理。估计大家看到这句就已经联想到 QuartZ 等类似第三方类库了,不好意思,后边的事情和它们没有关系。这里要展开的是用.Net Core 下的 Generic Host 配合封装简版定时任务处理框架的过程。至于什么是Generic Host,简单来说就是一个简化版不含Http管道等的非Web应用托管宿主服务,至于它如何来,其内有着什么样的实现细节,官方介绍已经足够。这篇文章主要还是回到实际的基础封装过程实现层面,用一个小东西来演示如何在常见业务代码中梳理职责,内容主要如下:

1.  概要分解

2.  封装实现

3.   示例演示

4.   注意事项

 一. 概要分解

  如果对Generic Host 已经有了解的同学可能也看过网上其他文章,大多也都介绍用它如何实现定时任务处理。这些文章基本提供了一个通用实现,对业务实现还是稍显啰嗦。这两天整理逻辑有个任务不得不临时定时处理,想到这个东西,花了点时间处理了下,东西不复杂不过还是想把这个思路分享给需要的朋友。

  定时任务,分解来看特别简单,就是两个维度“  定时 +  任务 ”,如果还有另外一个维度,那就是 任务运行的托管服务。在托管平台上添加定时规则,根据规则触发任务,工作结束。

  1.  关于定时,主要就是一套任务触发的规则,其作为一个调度者,只需要关心的是 在什么时间,以何种频率 触发任务。   在.Net 下我们通过定时器(Timer - 构造函数包含这两个核心参数,.net 下有两个Timer实现,一个是System.Timer.Timer,一个是System.Threading.Timer, 这里用的第二者,自由度更高)来实现,但是它不应该直接和具体的任务挂钩,使用方也不应该每次都自己来处理Timer的初始化及相关回收释放等相同操作,我们需要的是使用方只需告知框架层要执行什么任务,和任务对应的时间规则。

  2.  关于任务, 这个角色是一个任务的执行者, 定时调度者 告诉 任务执行者 在什么时候开始执行和结束任务,其本身不会关注调度的实现。

  3.  关于托管服务,也就是已经说过的Generic Host,当然你也可以使用windows服务等。它的职责就是保证给任务提供执行环境,并告诉任务定时器当前服务在什么时候开始运行和关闭。  实现时提供了统一 IHostedService  接口,具体实现下边实现会有展示。Generic Host 启动方式有两种形式:

    a. 如果是.NetCore 站点,默认已经包含,只需要在 ConfigureServices 时注册具体实现即可。

    b. 可以独立创建,比如控制台通过 new HostBuilder() 形式启动,具体参见官方文档。

  为了更直观的展示相关之间的关系,这里我画了个类图来分解相关的职责,同时也是后边具体实现的主要内容:

 二.  封装实现

  从上边类图可以看出当前基础框架主要由 BaseJobTrigger(触发器基类),IJobExcutor(任务执行者接口),ListJobExcutor<IType>(通用列表循环任务执行者基类)。下边分别就上边三者贴出具体实现。

  1.  BaseJobTrigger(触发器基类),实现代码如下:

public abstract class BaseJobTrigger
: IHostedService, IDisposable
{
private Timer _timer;
private readonly TimeSpan _dueTime;
private readonly TimeSpan _periodTime; private readonly IJobExecutor _jobExcutor; /// <summary>
/// 构造函数
/// </summary>
/// <param name="dueTime">到期执行时间</param>
/// <param name="periodTime">间隔时间</param>
/// <param name="jobExcutor">任务执行者</param>
protected BaseJobTrigger(TimeSpan dueTime,
TimeSpan periodTime,
IJobExecutor jobExcutor)
{
_dueTime = dueTime;
_periodTime = periodTime;
_jobExcutor = jobExcutor;
} #region 计时器相关方法 private void StartTimerTrigger()
{
if (_timer == null)
_timer = new Timer(ExcuteJob,_jobExcutor,_dueTime, _periodTime);
else
_timer.Change(_dueTime, _periodTime);
} private void StopTimerTrigger()
{
_timer?.Change(Timeout.Infinite, Timeout.Infinite);
} private void ExcuteJob(object obj)
{
try
{
var excutor = obj as IJobExecutor;
excutor?.StartJob();
}
catch (Exception e)
{
LogUtil.Error($"执行任务({nameof(GetType)})时出错,信息:{e}");
}
}
#endregion /// <summary>
/// 系统级任务执行启动
/// </summary>
/// <returns></returns>
public virtual Task StartAsync(CancellationToken cancellationToken)
{
try
{
StartTimerTrigger();
}
catch (Exception e)
{
LogUtil.Error($"启动定时任务({nameof(GetType)})时出错,信息:{e}");
}
return Task.CompletedTask;
} /// <summary>
/// 系统级任务执行关闭
/// </summary>
/// <returns></returns>
public virtual Task StopAsync(CancellationToken cancellationToken)
{
try
{
_jobExcutor.StopJob();
StopTimerTrigger();
}
catch (Exception e)
{
LogUtil.Error($"停止定时任务({nameof(GetType)})时出错,信息:{e}");
}
return Task.CompletedTask;
} public void Dispose()
{
_timer?.Dispose();
}
}

  这个主要是完成对定时器的封装,StartAsync和StopAsync 为 IHostService 系统服务接口,表示托管服务的开始和结束。

   2. IJobExcutor(任务执行者接口)

public interface IJobExecutor
{
/// <summary>
/// 开始任务
/// </summary>
void StartJob(); /// <summary>
/// 结束任务
/// </summary>
void StopJob();
}

  3. ListJobExcutor<IType>(通用列表循环任务执行者基类)

public abstract class ListJobExcutor<IType> 
                : IJobExecutor
{
/// <summary>
/// 运行状态
/// </summary>
public bool IsRuning { get;protected set; }
/// <summary>
/// 开始任务
/// </summary>
public void StartJob()
{
// 任务依然在执行中,不需要再次唤起
if (IsRuning)
return; IsRuning = true;
IList<IType> list = null; // 结清实体list
do
{
for (var i = 0; IsRuning && i < list?.Count;i++)
{
ExcuteItem(list[i],i);
}
list = GetExcuteSource(); } while (IsRuning && list?.Count > 0);
IsRuning = false;
}
public void StopJob()
{
IsRuning = false;
} /// <summary>
/// 获取list数据源
/// </summary>
/// <returns></returns>
protected virtual IList<IType> GetExcuteSource()
{
return null;
}
/// <summary>
/// 个体任务执行
/// </summary>
/// <param name="item">单个实体</param>
/// <param name="index">在数据源中的索引</param>
protected virtual void ExcuteItem(IType item,int index)
{
}
}

  这个是通用列表循环执的基础封装,因为业务中需要定时处理的大多是需要从数据库或文件批量获取数据,执行处理,例如到期提醒,定时清理超时订单等场景。

  其主要功能实现是 从 GetExcuteSource() 获取执行数据源,循环并通过 ExcuteItem() 执行个体任务,直到没有数据源返回,则此次任务执行结束,等待下次任务触发。如果当次执行时间过长,超过计时器时间间隔,重复触发时 当前任务还在进行中,则不做任何处理。如果数据量过大需要并发执行,子类可以在  ExcuteItem 中异步处理。这样既可保证并发顺序执行。

 三. 示例演示

  以上三个元素就构成了当前定时任务的主要基础框架,在实际处理一个任务的过程中,我们需要定义一个执行者(XXXJobExcutor),一个触发器(XXXJobTrigger,构造函数传入触发时间,间隔,执行者)即可。这里用两个示例来做演示

  1. 基础任务处理

public class TestJobTrigger:BaseJobTrigger
{
public TestJobTrigger() :
base(TimeSpan.Zero,
TimeSpan.FromMinutes(10),
new TestJobExcutor())
{
}
}
public class TestJobExcutor
: IJobExecutor
{
public void StartJob()
{
LogUtil.Info("执行任务!");
} public void StopJob()
{
LogUtil.Info("系统终止任务");
}
}

  以上实现了TestJobTrigger 做任务触发器,十分钟执行一次。TestJobExcutor 作为具体执行者,做任务处理。启动时只需在Startup.cs 中的ConfigureServices方法中添加如下代码即可:

services.AddHostedService<TestJobTrigger>();

  2.  列表循环处理

public class ListJobTrigger
: BaseJobTrigger
{
public ListJobTrigger() :
base(TimeSpan.Zero,
TimeSpan.FromMinutes(10),
new ListJobExcutor())
{ }
} public class ListJobExcutor
: ListJobExcutor<string>
{
private int _page = 0; protected override IList<string> GetExcuteSource()
{
if (_page==0)
{
_page++;
return new List<string>{ "1", "2", "3" };
}
return null;
} protected override void ExcuteItem(string item, int index)
{
LogUtil.Info(item);
}
}

  这个示例定时获取字符串列表,并打印。一样在Startup中注册即可。    

 四.   注意事项

  1. 关于何时使用定时任务的问题

   之所以要说这个问题,是因为我看过不少同学把定时任务这种方式当成万能胶,哪里有缝往哪贴,一个不行起两个。其实有很多场景都可以通过其关联事件加消息队列来完成,比如发短信,接收发送请求后塞消息队列并返回请求方接收成功,队列消费者来负责和短信服务商接口交互。只有对一些对时间属性有要求的处理,咱们通过定时任务等处理,如.....会员生日提醒....

  2. 关于框架元素在解决方案的引用放置

  一个建议: IJobExcutor,ListJobExcutor<IType> 可以放置在通用类库中,BaseJobTrigger,因为其依赖IHostService 放置在站点目录下比较合适。

  3. 关于GenericHost的生存周期问题

  如果你使用的是控制台启动,则此问题暂时可以忽略。

  如果你使用的是站点项目,并且还是通过IIS启动,那么你可能要注意了,因为.net core 的站点自身是有HOST宿主处理,IIS是其上代理,其启动关闭,端口映射等由IIS内部完成。所以其依然受限于IIS的闲置回收影响,当IIS闲置回收时,其后的.Net Host也会被一同关闭,需要有新的请求进来时才会再次启动。不过鉴于当前任务处理已经如此简单,有个取巧的做法,实现一个站点自身的心跳检测任务,IIS默认20分钟回收,任务时间可以设为15分钟(你也可以设置IIS站点回收时间),当然如果你的任务如果没有那么严格的时间要求你也可以不用处理,因为回收后一旦接受到新的请求,任务会再次发起。

  如果你已经看到这里,并且感觉还行的话可以在下方点个赞,或者也可以关注我的公总号(见二维码)

_________________________________________

.Net Core 简单定时任务框架封装的更多相关文章

  1. 仿照jQuery进行一些简单的框架封装(欢迎指教~)

    (function(window,undefined){ var arr = [], push = arr.push, slice = arr.slice; //首先要做的就是封装一个parseHtm ...

  2. .net core的定时任务框架Timed Job

    参考文档:http://www.1234.sh/post/pomelo-extensions-timed-jobs 在该文档中介绍了怎么使用timed job,但是在使用db的时候会发生错误,错误一般 ...

  3. .NET core Quartz 定时任务框架 demo

    开始先建个空的web项目. 创建一个新类 QuartzFactory 狠狠的复制就完事了. public class QuartzFactory : IJobFactory { private rea ...

  4. .NET 跨平台RPC框架DotNettyRPC Web后台快速开发框架(.NET Core) EasyWcf------无需配置,无需引用,动态绑定,轻松使用 C# .NET 0配置使用Wcf(半成品) C# .NET Socket 简单实用框架 C# .NET 0命令行安装Windows服务程序

    .NET 跨平台RPC框架DotNettyRPC   DotNettyRPC 1.简介 DotNettyRPC是一个基于DotNetty的跨平台RPC框架,支持.NET45以及.NET Standar ...

  5. 基于.NET CORE微服务框架 -surging的介绍和简单示例 (开源)

    一.前言 至今为止编程开发已经11个年头,从 VB6.0,ASP时代到ASP.NET再到MVC, 从中见证了.NET技术发展,从无畏无知的懵懂少年,到现在的中年大叔,从中的酸甜苦辣也只有本人自知.随着 ...

  6. Quartz.Net的使用(简单配置方法)定时任务框架

    Quartz.dll 安装nuget在线获取dll包管理器,从中获取最新版 Quartz.Net是一个定时任务框架,可以实现异常灵活的定时任务,开发人员只要编写少量的代码就可以实现“每隔1小时执行”. ...

  7. ASP.NET Core 使用 EF 框架查询数据 - ASP.NET Core 基础教程 - 简单教程,简单编程

    原文:ASP.NET Core 使用 EF 框架查询数据 - ASP.NET Core 基础教程 - 简单教程,简单编程 ASP.NET Core 使用 EF 框架查询数据 上一章节我们学习了如何设置 ...

  8. ASP.NET Core 配置 EF 框架服务 - ASP.NET Core 基础教程 - 简单教程,简单编程

    原文:ASP.NET Core 配置 EF 框架服务 - ASP.NET Core 基础教程 - 简单教程,简单编程 ASP.NET Core 配置 EF 框架服务 上一章节中我们了解了 Entity ...

  9. C# .NET Socket 简单实用框架,socket组件封装

    参考资料 https://www.cnblogs.com/coldairarrow/p/7501645.html 根据.NET Socket 简单实用框架进行了改造,这个代码对socket通信封装还是 ...

随机推荐

  1. python 实践项目 强密码检测

    需求:写一个函数,它使用正则表达式,确保传入的口令字符串是强口令.强口令的定义是:长度不少于 8 个字符,同时包含大写和小写字符,至少有一位数字.你可能需要用多个正则表达式来测试该字符串,以保证它的强 ...

  2. 使用jquery load遇到一个问题解决

    1.环境 动态加载页面后给页面中的控件动态赋值,赋完更新后一闪值就没了. 2.原因    load()方法是异步加载,页面没有加载完就开始赋值,导致情况出现. 3.解决   加载完成后赋值 $(&qu ...

  3. LOJ-10102(求A到B之间的割点)

    题目链接:传送门 思路:求A到B之间必要的中间节点 条件:(1)只有一条路径经过中间节点:(low[B]>=num[u]&&num[v]<=num[B],没有从B到u的路径 ...

  4. 第35章:MongoDB-集群--Master Slave(主从复制)

    ①主从复制 最基本的设置方式就是建立一个主节点和一个或多个从节点,每个从节点要知道主节点的地址.采用双机备份后主节点挂掉了后从节点可以接替主机继续服务,所以这种模式比单节点的高可用性要好很多. ②注意 ...

  5. Java ,python面向对象的继承及其区别

    JAVA JAVA继承基本样式 class Demo extends Object{ Demo(int a){ this(); } Demo(){ super(); } } java默认继承Objec ...

  6. Axure RP Xmind

    官方网站下载地址:http://www.axure.com/download 下载地址:http://www.iaxure.com/2941.html 汉化安装:http://www.iaxure.c ...

  7. 一个友盟BUG的思考和分析:Invalid update

    1.友盟错误信息 Invalid update: invalid number of rows . The number of rows contained ) must be equal to th ...

  8. 顺藤摸瓜:一个专黑建筑行业的QQ黏虫团伙现形记

    QQ粘虫是已经流行多年的盗号木马,它会伪装QQ登陆界面,诱骗受害者在钓鱼窗口提交账号密码.近期,360QVM引擎团队发现一支专门攻击建筑行业人群的QQ粘虫变种,它伪装为招标文档,专门在一些建筑/房产行 ...

  9. [SRC初探]手持新手卡挖SRC逻辑漏洞心得分享

    文章来源i春秋 本文适合新手参阅,大牛笑笑就好了,嘿嘿末尾有彩蛋!!!!!!!!!!!!!!!!!本人参加了本次"i春秋部落守卫者联盟"活动,由于经验不足,首次挖SRC,排名不是那 ...

  10. Yii2 三层设计模式:SQL Command、Query builder、Active Record(ORM)

    用Yii2也有一段时间了,发现Yii2 Framework对Database的操作有非常良好的结构和弹性. 接下来介绍三种数据库操作方式. SQL Command Level: // Get DB c ...