Hangfire Redis 實(shí)現(xiàn)秒級(jí)定時(shí)任務(wù)、使用 CQRS 實(shí)現(xiàn)動(dòng)態(tài)執(zhí)行代碼
定時(shí)任務(wù)需求
本文示例項(xiàng)目倉(cāng)庫(kù):https://github.com/whuanle/HangfireDemo
主要有兩個(gè)核心需求:
- 需要實(shí)現(xiàn)秒級(jí)定時(shí)任務(wù);
- 開發(fā)者使用定時(shí)任務(wù)要簡(jiǎn)單,不要弄復(fù)雜了;
在微服務(wù)架構(gòu)中中,定時(shí)任務(wù)是最常用的基礎(chǔ)設(shè)施組件之一,社區(qū)中有很多定時(shí)任務(wù)類庫(kù)或平臺(tái),例如 Quartz.NET、xxx-job,使用方法差異很大,比如 xxx-job 的核心是 http 請(qǐng)求,配置定時(shí)任務(wù)實(shí)現(xiàn) http 請(qǐng)求具體的接口,不過用起來還是比較復(fù)雜的。
在微服務(wù)中,使用的組件太多了,如果每個(gè)組件的集成都搞得很麻煩,那么服務(wù)的代碼很可能會(huì)大量膨脹,并且容易出現(xiàn)各種 bug。以 xxx-job 為例,如果項(xiàng)目中有 N 個(gè)定時(shí)任務(wù),設(shè)計(jì) N 個(gè) http 接口被 xxx-job 回調(diào)觸發(fā),除了 http 接口數(shù)量龐大,在各個(gè)環(huán)節(jié)中還容易出現(xiàn) bug。
在近期項(xiàng)目需求中,剛好要用到定時(shí)任務(wù),結(jié)合 C# 語言的特性,筆者的方法是利用 Hangfire 框架和語言特性,封裝一些方法,使得開發(fā)者可以無感使用定時(shí)任務(wù),大大簡(jiǎn)化鏈路和使用難度。
使用示例,結(jié)合 MediatR 框架定義 CQRS ,該 Command 將會(huì)被定時(shí)任務(wù)觸發(fā)執(zhí)行:
public class MyTestRequest : HangfireRequest, IRequest<ExecteTasResult>
{
}
/// <summary>
/// 要被定時(shí)任務(wù)執(zhí)行的代碼.
/// </summary>
public class MyTestHandler : IRequestHandler<MyTestRequest, ExecteTasResult>
{
public async Task<ExecteTasResult> Handle(MyTestRequest request, CancellationToken cancellationToken)
{
// 邏輯
return new ExecteTasResult
{
CancelTask = false
};
}
}
要啟動(dòng)一個(gè)定時(shí)任務(wù),只需要:
private readonly SendHangfireService _hangfireService;
public SendTaskController(SendHangfireService hangfireService)
{
_hangfireService = hangfireService;
}
[HttpGet("aaa")]
public async Task<string> SendAsync()
{
await _hangfireService.Send(new MyTestRequest
{
CreateTime = DateTimeOffset.Now,
CronExpression = "* * * * * *",
TaskId = Guid.NewGuid().ToString(),
});
return "aaa";
}
通過這種方式使用定時(shí)任務(wù),開發(fā)者只需要使用很簡(jiǎn)單的代碼即可完成需求,不需要關(guān)注細(xì)節(jié),也不需要定義各種 http 接口,并且猶豫不需要關(guān)注使用的外部定時(shí)任務(wù)框架,所以隨時(shí)可以切換不同的定時(shí)任務(wù)實(shí)現(xiàn)。
核心邏輯
本文示例項(xiàng)目倉(cāng)庫(kù):whuanle/HangfireDemo
示例項(xiàng)目結(jié)構(gòu)如下:

