使用 Topshelf 结合 Quartz.NET 创建 Windows 服务

Ø  前言

之前一篇文章已经介绍了,如何使用 Topshelf 创建 Windows 服务。当时提到还缺少一个任务调度框架,就是 Quartz.NET。而本文就展开对 Quartz.NET 的研究,以及如何使用 Topshelf 结合 Quartz.NET 运行一个定时的 Windows 服务。

 

Ø  本文主要内容

1.   搭建 Topshelf 的运行环境。

2.   编写一个存储过程,用于更新商品表中的库存。

3.   安装 Quartz 所需的 dll 文件。

4.   创建 Quartz 的配置文件。

5.   创建 Windwos 服务调度程序。

6.   创建作业类,实现 IJob 接口。

7.   开启 Windows 服务。

 

1.   搭建 Topshelf 的运行环境

1)   创建一个控制台应用程序。

2)   添加 Topshelf 相关的 dll 的引用,可参考使用 Topshelf 创建 Windows 服务

 

2.   编写一个存储过程,用于更新商品表中的库存

1)   首先,创建一张商品表 Goods

IF(OBJECT_ID('Goods', 'U') IS NOT NULL)

    DROP TABLE Goods;

GO

CREATE TABLE Goods

(

    Id int IDENTITY(1, 1) NOT NULL,

    Name nvarchar(30) NOT NULL,

    Inventory int NOT NULL

    CONSTRAINT PK_Goods_Id PRIMARY KEY CLUSTERED

    (

        Id ASC

    ) ON [PRIMARY]

) ON [PRIMARY];

INSERT INTO Goods VALUES('大米', 0),('香蕉', 0),('苹果', 0);

SELECT * FROM Goods;

clip_image001[1]

 

2)   然后,创建存储过程 proc_UpdateInventory

IF(OBJECT_ID('proc_UpdateInventory', 'P') IS NOT NULL)

    DROP PROCEDURE proc_UpdateInventory;

GO

CREATE PROCEDURE proc_UpdateInventory(@GoodsId int, @Inventory int)

AS

    UPDATE Goods SET Inventory=@Inventory WHERE Id=@GoodsId;

GO

 

3.   安装 Quartz 所需的 dll 文件

1)   安装 Quartz,控制台输入:Install-Package Quartz

2)   安装 Common.Logging.Log4Net1211,控制台输入:Install-Package Common.Logging.Log4Net1211

3)   安装成功后,将看到如下图的引用及配置:

clip_image002[1]

 

4.   创建 Quartz 的配置文件

Ø  注意:必须将以下配置文件的“复制到输出目录”设置为始终复制。

Ø  关于 Quartz 的配置可参考:Quartz.NET 配置文件详解

1)   创建 quartz.config 文件,编辑内容:

# You can configure your scheduler in either<quartz> configuration section

# or in quartz properties file

# Configuration section has precedence

 

quartz.scheduler.instanceName = TopshelfAndQuartz

 

# configure thread pool info

quartz.threadPool.type = Quartz.Simpl.SimpleThreadPool, Quartz

quartz.threadPool.threadCount = 10

quartz.threadPool.threadPriority = Normal

 

# job initialization plugin handles our xml reading, without it defaults are used

quartz.plugin.xml.type = Quartz.Plugin.Xml.XMLSchedulingDataProcessorPlugin, Quartz

quartz.plugin.xml.fileNames = ~/quartz_jobs.xml

 

# export this server to remoting context

#quartz.scheduler.exporter.type = Quartz.Simpl.RemotingSchedulerExporter, Quartz

#quartz.scheduler.exporter.port = 555

#quartz.scheduler.exporter.bindName = QuartzScheduler

#quartz.scheduler.exporter.channelType = tcp

#quartz.scheduler.exporter.channelName = httpQuartz

 

2)   创建 quartz_jobs.xml 文件,编辑内容:

<?xml version="1.0" encoding="UTF-8"?>

 

<!-- This file contains job definitions in schema version 2.0 format -->

<job-scheduling-data xmlns="http://quartznet.sourceforge.net/JobSchedulingData" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="2.0">

 

  <processing-directives>

    <overwrite-existing-data>true</overwrite-existing-data>

  </processing-directives>

 

  <schedule>

    <!--该作业用于定时更新商品库存-->

    <job>

      <name>UpdateInventoryJob</name>

      <group>Update</group>

      <description>定时更新商品库存</description>

      <job-type>TopshelfAndQuartz.UpdateInventoryJob,TopshelfAndQuartz</job-type>

      <durable>true</durable>

      <recover>false</recover>

    </job>

    <trigger>

      <cron>

        <name>UpdateInventoryTrigger</name>

        <group>Update</group>

        <job-name>UpdateInventoryJob</job-name>

        <job-group>Update</job-group>

        <start-time>2017-12-01T00:00:00+08:00</start-time>

        <cron-expression>0 0/1 * * * ?</cron-expression>

      </cron>

    </trigger>

  </schedule>

 

