sqlserver搭建高可用雙機熱備的Quartz集群部署【附源碼】
一般拿Timer和Quartz相比較的,簡直就是對Quartz的侮辱,兩者的功能根本就不在一個層級上,如本篇介紹的Quartz強大的集群機制,可以采用基於
sqlserver,mysql的集群方案,當然還可以在第三方插件的基礎上實現quartz序列化到熱炒的mongodb,redis,震撼力可想而知,接下來本篇就和大家聊
一聊怎么搭建基於sqlserver的quartz集群,實現這么一種雙機熱備的強大功能。
一:下載sqlserver版的建表腳本
首先大家可以通過github上搜索quartz的源代碼,在源碼項目的/database/tables目錄下,可以找到firebird,oracle,mysql,sqlserver等建庫腳本,
本篇只需拿取sqlserver版本即可。 https://github.com/quartznet/quartznet/tree/master/database/tables 如下圖所示


從上面的截圖中可以看到,我接下來要做的事情就是增加一個你需要創建的database名字,這里取為:【quartz】,完整的腳本如下:
View Code

二:配置quartz的集群參數
當我們寫var scheduler = StdSchedulerFactory.GetDefaultScheduler()這段代碼的時候,如果大家看過源碼的話,會知道這個GetScheduler的
過程中有一個初始化方法【Instantiate】方法,此方法中你會發現在做DBProvider的時候會需要幾個參數來初始化DB的,比如下面看到的幾個標紅屬性。
1 IList<string> dsNames = cfg.GetPropertyGroups(PropertyDataSourcePrefix);
2 foreach (string dataSourceName in dsNames)
3 {
4 string datasourceKey = "{0}.{1}".FormatInvariant(PropertyDataSourcePrefix, dataSourceName);
5 NameValueCollection propertyGroup = cfg.GetPropertyGroup(datasourceKey, true);
6 PropertiesParser pp = new PropertiesParser(propertyGroup);
7
8 Type cpType = loadHelper.LoadType(pp.GetStringProperty(PropertyDbProviderType, null));
9
10 // custom connectionProvider...
11 if (cpType != null)
12 {
13 IDbProvider cp;
14 try
15 {
16 cp = ObjectUtils.InstantiateType<IDbProvider>(cpType);
17 }
18 catch (Exception e)
19 {
20 initException = new SchedulerException("ConnectionProvider of type '{0}' could not be instantiated.".FormatInvariant(cpType), e);
21 throw initException;
22 }
23
24 try
25 {
26 // remove the type name, so it isn't attempted to be set
27 pp.UnderlyingProperties.Remove(PropertyDbProviderType);
28
29 ObjectUtils.SetObjectProperties(cp, pp.UnderlyingProperties);
30 cp.Initialize();
31 }
32 catch (Exception e)
33 {
34 initException = new SchedulerException("ConnectionProvider type '{0}' props could not be configured.".FormatInvariant(cpType), e);
35 throw initException;
36 }
37
38 dbMgr = DBConnectionManager.Instance;
39 dbMgr.AddConnectionProvider(dataSourceName, cp);
40 }
41 else
42 {
43 string dsProvider = pp.GetStringProperty(PropertyDataSourceProvider, null);
44 string dsConnectionString = pp.GetStringProperty(PropertyDataSourceConnectionString, null);
45 string dsConnectionStringName = pp.GetStringProperty(PropertyDataSourceConnectionStringName, null);
46
47 if (dsConnectionString == null && !String.IsNullOrEmpty(dsConnectionStringName))
48 {
49
50 ConnectionStringSettings connectionStringSettings = ConfigurationManager.ConnectionStrings[dsConnectionStringName];
51 if (connectionStringSettings == null)
52 {
53 initException = new SchedulerException("Named connection string '{0}' not found for DataSource: {1}".FormatInvariant(dsConnectionStringName, dataSourceName));
54 throw initException;
55 }
56 dsConnectionString = connectionStringSettings.ConnectionString;
57 }
58
59 if (dsProvider == null)
60 {
61 initException = new SchedulerException("Provider not specified for DataSource: {0}".FormatInvariant(dataSourceName));
62 throw initException;
63 }
64 if (dsConnectionString == null)
65 {
66 initException = new SchedulerException("Connection string not specified for DataSource: {0}".FormatInvariant(dataSourceName));
67 throw initException;
68 }
69 try
70 {
71 DbProvider dbp = new DbProvider(dsProvider, dsConnectionString);
72 dbp.Initialize();
73
74 dbMgr = DBConnectionManager.Instance;
75 dbMgr.AddConnectionProvider(dataSourceName, dbp);
76 }
77 catch (Exception exception)
78 {
79 initException = new SchedulerException("Could not Initialize DataSource: {0}".FormatInvariant(dataSourceName), exception);
80 throw initException;
81 }
82 }
83 }
接下來的問題就是這幾個屬性是如何配置進去的,仔細觀察上面代碼,你會發現所有的配置的源頭都來自於cfg變量,ok,接下來你可以繼續翻看代碼,相信
你會看到有一個Initialize方法就是做cfg變量的初始化,如下代碼所示:
1 public void Initialize()
2 {
3 // short-circuit if already initialized
4 if (cfg != null)
5 {
6 return;
7 }
8 if (initException != null)
9 {
10 throw initException;
11 }
12
13 NameValueCollection props = (NameValueCollection) ConfigurationManager.GetSection(ConfigurationSectionName);
14
15 string requestedFile = QuartzEnvironment.GetEnvironmentVariable(PropertiesFile);
16
17 string propFileName = requestedFile != null && requestedFile.Trim().Length > 0 ? requestedFile : "~/quartz.config";
18
19 // check for specials
20 try
21 {
22 propFileName = FileUtil.ResolveFile(propFileName);
23 }
24 catch (SecurityException)
25 {
26 log.WarnFormat("Unable to resolve file path '{0}' due to security exception, probably running under medium trust");
27 propFileName = "quartz.config";
28 }
29
30 if (props == null && File.Exists(propFileName))
31 {
32 // file system
33 try
34 {
35 PropertiesParser pp = PropertiesParser.ReadFromFileResource(propFileName);
36 props = pp.UnderlyingProperties;
37 Log.Info(string.Format("Quartz.NET properties loaded from configuration file '{0}'", propFileName));
38 }
39 catch (Exception ex)
40 {
41 Log.Error("Could not load properties for Quartz from file {0}: {1}".FormatInvariant(propFileName, ex.Message), ex);
42 }
43
44 }
45 if (props == null)
46 {
47 // read from assembly
48 try
49 {
50 PropertiesParser pp = PropertiesParser.ReadFromEmbeddedAssemblyResource("Quartz.quartz.config");
51 props = pp.UnderlyingProperties;
52 Log.Info("Default Quartz.NET properties loaded from embedded resource file");
53 }
54 catch (Exception ex)
55 {
56 Log.Error("Could not load default properties for Quartz from Quartz assembly: {0}".FormatInvariant(ex.Message), ex);
57 }
58 }
59 if (props == null)
60 {
61 throw new SchedulerConfigException(
62 @"Could not find <quartz> configuration section from your application config or load default configuration from assembly.
63 Please add configuration to your application config file to correctly initialize Quartz.");
64 }
65 Initialize(OverrideWithSysProps(props));
66 }
仔細閱讀上面的一串代碼,你會發現,默認quartz參數配置來源於三個地方。
1. app.config中的section節點。
2. bin目錄下的~/quartz.config文件。
3. 默認配置的NameValueCollection字典集合,也就是上一篇博客給大家做的一個演示。
我個人不怎么喜歡通過quartz.config文件進行配置,這樣也容易寫死,所以我還是喜歡使用最簡單的NameValueCollection配置,因為它的數據源可來源
於第三方存儲結構中,配置代碼如下:
1 //1.首先創建一個作業調度池 2 var properties = new NameValueCollection(); 3 //存儲類型 4 properties["quartz.jobStore.type"] = "Quartz.Impl.AdoJobStore.JobStoreTX, Quartz"; 5 6 //驅動類型 7 properties["quartz.jobStore.driverDelegateType"] = "Quartz.Impl.AdoJobStore.SqlServerDelegate, Quartz"; //數據源名稱 8 properties["quartz.jobStore.dataSource"] = "myDS"; 9 10 //連接字符串 11 properties["quartz.dataSource.myDS.connectionString"] = @"server=.;Initial Catalog=quartz;Integrated Security=True"; 12 //sqlserver版本 13 properties["quartz.dataSource.myDS.provider"] = "SqlServer-20"; 14 15 //是否集群 16 properties["quartz.jobStore.clustered"] = "true"; 17 properties["quartz.scheduler.instanceId"] = "AUTO";
上面的代碼配置我都加過詳細的注釋,大家應該都能看得懂,而且這些配置就是這么定死的,沒什么修改的空間,大家記住即可。
三:Job和Trigger定義
在集群中環境下,job和trigger的定義該怎么寫的?大家也不要想的太復雜,注意一點就可以了,在Schedule一個Job時候,通過CheckExists判斷一下
這個Job在Scheduler中是否已經存在了,如果存在,你就不能再次通過Schedule去重復調度一個Job就可以了。。。所以判斷的代碼也很簡單,如下所示:
1 IScheduler scheduler = factory.GetScheduler();
2
3 scheduler.Start();
4
5 var jobKey = JobKey.Create("myjob", "group");
6
7 if (scheduler.CheckExists(jobKey))
8 {
9 Console.WriteLine("當前job已經存在,無需調度:{0}", jobKey.ToString());
10 }
11 else
12 {
13 IJobDetail job = JobBuilder.Create<HelloJob>()
14 .WithDescription("使用quartz進行持久化存儲")
15 .StoreDurably()
16 .RequestRecovery()
17 .WithIdentity(jobKey)
18 .UsingJobData("count", 1)
19 .Build();
20
21 ITrigger trigger = TriggerBuilder.Create().WithSimpleSchedule(x => x.WithIntervalInSeconds(2).RepeatForever())
22 .Build();
23
24 scheduler.ScheduleJob(job, trigger);
25
26 Console.WriteLine("調度進行中!!!");
27 }
上面這段代碼,大家就可以部署在多台機器中了,是不是很簡單?
四:強大的cluster完整演示
所有的初始化工作都做完了,接下來我們copy一份bin文件,同時打開兩個console程序,如下所示,可以看到job任務只會被一個console調度,另外
一個在空等待。

然后你肯定很好奇的跑到sqlserver中去看看,是否已經有job和trigger的db存儲,很開心吧,數據都有的。。。
、
好了,一切都是那么完美,接下來可以展示一下quartz集群下的高可用啦,如果某一個console掛了,那么另一台console會把這個任務給接過來,實
現強大的高可用。。。所以我要做的事情就是把console1關掉,再看看console2是不是可以開始調度job了???

完美,這個就是本篇給大家介紹的Quartz的Cluster集群,一台掛,另一台頂住,雙機熱備,當然這些console你可以部署在多台機器中,要做的就是保持各
個server的時間同步,因為quarz是依賴於本機server的時間,好了,本篇就先說到這里吧。
小禮物走一波,雙擊666。。。 完整代碼:SimpleSchedulerApp.zip

