Redis延迟队列的实现

160 阅读1分钟

概述

延迟队列有很多适用场景,比如在订单创建15分钟后未支付,自动取消订单等,而实现延迟队列有很多方案,比如可以利用RabbitMq的死信队列实现。本文将采用 Redis 的 ZSet 数据结构实现一个简易版的延迟队列,代码采用.net7.0,宗旨就是简单易用,直接看代码。

生产端

//delaySetting 代表延迟表达式,1h:1小时后,2m:2分钟后,3s:3秒中后
//也可以直接传入要发送的时间
public async Task<bool> ProduceMessage(string msg, string delaySetting)
{
    var now = DateTimeOffset.UtcNow;
    long score = 0;
    var matchResult = Regex.Match(delaySetting.ToLower(), @"^(\d+)([smhd])$");
    if (matchResult.Success && matchResult.Groups.Count == 3 &&
        int.TryParse(matchResult.Groups[1].Value, out var delayValue)
        && delayValue > 0)
    {
        score = matchResult.Groups[2].Value switch
        {
            "s" => now.AddSeconds(delayValue).ToUnixTimeSeconds(),
            "m" => now.AddMinutes(delayValue).ToUnixTimeSeconds(),
            "h" => now.AddHours(delayValue).ToUnixTimeSeconds(),
            "d" => now.AddDays(delayValue).ToUnixTimeSeconds(),
            _ => 0
        };
    }
    else if (DateTimeOffset.TryParse(delaySetting, out var delayTime) && delayTime > now)
    {
        score = delayTime.ToUnixTimeSeconds();
    }

    if (score <= 0) return false;
    //这里将延迟后的时间戳作为score,传给 ZSet
    await _redisDb.SortedSetAddAsync(ConstantVars.DelayMsgQueue, msg, score);
    return true;
}

消费端

//这里采用后台服务实现,记得在启动代码中注入此服务
public class DelayMsgHostedService : BackgroundService
{
    private readonly IDatabase _redisDb;
    private readonly ILogger _logger;
    private readonly PeriodicTimer _timer;

    public DelayMsgHostedService(ILogger logger)
    {
        _redisDb = SmartRedisClient.Db2;
        _logger = logger;
        _timer = new PeriodicTimer(TimeSpan.FromSeconds(1));
    }

    protected override async Task ExecuteAsync(CancellationToken cancellationToken)
    {
        try
        {
            while (await _timer.WaitForNextTickAsync(cancellationToken))
            {
                await ConsumeMessage();
            }
        }
        catch (Exception ex)
        {
            _logger.LogError($"DelayMsgHostedService:{ex.Message}-{ex.StackTrace}");
        }
    }

    private async Task ConsumeMessage()
    {
        var now = DateTimeOffset.UtcNow.ToUnixTimeSeconds();
        var messagesToProcess = await _redisDb.SortedSetRangeByScoreAsync(ConstantVars.DelayMsgQueue, 0, now);
        foreach (var messageJson in messagesToProcess)
        {
            _logger.LogInformation($"收到延迟消息:{messageJson}");
            //TODO:收到延迟消息,实现业务逻辑
            
        }
    }

    public override Task StopAsync(CancellationToken cancellationToken)
    {
        _timer.Dispose();
        return Task.CompletedTask;
    }
}

总结

  1. 本文中的生产消息和发送消息,Redis Key必须保持一致
  2. 这里巧妙的将时间戳作为 ZSet 的Score,后台扫描的时候,利用 RangeByScore 将当前时间戳传入,这样就可以获取到要消费的消息内容。