介绍

具有队列的特性,再给它附加一个延迟消费队列消息的功能,也就是说可以指定队列中的消息在哪个时间点被消费。

使用场景

延时队列在项目中的应用还是比较多的,尤其像电商类平台:

  1. 订单成功后,在30分钟内没有支付,自动取消订单
  2. 外卖平台发送订餐通知,下单成功后60s给用户推送短信。
  3. 如果订单一直处于某一个未完结状态时,及时处理关单,并退还库存
  4. 淘宝新建商户一个月内还没上传商品信息,将冻结商铺等

该介绍来自其他文章

方案

下面的例子没有进行封装,所以代码仅供参考

Redis过期事件

注意:

不保证在设定的过期时间立即删除并发送通知,数据量大的时候会延迟比较大

不保证一定送达

发送即忘策略,不包含持久化

但是比如有些场景,对这个时间不是那么看重,并且有其他措施双层保障,该实现方案是比较简单。

redis自2.8.0之后版本提供Keyspace Notifications功能,允许客户订阅Pub / Sub频道,以便以某种方式接收影响Redis数据集事件。

配置

需要修改配置启用过期事件,比如在windows客户端中,需要修改redis.windows.conf文件,在linux中需要修改redis.conf,修改内容是:

-- 取消注释
notify-keyspace-events Ex -- 注释
#notify-keyspace-events ""

然后重新启动服务器,比如windows

 .\redis-server.exe  .\redis.windows.conf

或者linux中使用docker-compose重新部署redis

  redis:
container_name: redis
image: redis
hostname: redis
restart: always
ports:
- "6379:6379"
volumes:
- $PWD/redis/redis.conf:/etc/redis.conf
- /root/common-docker-compose/redis/data:/data
command:
/bin/bash -c "redis-server /etc/redis.conf" #启动执行指定的redis.conf文件

然后使用客户端订阅事件

-- windows
.\redis-cli -- linux
docker exec -it 容器标识 redis-cli psubscribe __keyevent@0__:expired

控制台订阅

使用StackExchange.Redis组件订阅过期事件

var connectionMultiplexer = ConnectionMultiplexer.Connect(_redisConnection);
var db = connectionMultiplexer.GetDatabase(0); db.StringSet("orderno:123456", "订单创建", TimeSpan.FromSeconds(10));
Console.WriteLine("开始订阅"); var subscriber = connectionMultiplexer.GetSubscriber(); //订阅库0的过期通知事件
subscriber.Subscribe("__keyevent@0__:expired", (channel, key) =>
{
Console.WriteLine($"key过期 channel:{channel} key:{key}");
}); Console.ReadLine();

输出结果:

key过期 channel:keyevent@0:expired key:orderno:123456

如果启动多个客户端监听,那么多个客户端都可以收到过期事件。

WebApi中订阅

创建RedisListenService继承自:BackgroundService

public class RedisListenService : BackgroundService
{
private readonly ISubscriber _subscriber; public RedisListenService(IServiceScopeFactory serviceScopeFactory)
{
using var scope = serviceScopeFactory.CreateScope();
var configuration = scope.ServiceProvider.GetRequiredService<IConfiguration>(); var connectionMultiplexer = ConnectionMultiplexer.Connect(configuration["redis"]);
var db = connectionMultiplexer.GetDatabase(0);
_subscriber = connectionMultiplexer.GetSubscriber();
} protected override Task ExecuteAsync(CancellationToken stoppingToken)
{
//订阅库0的过期通知事件
_subscriber.Subscribe("__keyevent@0__:expired", (channel, key) =>
{
Console.WriteLine($"key过期 channel:{channel} key:{key}");
}); return Task.CompletedTask;
}
}

注册该后台服务

services.AddHostedService<RedisListenService>();

启用项目,给redis指定库设置值,等过期后会接收到过期通知事件。

RabbitMq延迟队列

版本信息 Rabbitmq版本:3.10.5 Erlang版本:24.3.4.2

