最近项目里需要在延时队列,但是开源版本rocketmq不支持任意时间延时,造成有些任务无法执行

参考了网上的不少文章,但是都么有实现,所以再开源的基础上改造了个支持任意时间延时的队列。

源码地址: https://gitee.com/venus-suite/rocketmq-with-delivery-time

概念介绍

开源版本中,只有RocketMQ支持延迟消息,且只支持18个特定级别的延迟
付费版本中,阿里云和腾讯云上的MQ产品都支持精度为秒级别的延迟消息

定时消息:Producer将消息发送到消息队列RocketMQ版服务端,但并不期望立马投递这条消息,而是推迟到在当前时间点之后的某一个时间投递到Consumer进行消费,该消息即定时消息。

延时消息:Producer将消息发送到消息队列RocketMQ版服务端,但并不期望立马投递这条消息,而是延迟一定时间后才投递到Consumer进行消费,该消息即延时消息。

定时消息与延时消息在代码配置上存在一些差异,但是最终达到的效果相同:消息在发送到消息队列RocketMQ版服务端后并不会立马投递,而是根据消息中的属性延迟固定时间后才投递给消费者。

实现原理(3种实现方案)

1.代理实现 [链接](https://gitee.com/venus-suite/rocketmq-with-delivery-time/wikis/1.%E4%BB%A3%E7%90%86%E5%AE%9E%E7%8E%B0)
2.时间轮和delay-commit-log实现 [链接](https://gitee.com/venus-suite/rocketmq-with-delivery-time/wikis/2.%E6%97%B6%E9%97%B4%E8%BD%AE%E5%92%8Cdelay-file%E5%AE%9E%E7%8E%B0)
3.时间轮和时间file实现 [链接](https://gitee.com/venus-suite/rocketmq-with-delivery-time/wikis/3.%E6%97%B6%E9%97%B4%E8%BD%AE%E5%92%8C%E7%A7%92%E7%BA%A7%E6%96%87%E4%BB%B6%E5%AE%9E%E7%8E%B0)

## 适用场景

定时消息和延时消息适用于以下一些场景:

消息生产和消费有时间窗口要求,例如在电商交易中超时未支付关闭订单的场景,在订单创建时会发送一条延时消息。

这条消息将会在30分钟以后投递给消费者,消费者收到此消息后需要判断对应的订单是否已完成支付。

如支付未完成,则关闭订单。如已完成支付则忽略。

通过消息触发一些定时任务,例如在某一固定时间点向用户发送提醒消息。

使用方式
定时消息和延时消息的使用在代码编写上存在略微的区别:

发送定时消息需要明确指定消息发送时间点之后的某一时间点作为消息投递的时间点。

发送延时消息时需要设定一个延时时间长度,消息将从当前发送时间点开始延迟固定时间之后才开始投递。

### 注意事项
定时消息的精度会有1s~2s的延迟误差。

定时和延时消息的msg.setStartDeliverTime参数需要设置成当前时间戳之后的某个时刻(单位毫秒)。

如果被设置成当前时间戳之前的某个时刻,消息将立刻投递给消费者。

定时和延时消息的msg.setStartDeliverTime参数可设置40天内的任何时刻(单位毫秒),超过40天消息发送将失败。

StartDeliverTime是服务端开始向消费端投递的时间。如果消费者当前有消息堆积,那么定时和延时消息会排在堆积消息后面,将不能严格按照配置的时间进行投递。

由于客户端和服务端可能存在时间差,消息的实际投递时间与客户端设置的投递时间之间可能存在偏差。

## 如何使用
推荐使用阿里云提供的rocketmq版本的pom

1 ```java
2 <dependency>
3 <groupId>com.aliyun.openservices</groupId>
4 <artifactId>ons-client</artifactId>
5 <version>1.8.4.Final</version>
6 </dependency>
7
8 ```

#### 消息发送

```java

import com.aliyun.openservices.ons.api.*;
import com.aliyun.openservices.shade.org.apache.commons.lang3.time.DateFormatUtils; import java.util.Date;
import java.util.Properties; public class ProducerDelayTest {
public static void main(String[] args) {
Properties properties = new Properties();
// AccessKey ID阿里云身份验证,在阿里云RAM控制台创建。
properties.put(PropertyKeyConst.AccessKey, "XXX");
// AccessKey Secret阿里云身份验证,在阿里云RAM控制台创建。
properties.put(PropertyKeyConst.SecretKey, "XXX");
// 设置TCP接入域名,进入消息队列RocketMQ版控制台实例详情页面的接入点区域查看。
properties.put(PropertyKeyConst.NAMESRV_ADDR, "localhost:9876");
Producer producer = ONSFactory.createProducer(properties);
// 在发送消息前,必须调用start方法来启动Producer,只需调用一次即可。
producer.start(); for(int i=0;i<1;i++) {
Message msg = new Message(
// 您在消息队列RocketMQ版控制台创建的Topic。
"TopicTest",
// Message Tag,可理解为Gmail中的标签,对消息进行再归类,方便Consumer指定过滤条件在消息队列RocketMQ版服务器过滤。
"TagA",
// Message Body可以是任何二进制形式的数据,消息队列RocketMQ版不做任何干预,需要Producer与Consumer协商好一致的序列化和反序列化方式。
"演示15秒钟>>> ".getBytes());
// 设置代表消息的业务关键属性,请尽可能全局唯一。
// 以方便您在无法正常收到消息情况下,可通过控制台查询消息并补发。
// 注意:不设置也不会影响消息正常收发。
msg.setKey("ORDERID_100e");
try {
// 延时消息,单位毫秒(ms),在指定延迟时间(当前时间之后)进行投递,例如消息在15秒后投递。
long delayTime = System.currentTimeMillis() + 15000;
System.out.println("发送时间>>" + DateFormatUtils.format(new Date(), "yyyy-MM-dd HH:mm:ss")); // 设置消息需要被投递的时间。
msg.setStartDeliverTime(delayTime); SendResult sendResult = producer.send(msg);
// 同步发送消息,只要不抛异常就是成功。
if (sendResult != null) {
System.out.println(DateFormatUtils.format(new Date(), "yyyy-MM-dd HH:mm:ss") + " Send mq message success. Topic is:" + msg.getTopic() + " msgId is: " + sendResult.getMessageId());
}
} catch (Exception e) {
// 消息发送失败,需要进行重试处理,可重新发送这条消息或持久化这条数据进行补偿处理。
System.out.println(new Date() + " Send mq message failed. Topic is:" + msg.getTopic());
e.printStackTrace();
}
}
// 在应用退出前,销毁Producer对象。
// 注意:如果不销毁也没有问题。
producer.shutdown();
}
}

```

#### 消息接收

  1 ```java
2
3 import com.aliyun.openservices.shade.org.apache.commons.lang3.time.DateFormatUtils;
4 import org.apache.rocketmq.client.consumer.DefaultMQPushConsumer;
5 import org.apache.rocketmq.client.consumer.listener.ConsumeConcurrentlyContext;
6 import org.apache.rocketmq.client.consumer.listener.ConsumeConcurrentlyStatus;
7 import org.apache.rocketmq.client.consumer.listener.MessageListenerConcurrently;
8 import org.apache.rocketmq.client.exception.MQClientException;
9 import org.apache.rocketmq.common.consumer.ConsumeFromWhere;
10 import org.apache.rocketmq.common.message.MessageExt;
11
12 import java.util.Date;
13 import java.util.List;
14
15 /**
16 * This example shows how to subscribe and consume messages using providing {@link DefaultMQPushConsumer}.
17 */
18 public class Consumer {
19
20 public static void main(String[] args) throws InterruptedException, MQClientException {
21
22 /*
23 * Instantiate with specified consumer group name.
24 */
25
26 final int[] totals = {0};
27 DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("please_rename_unique_group_name_4");
28
29 consumer.setNamesrvAddr("localhost:9876");
30 /*
31 * Specify name server addresses.
32 * <p/>
33 *
34 * Alternatively, you may specify name server addresses via exporting environmental variable: NAMESRV_ADDR
35 * <pre>
36 * {@code
37 * consumer.setNamesrvAddr("name-server1-ip:9876;name-server2-ip:9876");
38 * }
39 * </pre>
40 */
41
42 /*
43 * Specify where to start in case the specified consumer group is a brand new one.
44 */
45 consumer.setConsumeFromWhere(ConsumeFromWhere.CONSUME_FROM_FIRST_OFFSET);
46
47 /*
48 * Subscribe one more more topics to consume.
49 */
50 consumer.subscribe("TopicTest", "*");
51
52 /*
53 * Register callback to execute on arrival of messages fetched from brokers.
54 */
55 consumer.registerMessageListener(new MessageListenerConcurrently() {
56
57 @Override
58 public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> msgs,
59 ConsumeConcurrentlyContext context) {
60 System.out.println("接收到消息:"+ DateFormatUtils.format(new Date(),"yyyy-MM-dd HH:mm:ss"));
61 System.out.printf("%s Receive New Messages: %s %n", Thread.currentThread().getName(), msgs);
62 for(MessageExt m: msgs){
63 System.out.println(">>>"+new String( m.getBody()));
64 }
65 totals[0] +=1;
66 System.out.println(">>>>>total="+ totals[0]);
67 return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
68 }
69 });
70
71 /*
72 * Launch the consumer instance.
73 */
74 consumer.start();
75
76 System.out.printf("Consumer Started.%n");
77 }
78 }
79
80
81 ```
82 ### 如何使用社区版本的rocketmq 发送延迟消息
83
84 ```java
85 /*设置为您在消息队列RocketMQ版控制台创建的Topic。*/
86 Message msg = new Message("YOUR TOPIC",
87 /*设置消息的Tag。*/
88 "YOUR MESSAGE TAG",
89 /*消息内容。*/
90 "Hello world".getBytes(RemotingHelper.DEFAULT_CHARSET));
91 /*发送延时消息,需要设置延时时间,单位毫秒(ms),消息将在指定延时时间后投递,例如消息将在3秒后投递。*/
92 long delayTime = System.currentTimeMillis() + 3000;
93 msg.putUserProperty("__STARTDELIVERTIME", String.valueOf(delayTime));
94
95 /**
96 *若需要发送定时消息,则需要设置定时时间,消息将在指定时间进行投递,例如消息将在2021-08-10 18:45:00投递。
97 *定时时间格式为:yyyy-MM-dd HH:mm:ss,若设置的时间戳在当前时间之前,则消息将被立即投递给Consumer。
98 * long timeStamp = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").parse("2021-08-10 18:45:00").getTime();
99 * msg.putUserProperty("__STARTDELIVERTIME", String.valueOf(timeStamp));
100 */
101 SendResult sendResult = producer.send(msg);
102 System.out.printf("%s%n", sendResult);
103
104 ```

参考文章实现思路:

https://www.cnblogs.com/hzmark/p/mq-delay-msg.html

rocketmq实现延迟队列(精确到秒级)的更多相关文章

  1. rocketmq实现延迟队列精确到秒级实现方案3-时间轮和秒级文件实现

    时间轮和秒级文件实现原理图 这种方案比较简单实现,通过秒级时间,建立对应的文件夹,只要相同的时间超时的消息,就在同一个目录,通过msgid保证文件不重复,等到了时间后,就扫描对应的文件夹的文件,发送到 ...

  2. rocketmq实现延迟队列精确到秒级实现(总结编)

    前言篇: 为了节约成本,决定通过自研来改造rocketmq,添加任意时间延迟的延时队列,开源版本的rocketmq只有支持18个等级的延迟时间, 其实对于大部分的功能是够用了的,但是以前的项目,全部都 ...

  3. rocketmq实现延迟队列精确到秒级实现方案2-时间轮和delay-file实现

    上图是通过RocketMQ源码分析一个实现原理方案示意图. 分为两个部分: 消息的写入消息的Schedule 在写入CommitLog之前,如果是延迟消息,按照每10分钟写入delayfile文件,对 ...

  4. rocketmq实现延迟队列精确到秒级实现方案1-代理实现

    简单的来说,就是rocketmq发送消息到broker的时候,判断是否定时消息, 如果是定时消息,将消息发送到代理服务(这个是一个独立的服务,需要自己开发,定时地把消息发送出去), 当然了消息用什么来 ...

  5. android 可以精确到秒级的时间选择器

    android自带的时间选择器只能精确到分,但是对于某些应用要求选择的时间精确到秒级,此时只有自定义去实现这样的时间选择器了.下面介绍一个可以精确到秒级的时间选择器. 先上效果图: 下面是工程目录: ...

  6. js网页倒计时精确到秒级

    网页实时倒计时,精确到秒级,和天数倒计时原理一样. 一个很好用的js倒计时!网页实时倒计时,精确到秒级,和天数倒计时原理一样.js倒计时一般用于商城网站团购,特卖,很多地方都可用到!希望能够给大家带来 ...

  7. rocketmq之延迟队列(按照18个等级来发送)

    1 启动消费者等待传入的订阅消息 import org.apache.rocketmq.client.consumer.DefaultMQPushConsumer; import org.apache ...

  8. golang gin框架中实现一个简单的不是特别精确的秒级限流器

    起因 看了两篇关于golang中限流器的帖子: Gin 开发实践:如何实现限流中间件 常用限流策略--漏桶与令牌桶介绍 我照着用,居然没效果-- 时间有限没有深究.这实在是一个很简单的功能,我的需求是 ...

  9. 基于Redis实现延迟队列

    背景 在后端服务中,经常有这样一种场景,写数据库操作在异步队列中执行,且这个异步队列是多进程运行的,这时如果对同一资源进行写库操作,很有可能产生数据被覆盖等问题,于是就需要业务层在更新数据库之前进行加 ...

随机推荐

  1. 突出显示(Project)

    <Project2016 企业项目管理实践>张会斌 董方好 编著 当一个大的项目文件做好以后,查看全部内容,肉眼多少会有点吃不消,这时就需要"划重点".在Porect里 ...

  2. 小迪安全 Web安全 基础入门 第六天 - 信息打点-Web架构篇&域名&语言&中间件&数据库&系统&源码获取

    一 . Web架构 语言.常用的Web开发语言有PHP,Java,Python,JavaScript,.net等.具体可参考w3school的介绍. 中间件. (1)常见的Web服务器中间件:IIS. ...

  3. CF135A Replacement 题解

    Content 有 \(n\) 个数 \(a_1,a_2,a_3,...,a_n\),试用 \(1\) ~ \(10^9\) 之间的数(除了本身)代替其中的一个数,使得这 \(n\) 个数的总和最小, ...

  4. vue 判断页面是否滚动到底部

    需求 要求用户阅读完本页所有内容后,下一步按钮才可以点击. 实现思路 通过判断当前页面是否到达底部来设置按钮的点击事件. 要判断当前页面是否到达底部需要用到三个距离--距离顶部的距离scrollTop ...

  5. ACwing1211. 蚂蚁感冒

    题目: 长 100 厘米的细长直杆子上有 n 只蚂蚁. 它们的头有的朝左,有的朝右. 每只蚂蚁都只能沿着杆子向前爬,速度是 1 厘米/秒. 当两只蚂蚁碰面时,它们会同时掉头往相反的方向爬行. 这些蚂蚁 ...

  6. tomcat启动报错There is insufficient memory for the Java Runtime Environment to continue

    tomcat启动报错后显示以下错误 ## There is insufficient memory for the Java Runtime Environment to continue.# Nat ...

  7. c++设计模式概述之备忘录

    代买写的不够规范,,目的是缩短篇幅,实际中请不要这样做. 1.概述 和这个模式相似的生活场景,比如 office的撤销操作.VS  和 xcode等IDE的撤销操作 . 其实都是恢复到上一个或者下一个 ...

  8. C++ 获取函数耗时

    C++ 记录耗时 #include <sys/timeb.h> #include <stdio.h> long long getSystemTime() { struct ti ...

  9. 【LeetCode】147. Insertion Sort List 解题报告(Python)

    [LeetCode]147. Insertion Sort List 解题报告(Python) 标签(空格分隔): LeetCode 作者: 负雪明烛 id: fuxuemingzhu 个人博客: h ...

  10. 教学日志:javaSE-初识java

    一.编译执行第一个java程序 /* 总结: 1.编译执行第一个java程序 步骤如下: 1.安装JDK开发环境: 2.配置环境变量,JAVA_HOME,PATH;--验证环境变量配置是否成功 jav ...