HangfireServer 是定時(shí)任務(wù)服務(wù)實(shí)現(xiàn),HangfireServer 服務(wù)只需要暴露兩個(gè)接口 addtask、cancel,分別用于添加定時(shí)任務(wù)和取消定時(shí)任務(wù),無論什么業(yè)務(wù)的服務(wù),都通過 addtask 服務(wù)添加。
DemoApi 則是業(yè)務(wù)服務(wù),業(yè)務(wù)服務(wù)只需要暴露一個(gè)· execute 接口用于觸發(fā)定時(shí)任務(wù)即可。
基礎(chǔ)邏輯如下:
由于項(xiàng)目中使用的是 MediatR 框架實(shí)現(xiàn) CQRS 模式,因此很容易實(shí)現(xiàn)定時(shí)任務(wù)動(dòng)態(tài)調(diào)用代碼,只需要按照平時(shí)的 CQRS 發(fā)送定時(shí)任務(wù)命令,指定定時(shí)任務(wù)要執(zhí)行的 Command 即可。
例如,有以下 Command 需要被定時(shí)任務(wù)執(zhí)行:
ACommand
BCommand
CCommand
首先這些命令會(huì)被序列化為 json ,發(fā)送到 HangfireServer 服務(wù),HangfireServer 在恰當(dāng)時(shí)機(jī)將參數(shù)原封不動(dòng)推送到 DemoApi 服務(wù),DemoApi 服務(wù)拿到這些參數(shù)序列化為對(duì)應(yīng)的類型,然后通過 MediatR 發(fā)送命令,即可實(shí)現(xiàn)任意命令的定時(shí)任務(wù)動(dòng)態(tài)調(diào)用。
下面來分別實(shí)現(xiàn) HangfireServer 、DemoApi 服務(wù)。
在 Shred 項(xiàng)目中添加以下文件。

其中 TaskRequest 內(nèi)容如下,其它文件請(qǐng)參考示例項(xiàng)目。
public class TaskRequest
{
/// <summary>
/// 任務(wù) id.
/// </summary>
public string TaskId { get; set; } = "";
/// <summary>
/// 定時(shí)任務(wù)要請(qǐng)求的服務(wù)地址或服務(wù)名稱.
/// </summary>
public string ServiceName { get; set; } = "";
/// <summary>
/// 參數(shù)類型名稱.
/// </summary>
public string CommandType { get; set; } = "";
/// <summary>
/// 請(qǐng)求參數(shù)內(nèi)容,json 序列化后的字符串.
/// </summary>
public string CommandBody { get; set; } = "";
/// <summary>
/// Cron 表達(dá)式.
/// </summary>
public string CronExpression { get; set; } = "";
/// <summary>
/// 創(chuàng)建時(shí)間.
/// </summary>
public string CreateTime { get; set; } = "";
}
使用 Redis 實(shí)現(xiàn)秒級(jí)定時(shí)任務(wù)
Hangfire 本身配置比較復(fù)雜,其分布式實(shí)現(xiàn)對(duì)數(shù)據(jù)庫(kù)性能要求比較高,因此使用 Mysql、Sqlserver 等數(shù)據(jù)庫(kù)存儲(chǔ)數(shù)據(jù)會(huì)帶了很大的壓力,而且要求實(shí)現(xiàn)秒級(jí)定時(shí)任務(wù),NoSql 數(shù)據(jù)庫(kù)可以更加好地實(shí)現(xiàn)這一需求,筆者這里使用 Redis 來存儲(chǔ)任務(wù)數(shù)據(jù)。
HangfireServer 項(xiàng)目結(jié)構(gòu)如下:

對(duì) HangfireServer 的設(shè)計(jì)主要分為幾步:
- Hangfire 支持容器管理;
- 配置 Hangfire ;
- 定義 RecurringJobHandler 執(zhí)行任務(wù)發(fā)起 http 請(qǐng)求到業(yè)務(wù)系統(tǒng);
- 定義 http 接口,接收定時(shí)任務(wù);
引入類庫(kù):
<PackageReference Include="Hangfire.AspNetCore" Version="1.8.18" />
<PackageReference Include="Hangfire.Redis.StackExchange" Version="1.12.0" />
首先是關(guān)于 Hangfire 本身的配置,現(xiàn)在幾乎都是基于依賴注入的設(shè)計(jì),不搞靜態(tài)類型,所以我們需要實(shí)現(xiàn)定時(shí)任務(wù)執(zhí)行器創(chuàng)建服務(wù)實(shí)例的,以便每次定時(shí)任務(wù)請(qǐng)求時(shí),服務(wù)實(shí)例都是在一個(gè)新的容器,處以一個(gè)新的上下文中。
第一步
創(chuàng)建 HangfireJobActivatorScope、HangfireActivator 兩個(gè)文件,實(shí)現(xiàn) Hangfire 支持容器上下文。
/// <summary>
/// 任務(wù)容器.
/// </summary>
public class HangfireJobActivatorScope : JobActivatorScope
{
private readonly IServiceScope _serviceScope;
private readonly string _jobId;
/// <summary>
/// Initializes a new instance of the <see cref="HangfireJobActivatorScope"/> class.
/// </summary>
/// <param name="serviceScope"></param>
/// <param name="jobId"></param>
public HangfireJobActivatorScope([NotNull] IServiceScope serviceScope, string jobId)
{
_serviceScope = serviceScope ?? throw new ArgumentNullException(nameof(serviceScope));
_jobId = jobId;
}
/// <inheritdoc/>
public override object Resolve(Type type)
{
var res = ActivatorUtilities.GetServiceOrCreateInstance(_serviceScope.ServiceProvider, type);
return res;
}
/// <inheritdoc/>
public override void DisposeScope()
{
_serviceScope.Dispose();
}
}
/// <summary>
/// JobActivator.
/// </summary>
public class HangfireActivator : JobActivator
{
private readonly IServiceScopeFactory _serviceScopeFactory;
/// <summary>
/// Initializes a new instance of the <see cref="HangfireActivator"/> class.
/// </summary>
/// <param name="serviceScopeFactory"></param>
public HangfireActivator(IServiceScopeFactory serviceScopeFactory)
{
_serviceScopeFactory = serviceScopeFactory ?? throw new ArgumentNullException(nameof(serviceScopeFactory));
}
/// <inheritdoc/>
public override JobActivatorScope BeginScope(JobActivatorContext context)
{
return new HangfireJobActivatorScope(_serviceScopeFactory.CreateScope(), context.BackgroundJob.Id);
}
}
第二步
配置 Hangfire 服務(wù),使其支持 Redis,并且配置一些參數(shù)。
private void ConfigureHangfire(IServiceCollection services)
{
var options =
new RedisStorageOptions
{
// 配置 redis 前綴,每個(gè)任務(wù)實(shí)例都會(huì)創(chuàng)建一個(gè) key
Prefix = "aaa:aaa:hangfire",
};
services.AddHangfire(
config =>
{
config.UseRedisStorage("{redis連接字符串}", options)
.SetDataCompatibilityLevel(CompatibilityLevel.Version_180)
.UseSimpleAssemblyNameTypeSerializer()
.UseRecommendedSerializerSettings();
config.UseActivator(new HangfireActivator(services.BuildServiceProvider().GetRequiredService<IServiceScopeFactory>()));
});
services.AddHangfireServer(options =>
{
// 注意,這里必須設(shè)置非常小的間隔
options.SchedulePollingInterval = TimeSpan.FromSeconds(1);
// 如果考慮到后續(xù)任務(wù)比較多,則需要調(diào)大此參數(shù)
options.WorkerCount = 50;
});
}