要使用rabbitmq做延迟是需要安装插件(rabbitmq_delayed_message_exchange)的

插件介绍:https://blog.rabbitmq.com/posts/2015/04/scheduling-messages-with-rabbitmq

下载地址:https://github.com/rabbitmq/rabbitmq-delayed-message-exchange/releases

将下载好的插件(d:/Download/rabbitmq_delayed_message_exchange-3.10.2.ez)映射到容器的plugins目录下:

docker run -d --name myrabbit -p 9005:15672 -p 5672:5672  -e RABBITMQ_DEFAULT_VHOST=customer -e RABBITMQ_DEFAULT_USER=admin -e RABBITMQ_DEFAULT_PASS=123456 -v d:/Download/rabbitmq_delayed_message_exchange-3.10.2.ez:/plugins/rabbitmq_delayed_message_exchange-3.10.2.ez  rabbitmq:3-management-alpine

进入容器

docker exec -it 容器名称/标识 bash

启用插件

rabbitmq-plugins enable rabbitmq_delayed_message_exchange

查看是否启用

rabbitmq-plugins list

[E]和[e]表示启用,然后重启服务

rabbitmq-server restart

然后在管理界面添加交换机都可以看到

生产消息

发送的消息类型是:x-delayed-message

[HttpGet("send/delay")]
public string SendDelayedMessage()
{
var factory = new ConnectionFactory()
{
HostName = "localhost",//IP地址
Port = 5672,//端口号
UserName = "admin",//用户账号
Password = "123456",//用户密码
VirtualHost = "customer"
};
using var connection = factory.CreateConnection();
using var channel = connection.CreateModel(); var exchangeName = "delay-exchange";
var routingkey = "delay.delay";
var queueName = "delay_queueName"; //设置Exchange队列类型
var argMaps = new Dictionary<string, object>()
{
{"x-delayed-type", "topic"}
};
//设置当前消息为延时队列
channel.ExchangeDeclare(exchange: exchangeName, type: "x-delayed-message", true, false, argMaps);
channel.QueueDeclare(queueName, true, false, false, argMaps);
channel.QueueBind(queueName, exchangeName, routingkey); var time = 1000 * 5;
var message = $"发送时间为 {DateTime.Now:yyyy-MM-dd HH:mm:ss} 延时时间为:{time}";
var body = Encoding.UTF8.GetBytes(message);
var props = channel.CreateBasicProperties();
//设置消息的过期时间
props.Headers = new Dictionary<string, object>()
{
{ "x-delay", time }
};
channel.BasicPublish(exchange: exchangeName, routingKey: routingkey, basicProperties: props, body: body);
Console.WriteLine("成功发送消息:" + message); return "success";
}

消费消息

消费消息我是弄了一个后台任务(RabbitmqDelayedHostService)在处理

public class RabbitmqDelayedHostService : BackgroundService
{
private readonly IModel _channel;
private readonly IConnection _connection; public RabbitmqDelayedHostService()
{
var connFactory = new ConnectionFactory//创建连接工厂对象
{
HostName = "localhost",//IP地址
Port = 5672,//端口号
UserName = "admin",//用户账号
Password = "123456",//用户密码
VirtualHost = "customer"
};
_connection = connFactory.CreateConnection();
_channel = _connection.CreateModel(); //交换机名称
var exchangeName = "exchangeDelayed";
var queueName = "delay_queueName";
var routingkey = "delay.delay";
var argMaps = new Dictionary<string, object>()
{
{"x-delayed-type", "topic"}
};
_channel.ExchangeDeclare(exchange: exchangeName, type: "x-delayed-message", true, false, argMaps);
_channel.QueueDeclare(queueName, true, false, false, argMaps);
_channel.QueueBind(queue: queueName, exchange: exchangeName, routingKey: routingkey);
//声明为手动确认
_channel.BasicQos(0, 1, false);
} protected override Task ExecuteAsync(CancellationToken stoppingToken)
{
var queueName = "delay_queueName"; var consumer = new EventingBasicConsumer(_channel);
consumer.Received += (model, ea) =>
{
var message = Encoding.UTF8.GetString(ea.Body.ToArray());
var routingKey = ea.RoutingKey;
Console.WriteLine($"接受到消息的时间为 {DateTime.Now:yyyy-MM-dd HH:mm:ss},routingKey:{routingKey} message:{message} "); //手动确认
_channel.BasicAck(ea.DeliveryTag, true);
};
_channel.BasicConsume(queue: queueName, autoAck: false, consumer: consumer); return Task.CompletedTask;
} public override void Dispose()
{
_connection.Dispose();
_channel.Dispose();
base.Dispose();
}
}

