rocketmq实现延迟队列精确到秒级实现(总结编)
前言篇:
为了节约成本,决定通过自研来改造rocketmq,添加任意时间延迟的延时队列,开源版本的rocketmq只有支持18个等级的延迟时间,
其实对于大部分的功能是够用了的,但是以前的项目,全部都是使用了阿里云的rocketmq,原因是不同的供应商的订单的延时时间是不同的
(部分供应商的订单未支付30分钟取消,有些1个半小时取消,各种时间都有),
所以使用了大量的延时队列,但是开源版本不支持任意时间延时(希望官方支持这个功能)
为了实现这个功能,网上查询了不少资料,查询到不少相关的文章,主要实现都是基于时间轮来实现的,
但是比较少开源的代码实现(也许大家都没有这个需求吧)
debug实践篇:
1. 撸起袖子加油干,首先,下载源代码 https://github.com/apache/rocketmq.git,导入ide
运行mvn package 生成jar包,如果成功的话,会生成到distribution目录下面

2. 查看文档,发现要运行namesvr 和 broker
找到 src\main\java\org\apache\rocketmq\namesrv\NamesrvStartup.java ,开心的执行main方法,
哦哦哦哦哦,果然报错了,提示 rocketmq.home.dir 目录不存在,查看源码, 原来是从system.propeties读取的,
为了调试,我毫不犹豫的加上了配置文件,

再次运行,不报错了,控制台显示,成功啦(生活是多么美好,空气是多么清晰!)

3.运行 broker ,打开 src\main\java\org\apache\rocketmq\broker\BrokerStartup.java,执行main方法,
添加 配置文件 (D:\\mq\\rocketmq-rocketmq-all-4.9.2是我本地的路径,你要修改成自己的)
1 System.setProperty("rocketmq.home.dir", "D:\\mq\\rocketmq-rocketmq-all-4.9.2\\rb");
2 System.setProperty("user.home", "D:\\mq\\rocketmq-rocketmq-all-4.9.2\\rb\\home\\");

运行一下,成功了,开心的发一条消息,试试,哦哦哦哦哦。发不出去哦(人生最痛苦的事情是,快要成功了,却没有成功)。
原来还要配置namesvr地址,在启动命令,添加 -n localhost:9876 ( 上面的namesvr 启动的ip和端口)