第三步
實(shí)現(xiàn) RecurringJobHandler 執(zhí)行定時(shí)任務(wù),發(fā)起 http 請(qǐng)求業(yè)務(wù)系統(tǒng)。
被調(diào)用方要返回 TaskInterfaceResponse 類型,主要考慮如果被調(diào)用方后續(xù)不需要在繼續(xù)此定時(shí)任務(wù),那么返回參數(shù) CancelTask = tre 時(shí),定時(shí)任務(wù)服務(wù)直接取消后續(xù)的任務(wù)即可,不需要被調(diào)用方手動(dòng)調(diào)用接口取消。
public class RecurringJobHandler
{
private readonly IServiceProvider _serviceProvider;
public RecurringJobHandler(IServiceProvider serviceProvider)
{
_serviceProvider = serviceProvider;
}
/// <summary>
/// 執(zhí)行任務(wù).
/// </summary>
/// <param name="taskRequest"></param>
/// <returns>Task.</returns>
public async Task Handler(TaskRequest taskRequest)
{
var ioc = _serviceProvider;
var recurringJobManager = ioc.GetRequiredService<IRecurringJobManager>();
var httpClientFactory = ioc.GetRequiredService<IHttpClientFactory>();
var logger = ioc.GetRequiredService<ILogger<RecurringJobHandler>>();
using var httpClient = httpClientFactory.CreateClient(taskRequest.ServiceName);
// 無論是否請(qǐng)求成功,都算完成了本次任務(wù)
try
{
// 請(qǐng)求子系統(tǒng)的接口
var response = await httpClient.PostAsJsonAsync(taskRequest.ServiceName, taskRequest);
var execteResult = await response.Content.ReadFromJsonAsync<ExecteTasResult>();
// 被調(diào)用方要求取消任務(wù)
if (execteResult != null && execteResult.CancelTask)
{
recurringJobManager.RemoveIfExists(taskRequest.TaskId);
}
}
catch (Exception ex)
{
logger.LogError(ex, "Task error.");
}
}
}
第四步
配置好 Hangfire 后,開始考慮如何接收任務(wù)和發(fā)起請(qǐng)求,首先定義一個(gè) Http 接口或 grpc 接口。
[ApiController]
[Route("/execute")]
public class HangfireController : ControllerBase
{
private readonly IRecurringJobManager _recurringJobManager;
public HangfireController(IRecurringJobManager recurringJobManager)
{
_recurringJobManager = recurringJobManager;
}
[HttpPost("addtask")]
public async Task<TaskResponse> AddTask(TaskRequest value)
{
await Task.CompletedTask;
_recurringJobManager.AddOrUpdate<RecurringJobHandler>(
value.TaskId,
task => task.Handler(value),
cronExpression: value.CronExpression,
options: new RecurringJobOptions
{
});
return new TaskResponse { };
}
[HttpPost("cancel")]
public async Task<TaskResponse> Cancel(CancelTaskRequest value)
{
await Task.CompletedTask;
_recurringJobManager.RemoveIfExists(value.TaskId);
return new TaskResponse
{
};
}
}
業(yè)務(wù)服務(wù)實(shí)現(xiàn)動(dòng)態(tài)代碼
業(yè)務(wù)服務(wù)只需要暴露一個(gè) exceute 接口給 HangfireServer 即可,DemoApi 將 Command 序列化包裝為請(qǐng)求參數(shù)給 HangfireServer ,然后 HangfireServer 原封不動(dòng)地將參數(shù)請(qǐng)求到 exceute 接口。