注册该后台任务

services.AddHostedService<RabbitmqDelayedHostService>();

输出结果

成功发送消息:发送时间为 2022-07-02 18:54:22 延时时间为:5000

成功发送消息:发送时间为 2022-07-02 18:54:22 延时时间为:5000

成功发送消息:发送时间为 2022-07-02 18:54:22 延时时间为:5000

成功发送消息:发送时间为 2022-07-02 18:54:23 延时时间为:5000

成功发送消息:发送时间为 2022-07-02 18:54:23 延时时间为:5000

成功发送消息:发送时间为 2022-07-02 18:54:23 延时时间为:5000

接受到消息的时间为 2022-07-02 18:54:27,routingKey:delay.delay message:发送时间为 2022-07-02 18:54:22 延时时间为:5000

接受到消息的时间为 2022-07-02 18:54:27,routingKey:delay.delay message:发送时间为 2022-07-02 18:54:22 延时时间为:5000

接受到消息的时间为 2022-07-02 18:54:27,routingKey:delay.delay message:发送时间为 2022-07-02 18:54:22 延时时间为:5000

接受到消息的时间为 2022-07-02 18:54:28,routingKey:delay.delay message:发送时间为 2022-07-02 18:54:23 延时时间为:5000

接受到消息的时间为 2022-07-02 18:54:28,routingKey:delay.delay message:发送时间为 2022-07-02 18:54:23 延时时间为:5000

接受到消息的时间为 2022-07-02 18:54:28,routingKey:delay.delay message:发送时间为 2022-07-02 18:54:23 延时时间为:5000

其他方案

  • Hangfire延迟队列
BackgroundJob.Schedule(
() => Console.WriteLine("Delayed!"),
TimeSpan.FromDays(7));
  • 时间轮
  • Redisson DelayQueue
  • 计时管理器

.Net之延迟队列的更多相关文章

  1. C#实现rabbitmq 延迟队列功能

    最近在研究rabbitmq,项目中有这样一个场景:在用户要支付订单的时候,如果超过30分钟未支付,会把订单关掉.当然我们可以做一个定时任务,每个一段时间来扫描未支付的订单,如果该订单超过支付时间就关闭 ...

  2. Java 延迟队列使用

    延时队列,第一他是个队列,所以具有对列功能第二就是延时,这就是延时对列,功能也就是将任务放在该延时对列中,只有到了延时时刻才能从该延时对列中获取任务否则获取不到…… 应用场景比较多,比如延时1分钟发短 ...

  3. Spring Boot(十四)RabbitMQ延迟队列

    一.前言 延迟队列的使用场景:1.未按时支付的订单,30分钟过期之后取消订单:2.给活跃度比较低的用户间隔N天之后推送消息,提高活跃度:3.过1分钟给新注册会员的用户,发送注册邮件等. 实现延迟队列的 ...

  4. 使用netty HashedWheelTimer构建简单延迟队列

    背景 最近项目中有个业务,需要对用户新增任务到期后进行业务处理.使用定时任务定时扫描过期时间,浪费资源,且不实时.只能使用延时队列处理. DelayQueue 第一想到的是java自带的延时队列del ...

  5. php使用redis的有序集合zset实现延迟队列

    延迟队列就是个带延迟功能的消息队列,相对于普通队列,它可以在指定时间消费掉消息. 延迟队列的应用场景: 1.新用户注册,10分钟后发送邮件或站内信. 2.用户下单后,30分钟未支付,订单自动作废. 我 ...

  6. C# RabbitMQ延迟队列功能实战项目演练

    一.需求背景 当用户在商城上进行下单支付,我们假设如果8小时没有进行支付,那么就后台自动对该笔交易的状态修改为订单关闭取消,同时给用户发送一份邮件提醒.那么我们应用程序如何实现这样的需求场景呢?在之前 ...

  7. rabbitmq延迟队列demo

    1. demo详解 1.1 工程结构: 1.2 pom 定义jar包依赖的版本.版本很重要,rabbit依赖spring,两者必须相一致,否则报错: <properties> <sp ...

  8. PHP 订单延时处理:延迟队列(未鉴定)

    PHP 订单延时处理:延迟队列: https://github.com/chenlinzhong/php-delayqueue

  9. Spring RabbitMQ 延迟队列

    一.说明 在实际业务场景中可能会用到延时消息发送,例如异步回调失败时的重发机制. RabbitMQ本身不具有延时消息队列的功能,但是可以通过rabbitmq-delayed-message-excha ...

  10. JUC——延迟队列

    所谓的延迟队列最大的特征是它可以自动通过队列进行脱离,例如:现在有一些对象被临时保存着,但是有可能该集合对象是一个公共对象,那么里面的某些数据如果不在使用的时候就希望其可以在指定的时间达到后自动的消失 ...

