.NET6實(shí)現(xiàn)分布式定時(shí)任務(wù)的完整方案
1. 基礎(chǔ)設(shè)施層
分布式鎖服務(wù)
// IDistributedLockService.cs public interface IDistributedLockService { ValueTask<IAsyncDisposable?> AcquireLockAsync(string resourceKey, TimeSpan expiryTime); } // RedisDistributedLockService.cs public class RedisDistributedLockService : IDistributedLockService { private readonly IConnectionMultiplexer _redis; private readonly ILogger<RedisDistributedLockService> _logger; public RedisDistributedLockService( IConnectionMultiplexer redis, ILogger<RedisDistributedLockService> logger) { _redis = redis; _logger = logger; } public async ValueTask<IAsyncDisposable?> AcquireLockAsync(string resourceKey, TimeSpan expiryTime) { var db = _redis.GetDatabase(); var lockToken = Guid.NewGuid().ToString(); var lockKey = $"distributed-lock:{resourceKey}"; try { var acquired = await db.LockTakeAsync(lockKey, lockToken, expiryTime); if (acquired) { _logger.LogDebug("成功獲取分布式鎖 {LockKey}", lockKey); return new RedisLockHandle(db, lockKey, lockToken, _logger); } _logger.LogDebug("無法獲取分布式鎖 {LockKey}", lockKey); return null; } catch (Exception ex) { _logger.LogError(ex, "獲取分布式鎖 {LockKey} 時(shí)發(fā)生錯(cuò)誤", lockKey); throw; } } private sealed class RedisLockHandle : IAsyncDisposable { private readonly IDatabase _db; private readonly string _lockKey; private readonly string _lockToken; private readonly ILogger _logger; private bool _isDisposed; public RedisLockHandle( IDatabase db, string lockKey, string lockToken, ILogger logger) { _db = db; _lockKey = lockKey; _lockToken = lockToken; _logger = logger; } public async ValueTask DisposeAsync() { if (_isDisposed) return; try { var released = await _db.LockReleaseAsync(_lockKey, _lockToken); if (!released) { _logger.LogWarning("釋放分布式鎖 {LockKey} 失敗", _lockKey); } else { _logger.LogDebug("成功釋放分布式鎖 {LockKey}", _lockKey); } } catch (Exception ex) { _logger.LogError(ex, "釋放分布式鎖 {LockKey} 時(shí)發(fā)生錯(cuò)誤", _lockKey); } finally { _isDisposed = true; } } } }
2. 任務(wù)服務(wù)層
定時(shí)任務(wù)服務(wù)
// IPollingService.cs public interface IPollingService { Task ExecutePollingTasksAsync(); Task ExecuteDailyTaskAsync(int hour); } // PollingService.cs public class PollingService : IPollingService { private readonly IDistributedLockService _lockService; private readonly ILogger<PollingService> _logger; public PollingService( IDistributedLockService lockService, ILogger<PollingService> logger) { _lockService = lockService; _logger = logger; } [DisableConcurrentExecution(timeoutInSeconds: 60 * 30)] // 30分鐘防并發(fā) public async Task ExecutePollingTasksAsync() { await using var lockHandle = await _lockService.AcquireLockAsync( "polling-tasks-lock", TimeSpan.FromMinutes(25)); // 鎖有效期25分鐘 if (lockHandle is null) { _logger.LogInformation("其他節(jié)點(diǎn)正在執(zhí)行輪詢?nèi)蝿?wù),跳過本次執(zhí)行"); return; } try { _logger.LogInformation("開始執(zhí)行輪詢?nèi)蝿?wù) - 節(jié)點(diǎn): {NodeId}", Environment.MachineName); // 執(zhí)行所有輪詢?nèi)蝿?wù) await Task.WhenAll( PollingTaskAsync(), PollingExpireTaskAsync(), PollingExpireDelCharactTaskAsync() ); // 觸發(fā)后臺(tái)任務(wù) _ = BackgroundTask.Run(() => PollingDelCharactTaskAsync(), _logger); _ = BackgroundTask.Run(() => AutoCheckApiAsync(), _logger); _ = BackgroundTask.Run(() => DelLogsAsync(), _logger); } catch (Exception ex) { _logger.LogError(ex, "執(zhí)行輪詢?nèi)蝿?wù)時(shí)發(fā)生錯(cuò)誤"); throw; } } [DisableConcurrentExecution(timeoutInSeconds: 60 * 60)] // 1小時(shí)防并發(fā) public async Task ExecuteDailyTaskAsync(int hour) { var lockKey = $"daily-task-{hour}:{DateTime.UtcNow:yyyyMMdd}"; await using var lockHandle = await _lockService.AcquireLockAsync( lockKey, TimeSpan.FromMinutes(55)); // 鎖有效期55分鐘 if (lockHandle is null) { _logger.LogInformation("其他節(jié)點(diǎn)已執(zhí)行今日 {Hour} 點(diǎn)任務(wù)", hour); return; } try { _logger.LogInformation("開始執(zhí)行 {Hour} 點(diǎn)任務(wù) - 節(jié)點(diǎn): {NodeId}", hour, Environment.MachineName); if (hour == 21) { await ExecuteNightlyMaintenanceAsync(); } else if (hour == 4) { await ExecuteEarlyMorningTasksAsync(); } } catch (Exception ex) { _logger.LogError(ex, "執(zhí)行 {Hour} 點(diǎn)任務(wù)時(shí)發(fā)生錯(cuò)誤", hour); throw; } } // 具體任務(wù)實(shí)現(xiàn)方法 private async Task PollingTaskAsync() { // 實(shí)現(xiàn)游戲角色啟動(dòng)/關(guān)閉邏輯 } private async Task ExecuteNightlyMaintenanceAsync() { // 21點(diǎn)特殊任務(wù)邏輯 } // 其他方法... } // BackgroundTask.cs (安全運(yùn)行后臺(tái)任務(wù)) public static class BackgroundTask { public static Task Run(Func<Task> task, ILogger logger) { return Task.Run(async () => { try { await task(); } catch (Exception ex) { logger.LogError(ex, "后臺(tái)任務(wù)執(zhí)行失敗"); } }); } }
3. 任務(wù)調(diào)度配置層
任務(wù)初始化器
// RecurringJobInitializer.cs public class RecurringJobInitializer : IHostedService { private readonly IRecurringJobManager _jobManager; private readonly IServiceProvider _services; private readonly ILogger<RecurringJobInitializer> _logger; public RecurringJobInitializer( IRecurringJobManager jobManager, IServiceProvider services, ILogger<RecurringJobInitializer> logger) { _jobManager = jobManager; _services = services; _logger = logger; } public Task StartAsync(CancellationToken cancellationToken) { try { using var scope = _services.CreateScope(); var pollingService = scope.ServiceProvider.GetRequiredService<IPollingService>(); // 每30分鐘執(zhí)行的任務(wù) _jobManager.AddOrUpdate<IPollingService>( "polling-tasks-30min", s => s.ExecutePollingTasksAsync(), "*/30 * * * *"); // 每天21:00執(zhí)行的任務(wù) _jobManager.AddOrUpdate<IPollingService>( "daily-task-21:00", s => s.ExecuteDailyTaskAsync(21), "0 21 * * *"); // 每天04:00執(zhí)行的任務(wù) _jobManager.AddOrUpdate<IPollingService>( "daily-task-04:00", s => s.ExecuteDailyTaskAsync(4), "0 4 * * *"); _logger.LogInformation("周期性任務(wù)初始化完成"); } catch (Exception ex) { _logger.LogError(ex, "初始化周期性任務(wù)失敗"); throw; } return Task.CompletedTask; } public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask; }
4. 應(yīng)用啟動(dòng)配置
Program.cs
var builder = WebApplication.CreateBuilder(args); // 添加Redis builder.Services.AddSingleton<IConnectionMultiplexer>(sp => ConnectionMultiplexer.Connect(builder.Configuration.GetConnectionString("Redis"))); // 配置Hangfire builder.Services.AddHangfire(config => { config.UseRedisStorage( builder.Configuration.GetConnectionString("Redis"), new RedisStorageOptions { Prefix = "hangfire:", Db = 1 // 使用單獨(dú)的Redis數(shù)據(jù)庫 }); config.UseColouredConsoleLogProvider(); }); builder.Services.AddHangfireServer(options => { options.ServerName = $"{Environment.MachineName}:{Guid.NewGuid():N}"; options.WorkerCount = 1; options.Queues = new[] { "default", "critical" }; }); // 注冊(cè)服務(wù) builder.Services.AddSingleton<IDistributedLockService, RedisDistributedLockService>(); builder.Services.AddScoped<IPollingService, PollingService>(); builder.Services.AddHostedService<RecurringJobInitializer>(); var app = builder.Build(); // 配置Hangfire儀表盤 app.UseHangfireDashboard("/jobs", new DashboardOptions { DashboardTitle = "任務(wù)調(diào)度中心", Authorization = new[] { new HangfireDashboardAuthorizationFilter() }, StatsPollingInterval = 60_000 // 60秒刷新一次 }); app.Run(); // Hangfire儀表盤授權(quán)過濾器 public class HangfireDashboardAuthorizationFilter : IDashboardAuthorizationFilter { public bool Authorize(DashboardContext context) { var httpContext = context.GetHttpContext(); return httpContext.User.Identity?.IsAuthenticated == true; } }
5. appsettings.json 配置
{ "ConnectionStrings": { "Redis": "localhost:6379,allowAdmin=true", "Hangfire": "Server=(localdb)\\mssqllocaldb;Database=Hangfire;Trusted_Connection=True;" }, "Hangfire": { "WorkerCount": 1, "SchedulePollingInterval": 5000 } }
關(guān)鍵設(shè)計(jì)說明
1.分布式鎖:
- 使用Redis RedLock算法實(shí)現(xiàn)
- 自動(dòng)處理鎖的獲取和釋放
- 包含完善的錯(cuò)誤處理和日志記錄
2.任務(wù)隔離:
- 使用Hangfire的[DisableConcurrentExecution]防止同一任務(wù)重復(fù)執(zhí)行
- 分布式鎖確??绻?jié)點(diǎn)唯一執(zhí)行
3.錯(cuò)誤處理:
- 所有關(guān)鍵操作都有try-catch和日志記錄
- 后臺(tái)任務(wù)使用安全包裝器執(zhí)行
4.可觀測(cè)性:
- 詳細(xì)的日志記錄
- Hangfire儀表盤監(jiān)控
5.擴(kuò)展性:
- 可以輕松添加新任務(wù)
- 支持動(dòng)態(tài)調(diào)整調(diào)度策略
這個(gè)實(shí)現(xiàn)方案完全符合.NET 6的最佳實(shí)踐,支持分布式部署,確保任務(wù)在集群環(huán)境中安全可靠地執(zhí)行。
到此這篇關(guān)于.NET6實(shí)現(xiàn)分布式定時(shí)任務(wù)的完整方案的文章就介紹到這了,更多相關(guān).NET6分布式定時(shí)任務(wù)內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
使用C# CefSharp Python采集某網(wǎng)站簡歷并且自動(dòng)發(fā)送邀請(qǐng)短信的方法
這篇文章主要給大家介紹了關(guān)于如何使用C# CefSharp Python采集某網(wǎng)站簡歷并且自動(dòng)發(fā)送邀請(qǐng)短信的相關(guān)資料,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面來一起看看吧2019-03-03C#中const,readonly和static關(guān)鍵字的用法介紹
這篇文章介紹了C#中const,readonly和static關(guān)鍵字的用法,文中通過示例代碼介紹的非常詳細(xì)。對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2022-08-08C#連接Mysql數(shù)據(jù)庫詳細(xì)教程(內(nèi)附Mysql及Navicat)
這篇文章主要給大家介紹了C#連接Mysql數(shù)據(jù)庫詳細(xì)教程(內(nèi)附Mysql及Navicat),文中通過代碼示例和圖文介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作有一定的幫助,需要的朋友可以參考下2023-10-10C#實(shí)現(xiàn)Stripe支付的方法實(shí)踐
本文主要介紹了C#實(shí)現(xiàn)Stripe支付的方法實(shí)踐,文中通過示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2022-02-02C# Web應(yīng)用調(diào)試開啟外部訪問步驟解析
本文主要介紹了C# Web應(yīng)用調(diào)試開啟外部訪問的實(shí)現(xiàn)過程與方法。具有一定的參考價(jià)值,下面跟著小編一起來看下吧2017-01-01