概述
延迟队列有很多适用场景,比如在订单创建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;
}
}
总结
- 本文中的生产消息和发送消息,Redis Key必须保持一致
- 这里巧妙的将时间戳作为 ZSet 的Score,后台扫描的时候,利用 RangeByScore 将当前时间戳传入,这样就可以获取到要消费的消息内容。