-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathindex.html
558 lines (468 loc) · 45 KB
/
index.html
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>李彭勇的日志</title>
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
<meta name="description" content="描述信息">
<meta property="og:type" content="website">
<meta property="og:title" content="李彭勇的日志">
<meta property="og:url" content="http://blog.51mono.com/index.html">
<meta property="og:site_name" content="李彭勇的日志">
<meta property="og:description" content="描述信息">
<meta name="twitter:card" content="summary">
<meta name="twitter:title" content="李彭勇的日志">
<meta name="twitter:description" content="描述信息">
<link rel="alternative" href="/atom.xml" title="李彭勇的日志" type="application/atom+xml">
<link rel="icon" href="/favicon.png">
<link href="http://fonts.useso.com/css?family=Source+Code+Pro" rel="stylesheet" type="text/css">
<link rel="stylesheet" href="/css/style.css" type="text/css">
</head>
<body>
<div id="container">
<div id="wrap">
<header id="header">
<div id="banner"></div>
<div id="header-outer" class="outer">
<div id="header-title" class="inner">
<h1 id="logo-wrap">
<a href="/" id="logo">李彭勇</a>
</h1>
<h2 id="subtitle-wrap">
<a href="/" id="subtitle">副标题</a>
</h2>
</div>
<div id="header-inner" class="inner">
<nav id="main-nav">
<a id="main-nav-toggle" class="nav-icon"></a>
<a class="main-nav-link" href="/">首页</a>
<a class="main-nav-link" href="/archives">归档</a>
<a class="main-nav-link" href="/about">关于</a>
</nav>
<nav id="sub-nav">
<a id="nav-rss-link" class="nav-icon" href="/atom.xml" title="RSS Feed"></a>
<a id="nav-search-btn" class="nav-icon" title="Search"></a>
</nav>
<div id="search-form-wrap">
<form action="//google.com/search" method="get" accept-charset="UTF-8" class="search-form"><input type="search" name="q" results="0" class="search-form-input" placeholder="Search"><button type="submit" class="search-form-submit"></button><input type="hidden" name="sitesearch" value="http://blog.51mono.com"></form>
</div>
</div>
</div>
</header>
<div class="outer">
<section id="main">
<article id="post-Hangfire" class="article article-type-post" itemscope itemprop="blogPost">
<div class="article-meta">
<a href="/2016/02/10/Hangfire/" class="article-date">
<time datetime="2016-02-09T17:47:34.000Z" itemprop="datePublished">2016-02-10</time>
</a>
</div>
<div class="article-inner">
<header class="article-header">
<h1 itemprop="name">
<a class="article-title" href="/2016/02/10/Hangfire/">Hangfire</a>
</h1>
</header>
<div class="article-entry" itemprop="articleBody">
<h2 id="u6982_u8FF0"><a href="#u6982_u8FF0" class="headerlink" title="概述"></a>概述</h2><p>Hangfire是一个开源框架,用来创建、处理和管理后台作业。比如:你不希望通过处理管道进行的操作</p>
<p>使用场景:</p>
<ul>
<li>大量的通知/消息</li>
<li>从xml、csv、json批量导入</li>
<li>创建档案</li>
<li>触发Web钩子</li>
<li><p>删除用户</p>
</li>
<li><p>创建不同的图</p>
</li>
<li>图片/视频处理</li>
<li>清理临时文件</li>
<li>重复生成的自动报告</li>
<li>数据维护</li>
</ul>
<p><strong>任务类型</strong></p>
<hr>
<p>Hangfire支持多种后台作业,短时间运行或长时间运行、CPU密集型或I/O密集型、一次执行或重复执行。你不需要重新创造轮子,因为Hangfire非常好用。</p>
<ul>
<li>即时任务(立即触发,立即销毁)</li>
</ul>
<p>这是一种常用的任务类型,使用队列来处理,默认队列为“default”,支持多个队列,这些队列会有专门的监控执行队列中的任务<br>示例:</p>
<pre><code>BackgroundJob.Enqueue(() => Console.WriteLine("Fire-and-forget"));
</code></pre><ul>
<li>延时任务</li>
</ul>
<p>可以设置一个延时时间,触发后立即销毁<br>示例:</p>
<pre><code>BackgroundJob.Schedule(() => Console.WriteLine("Delayed"), TimeSpan.FromDays(1));
</code></pre><ul>
<li>重复任务</li>
</ul>
<p>支持Cron表达式<br>示例:</p>
<pre><code>RecurringJob.AddOrUpdate(() => Console.WriteLine("Daily Job"), Cron.Daily);
</code></pre><ul>
<li>连续任务</li>
</ul>
<p>多个后台任务组合成任务链,完成复杂的工作<br>示例:</p>
<pre><code>var id = BackgroundJob.Enqueue(() => Console.WriteLine("Hello, "));
BackgroundJob.ContinueWith(id, () => Console.WriteLine("world!"));
</code></pre><p>批量接口在Pro版提供<br>批量任务</p>
<pre><code>var batchId = Batch.StartNew(x =>
{
x.Enqueue(() => Console.WriteLine("Job 1"));
x.Enqueue(() => Console.WriteLine("Job 2"));
});
</code></pre><p>批量连续任务</p>
<pre><code>BackgroundJob.ContinueWith(
jobId,
() => Console.WriteLine("Continuation!"));
</code></pre><h2 id="u5FEB_u901F_u5165_u95E8"><a href="#u5FEB_u901F_u5165_u95E8" class="headerlink" title="快速入门"></a>快速入门</h2><h2 id="u5B89_u88C5"><a href="#u5B89_u88C5" class="headerlink" title="安装"></a>安装</h2><h2 id="u914D_u7F6E"><a href="#u914D_u7F6E" class="headerlink" title="配置"></a>配置</h2><h3 id="u4F7F_u7528Dashboard"><a href="#u4F7F_u7528Dashboard" class="headerlink" title="使用Dashboard"></a>使用Dashboard</h3><h3 id="u4F7F_u7528SQL_Server"><a href="#u4F7F_u7528SQL_Server" class="headerlink" title="使用SQL Server"></a>使用SQL Server</h3><h3 id="u4F7F_u7528MSMQ"><a href="#u4F7F_u7528MSMQ" class="headerlink" title="使用MSMQ"></a>使用MSMQ</h3><h3 id="u4F7F_u7528Redis"><a href="#u4F7F_u7528Redis" class="headerlink" title="使用Redis"></a>使用Redis</h3><h3 id="u914D_u7F6E_u65E5_u5FD7"><a href="#u914D_u7F6E_u65E5_u5FD7" class="headerlink" title="配置日志"></a>配置日志</h3><p>从Hangfire1.3.0版本开始,配置日志变得简单。<br>支持如下类型的日志:</p>
<ul>
<li>Serilog</li>
<li>NLog</li>
<li>Log4Net</li>
<li>EntLib Loggin</li>
<li>Loupe</li>
<li>Elmah</li>
</ul>
<h4 id="u63A7_u5236_u53F0_u65E5_u5FD7"><a href="#u63A7_u5236_u53F0_u65E5_u5FD7" class="headerlink" title="控制台日志"></a>控制台日志</h4><hr>
<p>如果你在控制台应用中使用,可以开启控制台日志。提供<code>ColouredConsoleLogProvider</code>,你可以这样设置:</p>
<pre><code>LogProvider.SetCurrentLogProvider(new ColouredConsoleLogProvider());
</code></pre><h4 id="u81EA_u5B9A_u4E49_u65E5_u5FD7"><a href="#u81EA_u5B9A_u4E49_u65E5_u5FD7" class="headerlink" title="自定义日志"></a>自定义日志</h4><hr>
<p>如果你的系统没有使用以上日志,你可以通过实现<code>ILog</code>接口自定义日志:</p>
<pre><code>public interface ILog
{
/// <summary>
/// Log a message the specified log level.
/// </summary>
/// <param name="logLevel">The log level.</param>
/// <param name="messageFunc">The message function.</param>
/// <param name="exception">An optional exception.</param>
/// <returns>true if the message was logged. Otherwise false.</returns>
/// <remarks>
/// Note to implementers: the message func should not be called if the loglevel is not enabled
/// so as not to incur performance penalties.
///
/// To check IsEnabled call Log with only LogLevel and check the return value, no event will be written
/// </remarks>
bool Log(LogLevel logLevel, Func<string> messageFunc, Exception exception = null);
}
public interface ILogProvider
{
ILog GetLogger(string name);
}
</code></pre><p>实现接口之后,你可以设置使用自定义日志:</p>
<pre><code>LogProvider.SetCurrentLogProvider(new CustomLogProvider());
</code></pre><h4 id="NLog_u65E5_u5FD7_u914D_u7F6E"><a href="#NLog_u65E5_u5FD7_u914D_u7F6E" class="headerlink" title="NLog日志配置"></a>NLog日志配置</h4><hr>
<p>使用NLog作为日志管理器<br> public class Startup<br> {<br> public void Configuration(IAppBuilder app)<br> {<br> GlobalConfiguration.Configuration.UseNLogLogProvider();<br> }<br> }</p>
<h4 id="u65E5_u5FD7_u7EA7_u522B_u63CF_u8FF0"><a href="#u65E5_u5FD7_u7EA7_u522B_u63CF_u8FF0" class="headerlink" title="日志级别描述"></a>日志级别描述</h4><hr>
<p>和常规的日志级别类似:</p>
<ul>
<li>Trace 跟踪Hangfire本身</li>
<li>Debug 调试</li>
<li>Info 打印信息:启动或停止。这是<code>推荐</code>级别</li>
<li>Warn 警告</li>
<li>Error 错误</li>
<li>Fatal 程序崩溃信息</li>
</ul>
<h2 id="u540E_u53F0_u65B9_u6CD5"><a href="#u540E_u53F0_u65B9_u6CD5" class="headerlink" title="后台方法"></a>后台方法</h2><h3 id="u540E_u53F0_u8C03_u7528_u65B9_u6CD5"><a href="#u540E_u53F0_u8C03_u7528_u65B9_u6CD5" class="headerlink" title="后台调用方法"></a>后台调用方法</h3><h3 id="u8C03_u7528_u5EF6_u8FDF_u65B9_u6CD5"><a href="#u8C03_u7528_u5EF6_u8FDF_u65B9_u6CD5" class="headerlink" title="调用延迟方法"></a>调用延迟方法</h3><h3 id="u6267_u884C_u91CD_u590D_u4EFB_u52A1"><a href="#u6267_u884C_u91CD_u590D_u4EFB_u52A1" class="headerlink" title="执行重复任务"></a>执行重复任务</h3><h3 id="u4F20_u53C2"><a href="#u4F20_u53C2" class="headerlink" title="传参"></a>传参</h3><h3 id="u4F20_u9012_u4F9D_u8D56"><a href="#u4F20_u9012_u4F9D_u8D56" class="headerlink" title="传递依赖"></a>传递依赖</h3><p>支持泛型,构造函数默认使用反射创建方法Activator.CreateInstance<br>示例:</p>
<pre><code>BackgroundJob.Enqueue<EmailSender>(x => x.Send(13, "Hello!"));
</code></pre><p>EmailSender定义</p>
<pre><code>public class EmailSender
{
public void Send(int userId, string message)
{
var dbContext = new DbContext();
var emailService = new EmailService();
// Some processing logic
}
}
</code></pre><p>可以设置默认构造函数(必须有)</p>
<pre><code>public class EmailSender
{
private IDbContext _dbContext;
private IEmailService _emailService;
public EmailSender()
{
_dbContext = new DbContext();
_emailService = new EmailService();
}
// ...
}
</code></pre><p>可以自定义Ioc方式,也可以使用已有的依赖注入。参看:使用IoC容器</p>
<h3 id="u4F7F_u7528IoC_u5BB9_u5668"><a href="#u4F7F_u7528IoC_u5BB9_u5668" class="headerlink" title="使用IoC容器"></a>使用IoC容器</h3><h3 id="u4F7F_u7528_u53D6_u6D88_u6807_u8BB0"><a href="#u4F7F_u7528_u53D6_u6D88_u6807_u8BB0" class="headerlink" title="使用取消标记"></a>使用取消标记</h3><p>支持位作业设置取消记号,让我们可以清楚知道Shutdown请求的发起、作业中止。对于正常的关闭,会自动将没处理完的作业添加到队列,重新启动后会再次执行。<br>Cancellation Token通过IJobCancellationToke接口实现,该接口包含方法ThrowIfCancellationRequested方法,当收到取消请求时抛出OperationCanceledException异常。<br>示例:</p>
<pre><code>public void LongRunningMethod(IJobCancellationToken cancellationToken)
{
for (var i = 0; i < Int32.MaxValue; i++)
{
cancellationToken.ThrowIfCancellationRequested();
Thread.Sleep(TimeSpan.FromSeconds(1));
}
}
</code></pre><p>当你在执行一个耗时很长的作业时,我们手动关闭或者因为程序异常引发Server关闭,会在作业中抛出异常,自动记录详细信息。<br>你也可以不指定,通过传递null或者使用JobCancellationToken.Null,告诉程序你这是正确的执行:</p>
<pre><code>BackgroundJob.Enqueue(() => LongRunningMethod(JobCancellationToken.Null));
</code></pre><p>Hangfire负责在作业运行时传递非空IJobCancellationToken实例(难道是依赖注入?)。<br>你应该尽肯能多地使用Cancellation Tokens,这会大大降低应用程序关闭时间和抛出ThreadAbortException异常的风险。</p>
<h3 id="u7F16_u5199_u5355_u5143_u6D4B_u8BD5"><a href="#u7F16_u5199_u5355_u5143_u6D4B_u8BD5" class="headerlink" title="编写单元测试"></a>编写单元测试</h3><h3 id="u6279_u91CF_u5904_u7406"><a href="#u6279_u91CF_u5904_u7406" class="headerlink" title="批量处理"></a>批量处理</h3><h2 id="u540E_u53F0_u5904_u7406"><a href="#u540E_u53F0_u5904_u7406" class="headerlink" title="后台处理"></a>后台处理</h2><h3 id="u5904_u7406_u540E_u53F0_u4F5C_u4E1A"><a href="#u5904_u7406_u540E_u53F0_u4F5C_u4E1A" class="headerlink" title="处理后台作业"></a>处理后台作业</h3><hr>
<p>后台作业由Server部分处理(参看架构部分),Server不依赖Asp.Net可以在任何地方启动,如:控制台、Microsoft Azure。所有操作通过BackgroundJobServer类提供的简单API(方法)实现。<br>示例:</p>
<pre><code>// 创建Hangfire Server实例并启动
// 可以在构造函数中传递高级参数,如:指定作业存储
var server = new BackgroundJobServer();
// 等待服务器关闭
server.Dispose();
</code></pre><p>Server由不同的组件组成,分别执行不同的工作:Workers(工人)负责监听作业队列并处理作业;Recurring Scheduler(重复调度器)处理重复作业,通过轮询处理延迟作业;Expire Manager(到期管理器)负责移除过期作业,保持存储尽可能没有冗余数据。<br>Dispose是一个阻塞方法,会等待所有的组件结束之后才完成,如:Workers将会恢复中断的作业到相应的队列。<br>严格的说,你不需要调用Dispose方法,Hangfire可以处理进程的终止,会自动尝试中断作业,但是最好使用Cancellation Tokens(参考相应章节)。</p>
<h3 id="u5728Web_u5E94_u7528_u4E2D_u5904_u7406_u4F5C_u4E1A"><a href="#u5728Web_u5E94_u7528_u4E2D_u5904_u7406_u4F5C_u4E1A" class="headerlink" title="在Web应用中处理作业"></a>在Web应用中处理作业</h3><p>在Web应用中处理作业是Hangfire的一个重要解决的问题。没有额外的应用,如Windows服务、控制台应用来运行后台作业,当然你在真的需要的时候可以切换成相应的服务。<br>不依赖System.Web,可以集成到任何Web框架:</p>
<ul>
<li>Asp.net webform</li>
<li>Asp.net MVC</li>
<li>Asp.net WebApi</li>
<li>Asp.net vNext(通过app.UseOwin方法)</li>
<li>其他Owin框架(Nancy/FubuMVC/Simple.Web)</li>
<li>非Twin框架(ServcieStack)</li>
</ul>
<h4 id="u4F7F_u7528BackgroundJobServer_u7C7B"><a href="#u4F7F_u7528BackgroundJobServer_u7C7B" class="headerlink" title="使用BackgroundJobServer类"></a>使用BackgroundJobServer类</h4><hr>
<p>这是基本的方法,当然还有更快捷的方式。我们需要控制它的Start和Dispose方法。<br>最佳实践:我们不能保证所有的Web框架都能及时调用Dispose方法,所以最好是在应用程序关闭时手动调用Dispose方法。<br>举个例子,在Asp.net应用中,最好的地方是global.asax.cs中调用:</p>
<pre><code>using System;
using System.Web;
using Hangfire;
namespace WebApplication1
{
public class Global : HttpApplication
{
private BackgroundJobServer _backgroundJobServer;
protected void Application_Start(object sender, EventArgs e)
{
GlobalConfiguration.Configuration
.UseSqlServerStorage("DbConnection");
_backgroundJobServer = new BackgroundJobServer();
}
protected void Application_End(object sender, EventArgs e)
{
_backgroundJobServer.Dispose();
}
}
}
</code></pre><h4 id="u4F7F_u7528Owin_u6269_u5C55_u65B9_u6CD5"><a href="#u4F7F_u7528Owin_u6269_u5C55_u65B9_u6CD5" class="headerlink" title="使用Owin扩展方法"></a>使用Owin扩展方法</h4><hr>
<p>这个调用更简单<br>Hangfire基于Owin处理管道实现了一套UI界面<br>Hangfire扩展了IAppBuilder接口<br>确保安装Microsoft.Owin.Host.SystemWeb<br>使用:</p>
<pre><code>public class Startup
{
public void Configuration(IAppBuilder app)
{
app.UseHangfireServer();
}
}
</code></pre><p>这一行代码会自动创建BackgroundJobServer示例,并调用Start方法和注册Dispose方法。</p>
<h3 id="u5728_u63A7_u5236_u53F0_u5E94_u7528_u4E2D_u5904_u7406_u4F5C_u4E1A"><a href="#u5728_u63A7_u5236_u53F0_u5E94_u7528_u4E2D_u5904_u7406_u4F5C_u4E1A" class="headerlink" title="在控制台应用中处理作业"></a>在控制台应用中处理作业</h3><h3 id="u5728Windows_u670D_u52A1_u4E2D_u5904_u7406_u4F5C_u4E1A"><a href="#u5728Windows_u670D_u52A1_u4E2D_u5904_u7406_u4F5C_u4E1A" class="headerlink" title="在Windows服务中处理作业"></a>在Windows服务中处理作业</h3><h3 id="u5904_u7406_u5F02_u5E38"><a href="#u5904_u7406_u5F02_u5E38" class="headerlink" title="处理异常"></a>处理异常</h3><p>Hangfire能够处理所有的内部(Hangfire本身)或者外部异常(job、filter等),所以能够保持应用程序不崩溃。所有内部异常会通过日志记录(别忘了开启日志,参看:配置-启用日志章节)。<br>最坏的情况是,在后台作业中断后会进行10次自动重试。<br>在作业执行过程中产生异常会自动更改作业状态为<code>Failed</code>(失败),你可以在可视化面板中查看。<br><img src="/2016/02/10/Hangfire/failed-job.png" alt=""><br>使用AutomaticRetry拦截器可以设置重试次数。(参看:扩展-过滤器)<br>如果你不想重试,可以这样设置</p>
<pre><code>[AutomaticRetry(Attempts = 0)]
public void BackgroundMethod()
{
}
</code></pre><p>系统默认是10次,你也可以这样更改成5次:</p>
<p>GlobalJobFilters.Filters.Add(new AutomaticRetryAttribute { Attempts = 5 });</p>
<h3 id="u8FDB_u5EA6_u76D1_u63A7"><a href="#u8FDB_u5EA6_u76D1_u63A7" class="headerlink" title="进度监控"></a>进度监控</h3><p>实现进度监控有两种方式:轮询和推送。轮询容易实现,这种实现方式会增加服务器的访问压力;所以推荐使用推送,使用<a href="http://signalr.net" target="_blank" rel="external">SingalR</a>可以很容易实现服务器推送进度。<br>这里使用一个简单的示例,客户端需要检测作业是否完成。完整的例子,可以参看<a href="https://github.com/odinserj/Hangfire.Highlighter" target="_blank" rel="external">Hangfir.Highlighter</a>项目.<br>Highlighter项目有一个后台作业,是通过调用Web Service实现代码高亮:</p>
<pre><code>public void Highlight(int snippetId)
{
var snippet = _dbContext.CodeSnippets.Find(snippetId);
if (snippet == null) return;
snippet.HighlightedCode = HighlightSource(snippet.SourceCode);
snippet.HighlightedAt = DateTime.UtcNow;
_dbContext.SaveChanges();
}
</code></pre><p><strong>轮询作业的执行状态</strong></p>
<hr>
<p>当HighlightedCode属性值为null时,我们认定作业未完成;当该属性有值时,我们认定作业已经完成。<br>所以,我们需要通过Javascript发起ajax访问一个controller返回作业状态,直到作业完成。</p>
<pre><code>public ActionResult CheckHighlighted(int snippetId)
{
var snippet = _db.Snippets.Find(snippetId);
return snippet.HighlightedCode == null
? new HttpStatusCodeResult(HttpStatusCode.NoContent)
: Content(snippet.HighlightedCode);
}
</code></pre><p>当代码变为高亮,我们可以停止轮询然后显示高亮代码。但是如果我们需要监控作业的进度,我们需要执行以下额外的步骤:</p>
<ul>
<li>在snippets表中添加表示状态的列Status</li>
<li>在执行后台作业时,更新列</li>
<li>通过轮询获取该列的值</li>
</ul>
<p>但是我们有更好的方式。</p>
<p><strong>使用SignalR推送</strong></p>
<hr>
<p>SignalR是一个服务推送的框架,我相信你会喜欢上它的。如果你需要查看详细代码,可以参看我们的示例项目,这里只列出核心代码,你需要改变的两个地方。<br>首先你需要添加一个Hub:</p>
<pre><code>public class SnippetHub : Hub
{
public async Task Subscribe(int snippetId)
{
await Groups.Add(Context.ConnectionId, GetGroup(snippetId));
// When a user subscribes a snippet that was already
// highlighted, we need to send it immediately, because
// otherwise she will listen for it infinitely.
using (var db = new HighlighterDbContext())
{
var snippet = await db.CodeSnippets
.Where(x => x.Id == snippetId && x.HighlightedCode != null)
.SingleOrDefaultAsync();
if (snippet != null)
{
Clients.Client(Context.ConnectionId)
.highlight(snippet.Id, snippet.HighlightedCode);
}
}
}
public static string GetGroup(int snippetId)
{
return "snippet:" + snippetId;
}
}
</code></pre><p>然后,在后台作业代码中你只需要做一个小改动:</p>
<pre><code>public void HighlightSnippet(int snippetId)
{
...
_dbContext.SaveChanges();
var hubContext = GlobalHost.ConnectionManager
.GetHubContext<SnippetHub>();
hubContext.Clients.Group(SnippetHub.GetGroup(snippet.Id))
.highlight(snippet.HighlightedCode);
}
</code></pre><p>这就是所有的步骤,当用户打开包含没有代码块高亮显示的页面,那么浏览器会连接服务器,注册代码块通知,等待通知更新,当后再作业完成,它将发送高亮显示代码到所有订阅的用户。<br>如果你需要添加一个进度条,尽管去做好了,不需要改动表添加列,只需要添加Javascript方法。</p>
<h3 id="u914D_u7F6E_u4F5C_u4E1A_u5904_u7406_u5E76_u884C_u7B49_u7EA7"><a href="#u914D_u7F6E_u4F5C_u4E1A_u5904_u7406_u5E76_u884C_u7B49_u7EA7" class="headerlink" title="配置作业处理并行等级"></a>配置作业处理并行等级</h3><h3 id="u4F7F_u7528_u5355_u72EC_u7EBF_u7A0B_u5904_u7406_u4F5C_u4E1A"><a href="#u4F7F_u7528_u5355_u72EC_u7EBF_u7A0B_u5904_u7406_u4F5C_u4E1A" class="headerlink" title="使用单独线程处理作业"></a>使用单独线程处理作业</h3><h3 id="u8FD0_u884C_u591A_u4E2A_u670D_u52A1_u5668_u5B9E_u4F8B"><a href="#u8FD0_u884C_u591A_u4E2A_u670D_u52A1_u5668_u5B9E_u4F8B" class="headerlink" title="运行多个服务器实例"></a>运行多个服务器实例</h3><h3 id="u914D_u7F6E_u4F5C_u4E1A_u961F_u5217"><a href="#u914D_u7F6E_u4F5C_u4E1A_u961F_u5217" class="headerlink" title="配置作业队列"></a>配置作业队列</h3><h2 id="u6700_u4F73_u5B9E_u8DF5"><a href="#u6700_u4F73_u5B9E_u8DF5" class="headerlink" title="最佳实践"></a>最佳实践</h2><p>后台任务我们需要优先考虑两个内容:流畅运行和高效运行。<br>我们通过传递一个方法作为任务,任务的执行和方法的执行有很大的不同。</p>
<h3 id="u5EFA_u8BAE1_uFF1A_u7CBE_u7B80_u53C2_u6570"><a href="#u5EFA_u8BAE1_uFF1A_u7CBE_u7B80_u53C2_u6570" class="headerlink" title="建议1:精简参数"></a>建议1:精简参数</h3><hr>
<p>一个后台任务调用方法,在创建的过程中会序列化,参数会使用TypeConveter类转换成Json字符串格式。如果你有复杂的实体或大型对象,如:数组,最好的方式是将他们保存到数据库,然后通过ID查询。<br>示例:</p>
<pre><code>public void Method(Entity entity) { }
</code></pre><p>优化</p>
<pre><code>public void Method(int entityId) { }
</code></pre><h3 id="u5EFA_u8BAE2_uFF1A_u786E_u4FDD_u540E_u53F0_u4EFB_u52A1_u8C03_u7528_u7684_u662F_u53EF_u91CD_u5165_u51FD_u6570"><a href="#u5EFA_u8BAE2_uFF1A_u786E_u4FDD_u540E_u53F0_u4EFB_u52A1_u8C03_u7528_u7684_u662F_u53EF_u91CD_u5165_u51FD_u6570" class="headerlink" title="建议2:确保后台任务调用的是可重入函数"></a>建议2:确保后台任务调用的是可重入函数</h3><hr>
<p>可重入意味着一个函数可以在执行过程中被中断,然后再次调用。在任务执行时,我们往往会被多种情况中断,如:异常、断电,Hangfire会尝试再次执行任务。<br>如果编写的函数不可重入,可能会面临很多问题,举个例子:一个发送邮件的任务,在执行时SMTP服务器出错抛出异常,你可以结束多封邮件的发送。<br>示例:</p>
<pre><code>public void Method()
{
_emailService.Send("[email protected]", "Hello!");
}
</code></pre><p>直接发送邮件,没有做任何的可重入处理<br>改进:</p>
<pre><code>public void Method(int deliveryId)
{
if (_emailService.IsNotDelivered(deliveryId))
{
_emailService.Send("[email protected]", "Hello!");
_emailService.SetDelivered(deliveryId);
}
}
</code></pre><p>增加了两个方法:通过ID来确保任务的课重复,IsNotDelivered是否已经完成和SetDelivered标识已经完成。这样就算是任务中断,也可再次执行。</p>
<h2 id="u53D1_u5E03_u90E8_u7F72"><a href="#u53D1_u5E03_u90E8_u7F72" class="headerlink" title="发布部署"></a>发布部署</h2><h2 id="u6269_u5C55"><a href="#u6269_u5C55" class="headerlink" title="扩展"></a>扩展</h2><h3 id="u4F5C_u4E1A_u8FC7_u6EE4_u5668Job_Filter"><a href="#u4F5C_u4E1A_u8FC7_u6EE4_u5668Job_Filter" class="headerlink" title="作业过滤器Job Filter"></a>作业过滤器Job Filter</h3><hr>
<p>所有的处理采用Chain-of-responsibility(责任链)模式实现,可以像MVC中的过滤器一样进行拦截。<br>定义一个过滤器:</p>
<pre><code>public class LogEverythingAttribute : JobFilterAttribute,
IClientFilter, IServerFilter, IElectStateFilter, IApplyStateFilter
{
private static readonly ILog Logger = LogManager.GetCurrentClassLogger();
public void OnCreating(CreatingContext filterContext)
{
Logger.InfoFormat(
"Creating a job based on method `{0}`...",
filterContext.Job.MethodData.MethodInfo.Name);
}
public void OnCreated(CreatedContext filterContext)
{
Logger.InfoFormat(
"Job that is based on method `{0}` has been created with id `{1}`",
filterContext.Job.MethodData.MethodInfo.Name,
filterContext.JobId);
}
public void OnPerforming(PerformingContext filterContext)
{
Logger.InfoFormat(
"Starting to perform job `{0}`",
filterContext.JobId);
}
public void OnPerformed(PerformedContext filterContext)
{
Logger.InfoFormat(
"Job `{0}` has been performed",
filterContext.JobId);
}
public void OnStateElection(ElectStateContext context)
{
var failedState = context.CandidateState as FailedState;
if (failedState != null)
{
Logger.WarnFormat(
"Job `{0}` has been failed due to exception `{1}` but will be retried automatically until retry attempts exceeded",
context.JobId,
failedState.Exception);
}
}
public void OnStateApplied(ApplyStateContext context, IWriteOnlyTransaction transaction)
{
Logger.InfoFormat(
"Job `{0}` state was changed from `{1}` to `{2}`",
context.JobId,
context.OldStateName,
context.NewState.Name);
}
public void OnStateUnapplied(ApplyStateContext context, IWriteOnlyTransaction transaction)
{
Logger.InfoFormat(
"Job `{0}` state `{1}` was unapplied.",
context.JobId,
context.OldStateName);
}
}
</code></pre><p>你可以在类、方法中,或者全局设置过滤器</p>
<pre><code>[LogEverything]
public class EmailService
{
[LogEverything]
public static void Send() { }
}
GlobalJobFilters.Filters.Add(new LogEverythingAttribute());
</code></pre><h2 id="u9879_u76EE_u6307_u5357"><a href="#u9879_u76EE_u6307_u5357" class="headerlink" title="项目指南"></a>项目指南</h2><h2 id="u89E3_u60D1_u90E8_u5206"><a href="#u89E3_u60D1_u90E8_u5206" class="headerlink" title="解惑部分"></a>解惑部分</h2><h3 id="1-_u5982_u4F55_u5BF9Hangfire_u8FDB_u884C_u5355_u72EC_u670D_u52A1_u5668_u90E8_u7F72_uFF1F"><a href="#1-_u5982_u4F55_u5BF9Hangfire_u8FDB_u884C_u5355_u72EC_u670D_u52A1_u5668_u90E8_u7F72_uFF1F" class="headerlink" title="1.如何对Hangfire进行单独服务器部署?"></a>1.如何对Hangfire进行单独服务器部署?</h3><hr>
<p>可能存在的问题:Client和Server分离,作业在客户端定义,如何存储到Storage?服务器端如何执行作业?<br>可以创建一个测试程序</p>
<h3 id="2-_u5982_u4F55_u5BF9Hangfire_u76D1_u63A7_u7CFB_u7EDF_u8FDB_u884C_u6743_u9650_u63A7_u5236_uFF1F"><a href="#2-_u5982_u4F55_u5BF9Hangfire_u76D1_u63A7_u7CFB_u7EDF_u8FDB_u884C_u6743_u9650_u63A7_u5236_uFF1F" class="headerlink" title="2.如何对Hangfire监控系统进行权限控制?"></a>2.如何对Hangfire监控系统进行权限控制?</h3><p>默认是不会进行登录验证的</p>
</div>
<footer class="article-footer">
<a data-url="http://blog.51mono.com/2016/02/10/Hangfire/" data-id="cikfsda9s0001f59kg39xk50i" class="article-share-link">Share</a>
</footer>
</div>
</article>
</section>
<aside id="sidebar">
<div class="widget-wrap">
<h3 class="widget-title">归档</h3>
<div class="widget">
<ul class="archive-list"><li class="archive-list-item"><a class="archive-list-link" href="/archives/2016/02/">二月 2016</a><span class="archive-list-count">1</span></li></ul>
</div>
</div>
<div class="widget-wrap">
<h3 class="widget-title">最近文章</h3>
<div class="widget">
<ul>
<li>
<a href="/2016/02/10/Hangfire/">Hangfire</a>
</li>
</ul>
</div>
</div>
</aside>
</div>
<footer id="footer">
<div class="outer">
<div id="footer-info" class="inner">
© 2016 李彭勇<br>
Powered by <a href="http://hexo.io/" target="_blank">Hexo</a>
</div>
</div>
</footer>
</div>
<nav id="mobile-nav">
<a href="/" class="mobile-nav-link">首页</a>
<a href="/archives" class="mobile-nav-link">归档</a>
<a href="/about" class="mobile-nav-link">关于</a>
</nav>
<script src="//cdn.bootcss.com/jquery/2.2.0/jquery.min.js"></script>
<script type="text/javascript">
//<![CDATA[
if (typeof jQuery == 'undefined') {
document.write(unescape("%3Cscript src='/js/jquery-2.2.0.min.js' type='text/javascript'%3E%3C/script%3E"));
}
// ]]>
</script>
<link rel="stylesheet" href="/fancybox/jquery.fancybox.css" type="text/css">
<script src="/fancybox/jquery.fancybox.pack.js" type="text/javascript"></script>
<script src="/js/script.js" type="text/javascript"></script>
</div>
</body>
</html>