</job-scheduling-data>

Ø  该文件用于定义每个作业及触发行为。

 

5.   创建 Windwos 服务调度程序

4)   这里的调度程序是指,当 Windows 服务启动或停止后,通知 Quartz 调度程序做响应的操作,代码如下:

/// <summary>

/// 服务运行时。

/// </summary>

public class ServiceRunner : ServiceControl, ServiceSuspend

{

    private readonly IScheduler Scheduler = StdSchedulerFactory.GetDefaultScheduler();

 

    public bool Start(HostControl hostControl)

    {

        //开始调度作业

        Scheduler.Start();

        Log.Logger.Info("开始调度作业");

        return true;

    }

 

    public bool Stop(HostControl hostControl)

    {

        //停止调度作业

        Scheduler.Shutdown(false);  //false: 表示当停止服务时,所有正则在执行的作业也立即停止

        Log.Logger.Info("停止调度作业");

        return true;

    }

 

    public bool Continue(HostControl hostControl)

    {

        //所有调度作业重新开始

        Scheduler.ResumeAll();

        Log.Logger.Info("所有调度作业重新开始");

        return true;

    }

 

    public bool Pause(HostControl hostControl)

    {

        //暂停所有调度作业

        Scheduler.ResumeAll();

        Log.Logger.Info("暂停所有调度作业");

        return true;

    }

}

5)   注意:只有实现了 ServiceSuspend 接口,服务才支持暂停与恢复操作,否则会报错。

 

6.   创建作业类,实现 IJob 接口

6)   当作业被触发时,将调用对应作业的 Execute() 方法。

/// <summary>

/// 更新库存作业。

/// </summary>

public class UpdateInventoryJob : IJob

{

    /// <summary>

    /// 作业被触发时执行该方法。

    /// </summary>

    public void Execute(IJobExecutionContext context)

    {

        try

        {

            //模拟调用存储过程,更新商品库存

            string connStr = @"Data Source=127.0.0.1\MYMSSQLSERVER08;Initial Catalog=MyDB;Persist Security Info=True;User ID=sa;Password=xxxxxx;";

            using (SqlConnection conn = new SqlConnection(connStr))

            {

                using (SqlCommand cmd = new SqlCommand())

                {

                    conn.Open();

                    cmd.Connection = conn;

                    cmd.CommandType = CommandType.StoredProcedure;

                    cmd.CommandText = "proc_UpdateInventory";

                    Random random = new Random();

                    SqlParameter[] paras = new SqlParameter[]

                    {

                        new SqlParameter()

                        {

                            ParameterName = "@GoodsId",

                            SqlDbType = SqlDbType.Int,

                            Value =  random.Next(1, 4)

                        },

                        new SqlParameter()

                        {

                            ParameterName = "@Inventory",

                            SqlDbType = SqlDbType.Int,

                            Value = random.Next(1, 100)

                        }

                    };

                    cmd.Parameters.AddRange(paras);

                    int rowCount = cmd.ExecuteNonQuery();   //exec proc_UpdateInventory @GoodsId=1,@Inventory=25

                    if (rowCount > 0)

                        Log.Logger.InfoFormat("商品:{0},库存已更新,新的库存为:{1}", paras[0].Value, paras[1].Value);

                    else

                        Log.Logger.InfoFormat("更新商品库失败,无受影响记录:{0}", rowCount);

                }

            }

        }

        catch (Exception ex)

        {

            Log.Logger.ErrorFormat("UpdateInventoryJob 作业执行异常:{0}", ex);

        }

    }

}

 

7.   开启 Windows 服务

1)   首先在 Main() 方法中加入如下代码:

static void Main(string[] args)

{

    var configFile = new FileInfo(Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "log4net.config"));

    log4net.Config.XmlConfigurator.ConfigureAndWatch(configFile);

 

    Log.Logger.Info("服务开始运行");

    HostFactory.Run(o =>

    {

        //o.UseLog4Net(); //这里需要使用 log4net, Version=1.2.15.0 的版本,当前版本不兼容所以注释掉

        o.Service<ServiceRunner>();

        o.SetServiceName("TopshelfAndQuartzService");

        o.SetDisplayName("库存更新服务");

        o.SetDescription("该服务用于定时更新商品库存");

        o.EnablePauseAndContinue();

    });

}

2)   安装服务并启动

安装服务的具体操作可参考:使用 Topshelf 创建 Windows 服务

3)   启动服务后,等待3分钟,依次对服务进行暂停 -> 恢复 -> 停止操作,将看到如下结果:

1.   Log

clip_image003[1]

2.   Data

clip_image004[1]

 

Ø  总结

本文,使用 Topshelf 结合 Quartz 搭建了一个 Windows 服务,用于定时调用存储过程更新商品库存。可见 Topshelf Quartz 的结合是天衣无缝,非常适合在平时的开发工作中。

posted @ 2017-12-15 13:01  Abeam  阅读(3266)  评论(1编辑  收藏  举报