對(duì) DemoApi 主要設(shè)計(jì)過程如下:
- 定義 SendHangfireService 服務(wù),包裝 Command 數(shù)據(jù)和一些定時(shí)任務(wù)參數(shù),通過 http 發(fā)送到 HangfireServer 中;
- 定義 ExecuteTaskHandler ,當(dāng)接口被觸發(fā)時(shí),實(shí)現(xiàn)反序列化參數(shù)并使用 MediatR 發(fā)送 Command,實(shí)現(xiàn)動(dòng)態(tài)執(zhí)行;
- 定義 ExecuteController 接口,接收 HangfireServer 請(qǐng)求,并調(diào)用 ExecuteTaskHandler 處理請(qǐng)求;
DemoApi 引入類庫(kù)如下-:
<PackageReference Include="Maomi.Core" Version="2.2.0" />
<PackageReference Include="MediatR" Version="12.5.0" />
Maomi.Core 是一個(gè)模塊化和自動(dòng)服務(wù)注冊(cè)框架。
第一步
定義 SendHangfireService 服務(wù),包裝 Command 數(shù)據(jù)和一些定時(shí)任務(wù)參數(shù),通過 http 發(fā)送到 HangfireServer 中。
接收 HangfireServer 請(qǐng)求時(shí),需要通過字符串查找出 Type,這就需要 DemoApi 啟動(dòng)時(shí),自動(dòng)掃描程序集并將對(duì)應(yīng)的類型緩存起來。
為了將定時(shí)任務(wù)命令和其它 Command 區(qū)分處理,需要定義一個(gè)統(tǒng)一的抽象,當(dāng)然也可以不這樣做,也可以通過特性注解的方式做處理。
/// <summary>
/// 定時(shí)任務(wù)抽象參數(shù).
/// </summary>
public abstract class HangfireRequest : IRequest<HangfireResponse>
{
/// <summary>
/// 定時(shí)任務(wù) id.
/// </summary>
public string TaskId { get; init; } = string.Empty;
/// <summary>
/// 該任務(wù)創(chuàng)建時(shí)間.
/// </summary>
public DateTimeOffset CreateTime { get; init; }
}
定義 HangireTypeFactory ,以便能夠通過字符串快速查找 Type。
/// <summary>
/// 記錄 CQRS 中的命令類型,以便能夠通過字符串快速查找 Type.
/// </summary>
public class HangireTypeFactory
{
private readonly ConcurrentDictionary<string, Type> _typeDictionary;
public HangireTypeFactory()
{
_typeDictionary = new ConcurrentDictionary<string, Type>();
}
public void Add(Type type)
{
if (!_typeDictionary.ContainsKey(type.Name))
{
_typeDictionary[type.Name] = type;
}
}
public Type? Get(string typeName)
{
if (_typeDictionary.TryGetValue(typeName, out var type))
{
return type;
}
return _typeDictionary.FirstOrDefault(x => x.Value.FullName == typeName).Value;
}
}
最后實(shí)現(xiàn) SendHangfireService 服務(wù),能夠包裝參數(shù)發(fā)送到 HangfireServer 中。
當(dāng)然,可以使用 CQRS 處理。
/// <summary>
/// 定時(shí)任務(wù)服務(wù),用于發(fā)送定時(shí)任務(wù)請(qǐng)求.
/// </summary>
[InjectOnScoped]
public class SendHangfireService
{
private static readonly JsonSerializerOptions JsonOptions = new JsonSerializerOptions
{
AllowTrailingCommas = true,
PropertyNameCaseInsensitive = true,
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
ReadCommentHandling = JsonCommentHandling.Skip
};
private readonly IHttpClientFactory _httpClientFactory;
public SendHangfireService(IHttpClientFactory httpClientFactory)
{
_httpClientFactory = httpClientFactory;
}
/// <summary>
/// 發(fā)送定時(shí)任務(wù)請(qǐng)求.
/// </summary>
/// <typeparam name="TCommand"></typeparam>
/// <param name="request"></param>
/// <param name="cancellationToken"></param>
/// <returns></returns>
/// <exception cref="TypeLoadException"></exception>
public async Task Send<TCommand>(TCommand request)
where TCommand : HangfireRequest
{
using var httpClient = _httpClientFactory.CreateClient();
var taskRequest = new TaskRequest
{
TaskId = request.TaskId,
CommandBody = JsonSerializer.Serialize(request, JsonOptions),
ServiceName = "http://127.0.0.1:5000/hangfire/execute",
CommandType = typeof(TCommand).Name ?? throw new TypeLoadException(typeof(TCommand).Name),
CreateTime = request.CreateTime.ToUnixTimeMilliseconds().ToString(),
CronExpression = request.CronExpression,
};
_ = await httpClient.PostAsJsonAsync("http://127.0.0.1:5001/execute/addtask", taskRequest);
}
/// <summary>
/// 取消定時(shí)任務(wù).
/// </summary>
/// <param name="taskId"></param>
/// <returns></returns>
public async Task Cancel(string taskId)
{
using var httpClient = _httpClientFactory.CreateClient();
_ = await httpClient.PostAsJsonAsync("http://127.0.0.1:5001/hangfire/cancel", new CancelTaskRequest
{
TaskId = taskId
});
}
}
第二步
要實(shí)現(xiàn)通過 Type 動(dòng)態(tài)執(zhí)行某個(gè) Command ,其實(shí)思路比較簡(jiǎn)單,也并不需要表達(dá)式樹等麻煩的方式。
筆者的實(shí)現(xiàn)思路如下,定義 ExecuteTaskHandler 泛型類,直接以強(qiáng)類型的方式觸發(fā) Command,但是為了屏蔽泛型類型強(qiáng)類型在代碼調(diào)用中的麻煩,需要再抽象一個(gè)接口 IHangfireTaskHandler 屏蔽泛型。
/// <summary>
/// 定義執(zhí)行任務(wù)的抽象,便于忽略泛型處理.
/// </summary>
public interface IHangfireTaskHandler
{
/// <summary>
/// 執(zhí)行任務(wù).
/// </summary>
/// <param name="taskRequest"></param>
/// <returns></returns>
Task<ExecteTasResult> Handler(TaskRequest taskRequest);
}
/// <summary>
/// 用于反序列化參數(shù)并發(fā)送 Command.
/// </summary>
/// <typeparam name="TCommand">命令.</typeparam>
public class ExecuteTaskHandler<TCommand> : IHangfireTaskHandler
where TCommand : HangfireRequest, IRequest<ExecteTasResult>
{
private readonly IMediator _mediator;
/// <summary>
/// Initializes a new instance of the <see cref="ExecuteTaskHandler{TCommand}"/> class.
/// </summary>
/// <param name="mediator"></param>
public ExecuteTaskHandler(IMediator mediator)
{
_mediator = mediator;
}
private static readonly JsonSerializerOptions JsonSerializerOptions = new JsonSerializerOptions
{
AllowTrailingCommas = true,
PropertyNameCaseInsensitive = true,
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
ReadCommentHandling = JsonCommentHandling.Skip
};
/// <inheritdoc/>
public async Task<ExecteTasResult> Handler(TaskRequest taskRequest)
{
var command = JsonSerializer.Deserialize<TCommand>(taskRequest.CommandBody, JsonSerializerOptions)!;
if (command == null)
{
throw new Exception("解析命令參數(shù)失敗");
}
// 處理命令的邏輯
var response = await _mediator.Send(command);
return response;
}
}
第三步
實(shí)現(xiàn)定時(shí)任務(wù) execute 觸發(fā)接口,然后將參數(shù)轉(zhuǎn)發(fā)到 ExecuteTaskHandler 中,這里通過依賴注入的方式屏蔽和解決強(qiáng)類型的問題。
/// <summary>
/// 定時(shí)任務(wù)觸發(fā)入口.
/// </summary>
[ApiController]
[Route("/hangfire")]
public class ExecuteController : ControllerBase
{
private readonly IServiceProvider _serviceProvider;
private readonly HangireTypeFactory _hangireTypeFactory;
public ExecuteController(IServiceProvider serviceProvider, HangireTypeFactory hangireTypeFactory)
{
_serviceProvider = serviceProvider;
_hangireTypeFactory = hangireTypeFactory;
}
[HttpPost("execute")]
public async Task<ExecteTasResult> ExecuteTask([FromBody] TaskRequest request)
{
var commandType = _hangireTypeFactory.Get(request.CommandType);
// 找不到該事件類型,取消后續(xù)事件執(zhí)行
if (commandType == null)
{
return new ExecteTasResult
{
CancelTask = true
};
}
var commandTypeHandler = typeof(ExecuteTaskHandler<>).MakeGenericType(commandType);
var handler = _serviceProvider.GetService(commandTypeHandler) as IHangfireTaskHandler;
if(handler == null)
{
return new ExecteTasResult
{
CancelTask = true
};
}
return await handler.Handler(request);
}
}
第四步
封裝好代碼后,開始最后一個(gè)環(huán)境,配置和注冊(cè)服務(wù),由于筆者使用 Maomi.Core 框架,因此服務(wù)注冊(cè)配置和掃描程序集變得非常簡(jiǎn)單,只需要通過 Maomi.Core 框架提供的接口即可最簡(jiǎn)單地實(shí)現(xiàn)功能。
public class ApiModule : Maomi.ModuleCore, IModule
{
private readonly HangireTypeFactory _hangireTypeFactory;
public ApiModule()
{
_hangireTypeFactory = new HangireTypeFactory();
}
public override void ConfigureServices(ServiceContext context)
{
context.Services.AddTransient(typeof(ExecuteTaskHandler<>));
context.Services.AddSingleton(_hangireTypeFactory);
context.Services.AddHttpClient();
context.Services.AddMediatR(o =>
{
o.RegisterServicesFromAssemblies(context.Modules.Select(x => x.Assembly).ToArray());
});
}
public override void TypeFilter(Type type)
{
if (!type.IsClass || type.IsAbstract)
{
return;
}
if (type.IsAssignableTo(typeof(HangfireRequest)))
{
_hangireTypeFactory.Add(type);
}
}
}

