完整的 .NET 6 分布式定时任务实现(Hangfire + Redis 分布式锁)

完整的 .NET 6 分布式定时任务实现(Hangfire + Redis 分布式锁)

以下是完整的解决方案,包含所有必要组件:

1. 基础设施层

1.1 分布式锁服务

csharp 复制代码
// 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} 时发生错误", 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} 时发生错误", _lockKey);
            }
            finally
            {
                _isDisposed = true;
            }
        }
    }
}

2. 任务服务层

2.1 定时任务服务

csharp 复制代码
// 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分钟防并发
    public async Task ExecutePollingTasksAsync()
    {
        await using var lockHandle = await _lockService.AcquireLockAsync(
            "polling-tasks-lock",
            TimeSpan.FromMinutes(25)); // 锁有效期25分钟

        if (lockHandle is null)
        {
            _logger.LogInformation("其他节点正在执行轮询任务,跳过本次执行");
            return;
        }

        try
        {
            _logger.LogInformation("开始执行轮询任务 - 节点: {NodeId}", Environment.MachineName);
            
            // 执行所有轮询任务
            await Task.WhenAll(
                PollingTaskAsync(),
                PollingExpireTaskAsync(),
                PollingExpireDelCharactTaskAsync()
            );
            
            // 触发后台任务
            _ = BackgroundTask.Run(() => PollingDelCharactTaskAsync(), _logger);
            _ = BackgroundTask.Run(() => AutoCheckApiAsync(), _logger);
            _ = BackgroundTask.Run(() => DelLogsAsync(), _logger);
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "执行轮询任务时发生错误");
            throw;
        }
    }

    [DisableConcurrentExecution(timeoutInSeconds: 60 * 60)] // 1小时防并发
    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("其他节点已执行今日 {Hour} 点任务", hour);
            return;
        }

        try
        {
            _logger.LogInformation("开始执行 {Hour} 点任务 - 节点: {NodeId}", 
                hour, Environment.MachineName);
            
            if (hour == 21)
            {
                await ExecuteNightlyMaintenanceAsync();
            }
            else if (hour == 4)
            {
                await ExecuteEarlyMorningTasksAsync();
            }
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "执行 {Hour} 点任务时发生错误", hour);
            throw;
        }
    }

    // 具体任务实现方法
    private async Task PollingTaskAsync()
    {
        // 实现游戏角色启动/关闭逻辑
    }
    
    private async Task ExecuteNightlyMaintenanceAsync()
    {
        // 21点特殊任务逻辑
    }
    
    // 其他方法...
}

// BackgroundTask.cs (安全运行后台任务)
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, "后台任务执行失败");
            }
        });
    }
}

3. 任务调度配置层

3.1 任务初始化器

csharp 复制代码
// 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分钟执行的任务
            _jobManager.AddOrUpdate<IPollingService>(
                "polling-tasks-30min",
                s => s.ExecutePollingTasksAsync(),
                "*/30 * * * *");
            
            // 每天21:00执行的任务
            _jobManager.AddOrUpdate<IPollingService>(
                "daily-task-21:00",
                s => s.ExecuteDailyTaskAsync(21),
                "0 21 * * *");
            
            // 每天04:00执行的任务
            _jobManager.AddOrUpdate<IPollingService>(
                "daily-task-04:00",
                s => s.ExecuteDailyTaskAsync(4),
                "0 4 * * *");

            _logger.LogInformation("周期性任务初始化完成");
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "初始化周期性任务失败");
            throw;
        }

        return Task.CompletedTask;
    }

    public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask;
}

4. 应用启动配置

4.1 Program.cs

csharp 复制代码
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 // 使用单独的Redis数据库
        });
    
    config.UseColouredConsoleLogProvider();
});

builder.Services.AddHangfireServer(options =>
{
    options.ServerName = $"{Environment.MachineName}:{Guid.NewGuid():N}";
    options.WorkerCount = 1;
    options.Queues = new[] { "default", "critical" };
});

// 注册服务
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 = "任务调度中心",
    Authorization = new[] { new HangfireDashboardAuthorizationFilter() },
    StatsPollingInterval = 60_000 // 60秒刷新一次
});

app.Run();

// Hangfire仪表盘授权过滤器
public class HangfireDashboardAuthorizationFilter : IDashboardAuthorizationFilter
{
    public bool Authorize(DashboardContext context)
    {
        var httpContext = context.GetHttpContext();
        return httpContext.User.Identity?.IsAuthenticated == true;
    }
}

5. appsettings.json 配置

json 复制代码
{
  "ConnectionStrings": {
    "Redis": "localhost:6379,allowAdmin=true",
    "Hangfire": "Server=(localdb)\\mssqllocaldb;Database=Hangfire;Trusted_Connection=True;"
  },
  "Hangfire": {
    "WorkerCount": 1,
    "SchedulePollingInterval": 5000
  }
}

关键设计说明

  1. 分布式锁:

    • 使用Redis RedLock算法实现
    • 自动处理锁的获取和释放
    • 包含完善的错误处理和日志记录
  2. 任务隔离:

    • 使用Hangfire的[DisableConcurrentExecution]防止同一任务重复执行
    • 分布式锁确保跨节点唯一执行
  3. 错误处理:

    • 所有关键操作都有try-catch和日志记录
    • 后台任务使用安全包装器执行
  4. 可观测性:

    • 详细的日志记录
    • Hangfire仪表盘监控
  5. 扩展性:

    • 可以轻松添加新任务
    • 支持动态调整调度策略

这个实现方案完全符合.NET 6的最佳实践,支持分布式部署,确保任务在集群环境中安全可靠地执行。

相关推荐
我不会起名字3228 分钟前
Redis 从 0 到 1(三十七·收官):36 篇浓缩成 1 张知识地图 + 50 项生产 Checklist
redis·go·知识体系·checklist·收官
2601_967987372 小时前
安装 Redis 或 Redis Stack
linux·windows·redis·macos·安装
小番茄程序猿3 小时前
Redis 编码压测 59 轮:HSET 快 23 倍是假象,SMEMBERS 慢 30 倍是真的
redis·后端
鶴哥只手遮天4 小时前
《Redis 8.10.3 深度剖析》系列 ·第 13 章 Set:三种编码、集合运算与随机采样
redis·后端·开源
学以智用6 小时前
ASP.NET MVC5(.NET Framework)底层完整运行逻辑
.net
瑞克-桑切斯7 小时前
redis
数据库·redis·缓存
花间相见9 小时前
【计算基础|Redis02】【上手】五大数据类型命令逐条实操
数据库·redis·后端
时间的拾荒人10 小时前
Redis 入门详解:核心特性、重要文件与全局命令实战
开发语言·redis·面试
学以智用10 小时前
FilterAttribute(ASP.NET Core MVC 过滤器特性)
.net
小南家的青蛙10 小时前
基于Spring Boot 2.x开发的微服务访问Redis的方案
redis·springboot