随机推荐

  1. 技术管理进阶——一线Leader怎么做?经理的速成宝典

    原创不易,求分享.求一键三连 本期培训材料关注公众号后回复:经理培训,获得 前段时间有个同学问我有没有一线Leader的速成培训课程,很好的问题,首先我们需要定义一下什么是小Leader: 所谓小Le ...

  2. Ubuntu 百度飞桨和 CUDA 的安装

    Ubuntu 百度飞桨 和 CUDA 的安装 1.简介 本文主要是 Ubuntu 百度飞桨 和 CUDA 的安装 系统:Ubuntu 20.04 百度飞桨:2.2 为例 2.百度飞桨安装 访问百度飞桨 ...

  3. 什么?你们公司还没有将JVM初始和最大堆内存大小设置为相同值?

    微信公众号:Java大家族 JVM将初始和最大内存大小设置为相同值的好处 启动应用程序时,我们指定初始内存大小和最大内存大小.对于在 JVM(Java 虚拟机)上运行的应用程序,初始和最大内存大小通过 ...

  4. pgpool-II 4.3 中文手册 - 入门教程

    本章解释了如何开始使用 Pgpool-II. 安装 在本节中,我们假设您已经安装了 Pgpool-II 与 PostgreSQL 集群. 你的第一个复制(Replication) 在本节中,我们将解释 ...

  5. js 如何判断变量类型

    自定义一些类型 const boolean = true const string = '这是一个字符串' const number = 1 const array = [] const object ...

  6. NoClassDefFoundError问题

    问题: 遇到一个问题,报NoClassDefFoundError,如下图: NoClassDefFoundError和ClassNotFoundException区别 我们经常被java.lang.C ...

  7. 好客租房15-jsx中的条件渲染

    jsx中的条件渲染 场景:loding效果 条件渲染:根据条件渲染特定的jsx结构 可以使用if/else或者三元运算符和逻辑和运算符实现 //导入react import React from &q ...

  8. 将汇总结果导出到MySQL

    ①mysql建表test1 ②cd /opt/module/sqoop进入scoop路径 ③ bin/sqoop export \ > --connect jdbc:mysql://master ...

  9. python之模块(os、sys、json、subprocess)

    目录 os模块 sys模块 json模块 subprocess模块 os模块 os模块主要是与操作系统打交道. 导入os模块 import os 创建单层文件夹,路径必须要存在 os.mkdir(路径 ...

  10. java 5种IO模型

    每日一句 人的痛苦会把自己折磨到多深呢? 每日一句 You cannot swim for new horizons until you have courage to lose sight of t ...