第五步
開發(fā)者可以這樣寫定時(shí)任務(wù) Command 以及執(zhí)行器,然后通過接口觸發(fā)定時(shí)任務(wù)。
public class MyTestRequest : HangfireRequest, IRequest<ExecteTasResult>
{
}
/// <summary>
/// 要被定時(shí)任務(wù)執(zhí)行的代碼.
/// </summary>
public class MyTestHandler : IRequestHandler<MyTestRequest, ExecteTasResult>
{
private static volatile int _count;
private static DateTimeOffset _lastTime;
public async Task<ExecteTasResult> Handle(MyTestRequest request, CancellationToken cancellationToken)
{
_count++;
if (_lastTime == default)
{
_lastTime = DateTimeOffset.Now;
}
Console.WriteLine($"""
執(zhí)行時(shí)間:{DateTimeOffset.Now.ToString("HH:mm:ss.ffff")}
執(zhí)行頻率(每 10s):{(_count / (DateTimeOffset.Now - _lastTime).TotalSeconds * 10)}
""");
return new ExecteTasResult
{
CancelTask = false
};
}
}
[ApiController]
[Route("/test")]
public class SendTaskController : ControllerBase
{
private readonly SendHangfireService _hangfireService;
public SendTaskController(SendHangfireService hangfireService)
{
_hangfireService = hangfireService;
}
[HttpGet("aaa")]
public async Task<string> SendAsync()
{
await _hangfireService.Send(new MyTestRequest
{
CreateTime = DateTimeOffset.Now,
CronExpression = "* * * * * *",
TaskId = Guid.NewGuid().ToString(),
});
return "aaa";
}
}
最后
啟動(dòng)項(xiàng)目測(cè)試代碼,記錄執(zhí)行頻率和時(shí)間間隔。



浙公網(wǎng)安備 33010602011771號(hào)