4.漫长的改造之路 (我们是勇敢的斯巴达勇士,一直勇往直前)
用了阿里云的延时队列,发现它的message 可以传一个时间过来(任意的延时时间)
来来来,我们复制一下(不要告诉别人,我们一直是复制,粘贴的,没有原创, 嘘 ......)
1 /**
2 * 该类预定义一些系统键.
3 */
4 static public class SystemPropKey {
5 public static final String TAG = "__TAG";
6 public static final String KEY = "__KEY";
7 public static final String MSGID = "__MSGID";
8 public static final String SHARDINGKEY = "__SHARDINGKEY";
9 public static final String RECONSUMETIMES = "__RECONSUMETIMES";
10 public static final String BORNTIMESTAMP = "__BORNTIMESTAMP";
11 public static final String BORNHOST = "__BORNHOST";
12 /**
13 * 设置消息的定时投递时间(绝对时间). <p>例1: 延迟投递, 延迟3s投递, 设置为: System.currentTimeMillis() + 3000; <p>例2: 定时投递,
14 * 2016-02-01 11:30:00投递, 设置为: new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").parse("2016-02-01
15 * 11:30:00").getTime()
16 */
17 public static final String STARTDELIVERTIME = "__STARTDELIVERTIME";
18 }
/**
* <p> 设置消息的定时投递时间(绝对时间),最大延迟时间为7天. </p> <ol> <li>延迟投递: 延迟3s投递, 设置为: System.currentTimeMillis() + 3000;</li>
* <li>定时投递: 2016-02-01 11:30:00投递, 设置为: new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").parse("2016-02-01
* 11:30:00").getTime()</li> </ol>
*/
public void setStartDeliverTime(final long value) {
putSystemProperties(SystemPropKey.STARTDELIVERTIME, String.valueOf(value));
}
5.既然要改造rocketmq,在哪里改呢,debug,debug,debug(一直到天荒地老),功夫不负有心人,找到啦,
找到 \src\main\java\org\apache\rocketmq\broker\processor\SendMessageProcessor.java, 发现
public RemotingCommand processRequest(ChannelHandlerContext ctx,
RemotingCommand request) throws RemotingCommandException {
SendMessageContext mqtraceContext;
switch (request.getCode()) {
case RequestCode.CONSUMER_SEND_MSG_BACK:
return this.consumerSendMsgBack(ctx, request);
default:
SendMessageRequestHeader requestHeader = parseRequestHeader(request);
if (requestHeader == null) {
return null;
} mqtraceContext = buildMsgContext(ctx, requestHeader);
this.executeSendMessageHookBefore(ctx, request, mqtraceContext); RemotingCommand response;
if (requestHeader.isBatch()) {
response = this.sendBatchMessage(ctx, request, mqtraceContext, requestHeader);
} else {
response = this.sendMessage(ctx, request, mqtraceContext, requestHeader);
} this.executeSendMessageHookAfter(response, mqtraceContext);
return response;
}
}
继续debug,发现 sendMessage 就是处理发送消息的,
如果我们在这里判断是否延时消息就写入文件,然后返回成功到客户端,等到了时间就发送延迟消息,不就搞定了吗?
oh,yes,就是这么干的
//处理延迟消息 delay message
String startTime = msgInner.getProperty(Message.SystemPropKey.STARTDELIVERTIME);
boolean isDelayMsg = false;
long nextStartTime = 0;
if (startTime != null && msgInner.getDelayTimeLevel() <= 0) {
nextStartTime = Long.parseLong(startTime);
if (nextStartTime >= System.currentTimeMillis()) {
isDelayMsg = true;
}
}
if (isDelayMsg) {
return delayProcessor.handlePutMessageResultFuture(response, request, msgInner, ctx, queueIdInt, nextStartTime);
} else {
if (traFlag != null && Boolean.parseBoolean(traFlag)) {
if (this.brokerController.getBrokerConfig().isRejectTransactionMessage()) {
response.setCode(ResponseCode.NO_PERMISSION);
response.setRemark(
"the broker[" + this.brokerController.getBrokerConfig().getBrokerIP1()
+ "] sending transaction message is forbidden");
return response;
}
putMessageResult = this.brokerController.getTransactionalMessageService().prepareMessage(msgInner);
} else {
putMessageResult = this.brokerController.getMessageStore().putMessage(msgInner);
}
return handlePutMessageResult(putMessageResult, response, request, msgInner, responseHeader, sendMessageContext, ctx, queueIdInt);
}
}
其中 delayProcessor.handlePutMessageResultFuture 是我们用来处理延迟消息的地方
我们按照每个时间一个文件夹来保存延时消息,等延时消息到达后,定时的写入延时队列里面。
详细原理,请查考 rocketmq 原理实现篇 https://www.cnblogs.com/tomj2ee/p/15815186.html
package org.apache.rocketmq.broker.delay; import io.netty.channel.ChannelHandlerContext;
import org.apache.commons.lang3.time.DateFormatUtils;
import org.apache.rocketmq.broker.BrokerController;
import org.apache.rocketmq.common.protocol.ResponseCode;
import org.apache.rocketmq.common.protocol.header.SendMessageResponseHeader;
import org.apache.rocketmq.logging.InternalLogger;
import org.apache.rocketmq.logging.InternalLoggerFactory;
import org.apache.rocketmq.remoting.protocol.RemotingCommand;
import org.apache.rocketmq.store.MessageExtBrokerInner; import java.io.*;
import java.net.InetSocketAddress;
import java.net.SocketAddress;
import java.util.Date;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.ThreadLocalRandom; public class DelayProcessor implements Runnable { protected static final InternalLogger log = InternalLoggerFactory.getLogger(DelayProcessor.class.getCanonicalName()); protected final BrokerController brokerController;
protected final SocketAddress storeHost; private ExecutorService jobTaskExecute = Executors.newFixedThreadPool(16); public DelayProcessor(final BrokerController brokerController) {
this.brokerController = brokerController;
this.storeHost = new InetSocketAddress(brokerController.getBrokerConfig().getBrokerIP1(), brokerController
.getNettyServerConfig().getListenPort());
Thread thread = new Thread(this);
thread.setName("delayProcessor-run---thread"); thread.setDaemon(true);
new File(getDelayPath()).mkdirs();
thread.start();
Thread missCallThread = new Thread(() -> {
try {
for(;;) {
Thread.sleep(10 * 1000);
sendMissCallMsg();
}
} catch (InterruptedException e) {
e.printStackTrace();
}
});
missCallThread.setName("delayProcessor-callback-thread");
missCallThread.start();
System.out.println("init delay success " +getDelayPath()); } public RemotingCommand handlePutMessageResultFuture(RemotingCommand response,
RemotingCommand request,
MessageExtBrokerInner msgInner,
ChannelHandlerContext ctx,
int queueIdInt, long nextStartTime) {
return handlePutMessageResult(response, request, msgInner, ctx, queueIdInt, nextStartTime); } private RemotingCommand handlePutMessageResult(RemotingCommand response,
RemotingCommand request, MessageExtBrokerInner msg,
ChannelHandlerContext ctx,
int queueIdInt, long nextStartTime) {
boolean svOk = saveMsgFile(nextStartTime, msg);
SendMessageResponseHeader sendMessageResponseHeader = new SendMessageResponseHeader();
sendMessageResponseHeader.setQueueId(1);
sendMessageResponseHeader.setMsgId("0");
sendMessageResponseHeader.setQueueOffset(0l);
sendMessageResponseHeader.setTransactionId("");
RemotingCommand newCommand = RemotingCommand.createRequestCommand(ResponseCode.SUCCESS, sendMessageResponseHeader); if (svOk) {
newCommand.setCode(ResponseCode.SUCCESS);
} else {
newCommand.setCode(ResponseCode.SYSTEM_ERROR);
newCommand.setRemark("发送消息延迟失败!");
}
newCommand.setExtFields(request.getExtFields());
newCommand.setVersion(response.getVersion());
newCommand.setOpaque(response.getOpaque());
newCommand.setLanguage(response.getLanguage());
newCommand.setBody(request.getBody()); if (!request.isOnewayRPC()) {
try {
ctx.writeAndFlush(newCommand);
} catch (Throwable e) {
log.error("DelayProcessor process request over, but response failed", e);
log.error(request.toString());
log.error(response.toString());
}
}
return newCommand;
} public void putMessage(MessageExtBrokerInner msgInner) {
this.brokerController.getMessageStore().putMessage(msgInner);
} @Override
public void run() {
for (; ; ) {
long curTime = System.currentTimeMillis() / 1000;
jobTaskExecute.submit(() -> sendMsg(curTime));
try {
Thread.sleep(1000);
} catch (InterruptedException e) { }
}
} private String getDelayPath() {
String delayPath = "./delay-store"+ File.separator + "delay";
return delayPath;
} private boolean saveMsgFile(long startTime, MessageExtBrokerInner msgInner) { ObjectOutputStream objectOutputStream = null;
try {
String msgId =(startTime/1000 )+"-"+ System.currentTimeMillis() + "-" + ThreadLocalRandom.current().nextInt(99999999);
System.out.println( getCurrentTime()+"写入延迟消息 >>" + msgId);
String parentDir = getDelayPath() + File.separator + startTime / 1000;
File parentFile = new File(parentDir);
if (!parentFile.exists()) {
parentFile.mkdirs();
}
String fileName = parentDir + File.separator + msgId; FileOutputStream fos = new FileOutputStream(fileName);
BufferedOutputStream bos = new BufferedOutputStream(fos);
objectOutputStream = new ObjectOutputStream(bos);
objectOutputStream.writeObject(msgInner);
return true;
} catch (Exception ex) {
log.error("saveMsgFile ex:", ex);
return false;
} finally {
try {
if (objectOutputStream != null) {
objectOutputStream.close();
}
} catch (Exception ex) {
log.error("saveMsgFile ex:", ex);
}
} } private MessageExtBrokerInner readFile(File f) {
ObjectInputStream ois = null;
try {
ois = new ObjectInputStream(new FileInputStream(f));
return (MessageExtBrokerInner) ois.readObject();
} catch (Exception ex) {
return null;
} finally {
if (ois != null) {
try {
ois.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
} private void sendMissCallMsg() {
File lst = new File(getDelayPath());
File[] files = lst.listFiles();
long startTime = System.currentTimeMillis() / 1000 - 10 * 1000;
for (File f : files) {
String name = f.getName();
if (f.isDirectory() && !name.equals(".") && !name.equals("..")) {
try {
Long fileTime = Long.parseLong(name);
if (fileTime <= startTime) {
sendMsg(fileTime);
}
} catch (Exception ex) {
}
}
} } private String getCurrentTime(){
return Thread.currentThread().getName()+ ">>["+DateFormatUtils.format(new Date(), "yyyy-MM-dd HH:mm:ss")+"] ";
}
private void sendMsg(long startTime) {
File lst = new File(getDelayPath() + File.separator + startTime);
File[] files = lst.listFiles();
if (files != null) {
for (File f : files) {
System.out.println( getCurrentTime()+"时间到发送>> "+ startTime+" to commitLog " + f.getName());
MessageExtBrokerInner msgInner = readFile(f);
if (msgInner != null) {
putMessage(msgInner);
System.out.println( getCurrentTime()+"写入log >> "+ startTime+" to commitLog " + f.getName()+" success");
f.delete();
}
}
lst.delete();
}
}
}
总结:rocketmq延迟队列实现主要是通过时间轮和文件来保存延时消息,等到了时间后,再写入延时队列,来达到延时的目的。
总共有4种方式来实现延时队列,可以参考延时队列的实现原理篇
https://www.cnblogs.com/tomj2ee/p/15815157.html
rocketmq实现延迟队列精确到秒级实现(总结编)的更多相关文章
- rocketmq实现延迟队列精确到秒级实现方案3-时间轮和秒级文件实现
时间轮和秒级文件实现原理图 这种方案比较简单实现,通过秒级时间,建立对应的文件夹,只要相同的时间超时的消息,就在同一个目录,通过msgid保证文件不重复,等到了时间后,就扫描对应的文件夹的文件,发送到 ...
- rocketmq实现延迟队列精确到秒级实现方案2-时间轮和delay-file实现
上图是通过RocketMQ源码分析一个实现原理方案示意图. 分为两个部分: 消息的写入消息的Schedule 在写入CommitLog之前,如果是延迟消息,按照每10分钟写入delayfile文件,对 ...
- rocketmq实现延迟队列精确到秒级实现方案1-代理实现
简单的来说,就是rocketmq发送消息到broker的时候,判断是否定时消息, 如果是定时消息,将消息发送到代理服务(这个是一个独立的服务,需要自己开发,定时地把消息发送出去), 当然了消息用什么来 ...
- android 可以精确到秒级的时间选择器
android自带的时间选择器只能精确到分,但是对于某些应用要求选择的时间精确到秒级,此时只有自定义去实现这样的时间选择器了.下面介绍一个可以精确到秒级的时间选择器. 先上效果图: 下面是工程目录: ...
- js网页倒计时精确到秒级
网页实时倒计时,精确到秒级,和天数倒计时原理一样. 一个很好用的js倒计时!网页实时倒计时,精确到秒级,和天数倒计时原理一样.js倒计时一般用于商城网站团购,特卖,很多地方都可用到!希望能够给大家带来 ...
- rocketmq实现延迟队列(精确到秒级)
最近项目里需要在延时队列,但是开源版本rocketmq不支持任意时间延时,造成有些任务无法执行 参考了网上的不少文章,但是都么有实现,所以再开源的基础上改造了个支持任意时间延时的队列. 源码地址: h ...
- rocketmq之延迟队列(按照18个等级来发送)
1 启动消费者等待传入的订阅消息 import org.apache.rocketmq.client.consumer.DefaultMQPushConsumer; import org.apache ...
- golang gin框架中实现一个简单的不是特别精确的秒级限流器
起因 看了两篇关于golang中限流器的帖子: Gin 开发实践:如何实现限流中间件 常用限流策略--漏桶与令牌桶介绍 我照着用,居然没效果-- 时间有限没有深究.这实在是一个很简单的功能,我的需求是 ...
- 基于Redis实现延迟队列
背景 在后端服务中,经常有这样一种场景,写数据库操作在异步队列中执行,且这个异步队列是多进程运行的,这时如果对同一资源进行写库操作,很有可能产生数据被覆盖等问题,于是就需要业务层在更新数据库之前进行加 ...
随机推荐
- 从头造轮子:asyncio之 run_until_complete (1)
前言 今天开始聊一聊python3的asyncio.关于asyncio,大家肯定都有自己的理解,并且网上大神也把基础概念也解释的比较透彻. 本文写作的初衷,主要是理解asyncio的原理并且实现一遍. ...
- Java的垃圾回收机制:强制回收System.gc() Runtime.getTime().gc()
垃圾回收 当引用类型的实体,如对象.数组等不再被任何变量引用的时候.这块占用的内存就成为了垃圾.JVM会根据自己的策略决定是回收内存 注意: 垃圾回收只回收内存中的对象,无法回收物理资源(数据库连接, ...
- MLNX网卡驱动安装
安装/升级MLNX驱动 1. 安装准备 驱动下载地址:https://www.mellanox.com/products/ethernet-drivers/linux/mlnx_en 选择和系统版本匹 ...
- 百度地图AK密钥申请
注册登录 :http://lbsyun.baidu.com/apiconsole/key#/home 然后点击提交 这个就是AK密钥
- 【LeetCode】283. Move Zeroes 解题报告(Java & Python)
作者: 负雪明烛 id: fuxuemingzhu 个人博客: http://fuxuemingzhu.cn/ 目录 题目描述 题目大意 解题方法 方法一:首尾指针 方法二:头部双指针+双循环 方法三 ...
- 【LeetCode】856. Score of Parentheses 解题报告(Python & C++)
作者: 负雪明烛 id: fuxuemingzhu 个人博客: http://fuxuemingzhu.cn/ 目录 题目描述 题目大意 解题方法 栈 递归 计数 日期 题目地址:https://le ...
- 【九度OJ】题目1191:矩阵最大值 解题报告
[九度OJ]题目1191:矩阵最大值 解题报告 标签(空格分隔): 九度OJ http://ac.jobdu.com/problem.php?pid=1191 题目描述: 编写一个程序输入一个mXn的 ...
- 过河(状态压缩,dp)
描述 在河上有一座独木桥,一只青蛙想沿着独木桥从河的一侧跳到另一侧.在桥上有一些石子,青蛙很讨厌踩在这些石子上.由于桥的长度和青蛙一次跳过的距离都是正整数,我们可以把独木桥上青蛙可能到达的点看成数轴上 ...
- KKT (LICQ)
目录 基本内容 LICQ 假设 KKT 定理 KKT定理的证明 引理A Farkas 引理 推论 KKT定理的证明 H. E. Krogstad, TMA 4180 Optimeringsteori ...
- Golang项目的配置管理——Viper简易入门配置
Golang项目的配置管理--Viper简易入门配置 What is Viper? From:https://github.com/spf13/viper Viper is a complete co ...