rocketmq 消息指定_【源码】RocketMQ如何实现获取指定消息

本文围绕RocketMQ消息查询展开,介绍了消息查询是根据msgId从MQ取消息。分析了在分布式结构中客户端查询消息的方式,还对消息ID解析、长连接客户端RPC实现、查询消息处理器实现以及消息从CommitLog文件读取等源码进行了详细解读。

概要

消息查询是什么?

消息查询就是根据用户提供的msgId从MQ中取出该消息

RocketMQ如果有多个节点如何查询?

问题:RocketMQ分布式结构中,数据分散在各个节点,即便是同一Topic的数据,也未必都在一个broker上。客户端怎么知道数据该去哪个节点上查?

猜想1:逐个访问broker节点查询数据

猜想2:有某种数据中心存在,该中心知道所有消息存储的位置,只要向该中心查询即可得到消息具体位置,进而取得消息内容

实际:

1.消息Id中含有消息所在的broker的地址信息(IP\Port)以及该消息在CommitLog中的偏移量。

2.客户端实现会从msgId字符串中解析出broker地址,向指定broker节查询消息。

问题:CommitLog文件有多个,只有偏移量估计不能确定在哪个文件吧?

实际:单个Broker节点内offset是全局唯一的,不是每个CommitLog文件的偏移量都是从0开始的。单个节点内所有CommitLog文件共用一套偏移量,每个文件的文件名为其第一个消息的偏移量。所以可以根据偏移量和文件名确定CommitLog文件。

源码阅读

0.使用方式

MessageExt  msg = consumer.viewMessage(msgId);

1.消息ID解析

这个了解下就可以了

public classMessageId {privateSocketAddress address;private longoffset;public MessageId(SocketAddress address, longoffset) {this.address =address;this.offset =offset;

}//get-set

}//from MQAdminImpl.java

publicMessageExt viewMessage(

String msgId)throwsRemotingException, MQBrokerException, InterruptedException, MQClientException {

MessageId messageId= null;try{//从msgId字符串中解析出address和offset//address = ip:port//offset为消息在CommitLog文件中的偏移量

messageId =MessageDecoder.decodeMessageId(msgId);

}catch(Exception e) {throw new MQClientException(ResponseCode.NO_MESSAGE, "query message by id finished, but no message.");

}return this.mQClientFactory.getMQClientAPIImpl().viewMessage(RemotingUtil.socketAddress2String(messageId.getAddress()),

messageId.getOffset(), timeoutMillis);

}//from MessageDecoder.java

public static MessageId decodeMessageId(final String msgId) throwsUnknownHostException {

SocketAddress address;longoffset;//ipv4和ipv6的区别//如果msgId总长度超过32字符,则为ipv6

int ipLength = msgId.length() == 32 ? 4 * 2 : 16 * 2;byte[] ip = UtilAll.string2bytes(msgId.substring(0, ipLength));byte[] port = UtilAll.string2bytes(msgId.substring(ipLength, ipLength + 8));

ByteBuffer bb=ByteBuffer.wrap(port);int portInt = bb.getInt(0);

address= newInetSocketAddress(InetAddress.getByAddress(ip), portInt);//offset

byte[] data = UtilAll.string2bytes(msgId.substring(ipLength + 8, ipLength + 8 + 16));

bb=ByteBuffer.wrap(data);

offset= bb.getLong(0);return newMessageId(address, offset);

}

2.长连接客户端RPC实现

要发请求首先得先建立连接,这里方法可以看到创建连接相关的操作。值得注意的是,第一次访问的时候可能连接还没建立,建立连接需要消耗一段时间。代码中对这个时间也做了判断,如果连接建立完成后,发现已经超时,则不再发出请求。目的应该是尽可能减少请求线程的阻塞时间。

//from NettyRemotingClient.java

@Overridepublic RemotingCommand invokeSync(String addr, final RemotingCommand request, longtimeoutMillis)throwsInterruptedException, RemotingConnectException, RemotingSendRequestException, RemotingTimeoutException {long beginStartTime =System.currentTimeMillis();//这里会先检查有无该地址的通道,有则返回,无则创建

final Channel channel = this.getAndCreateChannel(addr);if (channel != null &&channel.isActive()) {try{//前置钩子

doBeforeRpcHooks(addr, request);//判断通道建立完成时是否已到达超时时间,如果超时直接抛出异常。不发请求

long costTime = System.currentTimeMillis() -beginStartTime;if (timeoutMillis

}//同步调用

RemotingCommand response = this.invokeSyncImpl(channel, request, timeoutMillis -costTime);//后置钩子

doAfterRpcHooks(RemotingHelper.parseChannelRemoteAddr(channel), request, response); //后置钩子

returnresponse;

}catch(RemotingSendRequestException e) {

log.warn("invokeSync: send request exception, so close the channel[{}]", addr);this.closeChannel(addr, channel);throwe;

}catch(RemotingTimeoutException e) {if(nettyClientConfig.isClientCloseSocketIfTimeout()) {this.closeChannel(addr, channel);

log.warn("invokeSync: close socket because of timeout, {}ms, {}", timeoutMillis, addr);

}

log.warn("invokeSync: wait response timeout exception, the channel[{}]", addr);throwe;

}

}else{this.closeChannel(addr, channel);throw newRemotingConnectException(addr);

}

}

下一步看看它的同步调用做了什么处理。注意到它会构建一个Future对象加入待响应池,发出请求报文后就挂起线程,然后等待唤醒(waitResponse内部使用CountDownLatch等待)。

//from NettyRemotingAbstract.java

public RemotingCommand invokeSyncImpl(final Channel channel, finalRemotingCommand request,final longtimeoutMillis)throwsInterruptedException, RemotingSendRequestException, RemotingTimeoutException {//请求id

final int opaque =request.getOpaque();try{//请求存根

final ResponseFuture responseFuture = new ResponseFuture(channel, opaque, timeoutMillis, null, null);//加入待响应的请求池

this.responseTable.put(opaque, responseFuture);final SocketAddress addr =channel.remoteAddress();//将请求发出,成功发出时更新状态

channel.writeAndFlush(request).addListener(newChannelFutureListener() {

@Overridepublic void operationComplete(ChannelFuture f) throwsException {if (f.isSuccess()) { //若成功发出,更新请求状态为“已发出”

responseFuture.setSendRequestOK(true);return;

}else{

responseFuture.setSendRequestOK(false);

}//若发出失败,则从池中移除(没用了,释放资源)

responseTable.remove(opaque);

responseFuture.setCause(f.cause());//putResponse的时候会唤醒等待的线程

responseFuture.putResponse(null);

log.warn("send a request command to channel failed.");

}

});//只等待一段时间,不会一直等下去//若正常响应,则收到响应后,此线程会被唤醒,继续执行下去//若超时,则到达该时间后线程苏醒,继续执行

RemotingCommand responseCommand =responseFuture.waitResponse(timeoutMillis);if (null ==responseCommand) {if(responseFuture.isSendRequestOK()) {throw newRemotingTimeoutException(RemotingHelper.parseSocketAddressAddr(addr), timeoutMillis,

responseFuture.getCause());

}else{throw newRemotingSendRequestException(RemotingHelper.parseSocketAddressAddr(addr), responseFuture.getCause());

}

}returnresponseCommand;

}finally{//正常响应完成时,将future释放(正常逻辑)//超时时,将future释放。这个请求已经作废了,后面如果再收到响应,就可以直接丢弃了(由于找不到相关的响应钩子,就不处理了)

this.responseTable.remove(opaque);

}

}

好,我们再来看看收到报文的时候是怎么处理的。我们都了解JDK中的Future的原理,大概就是将这个任务提交给其他线程处理,该线程处理完毕后会将结果写入到Future对象中,写入时如果有线程在等待该结果,则唤醒这些线程。这里也差不多,只不过执行线程在服务端,服务执行完毕后会将结果通过长连接发送给客户端,客户端收到后根据报文中的ID信息从待响应池中找到Future对象,然后就是类似的处理了。

class NettyClientHandler extends SimpleChannelInboundHandler{//底层解码完毕得到RemotingCommand的报文

@Overrideprotected void channelRead0(ChannelHandlerContext ctx, RemotingCommand msg) throwsException {

processMessageReceived(ctx, msg);

}

}public void processMessageReceived(ChannelHandlerContext ctx, RemotingCommand msg) throwsException {final RemotingCommand cmd =msg;if (cmd != null) {//判断类型

switch(cmd.getType()) {caseREQUEST_COMMAND:

processRequestCommand(ctx, cmd);break;caseRESPONSE_COMMAND:

processResponseCommand(ctx, cmd);break;default:break;

}

}

}public voidprocessResponseCommand(ChannelHandlerContext ctx, RemotingCommand cmd) {//取得消息id

final int opaque =cmd.getOpaque();//从待响应池中取得对应请求

final ResponseFuture responseFuture =responseTable.get(opaque);if (responseFuture != null) {//将响应值注入到ResponseFuture对象中,等待线程可从这个对象获取结果

responseFuture.setResponseCommand(cmd);//请求已处理完毕,释放该请求

responseTable.remove(opaque);//如果有回调函数的话则回调(由当前线程处理)

if (responseFuture.getInvokeCallback() != null) {

executeInvokeCallback(responseFuture);

}else{//没有的话,则唤醒等待线程(由等待线程做处理)

responseFuture.putResponse(cmd);

responseFuture.release();

}

}else{

log.warn("receive response, but not matched any request, " +RemotingHelper.parseChannelRemoteAddr(ctx.channel()));

log.warn(cmd.toString());

}

}

总结一下,客户端的处理时序大概是这样的:

结构大概是这样的:

3.服务端的处理

第一步先从收到报文开始,经过底层解码后,进站的最后一个Handler-NettyServerHandler类将收到解码后的RemotingCommand报文。

class NettyServerHandler extends SimpleChannelInboundHandler{

@Overrideprotected void channelRead0(ChannelHandlerContext ctx, RemotingCommand msg) throwsException {

processMessageReceived(ctx, msg);

}

}

处理类同样是NettyRemotingAbscract.java。不过这里报文的类型是请求,故将调用processRequestCommand方法进行处理。

//from NettyRemotingAbscract.java

public void processMessageReceived(ChannelHandlerContext ctx, RemotingCommand msg) throwsException {final RemotingCommand cmd =msg;if (cmd != null) {switch(cmd.getType()) {case REQUEST_COMMAND: //服务端走这里

processRequestCommand(ctx, cmd);break;caseRESPONSE_COMMAND:

processResponseCommand(ctx, cmd);break;default:break;

}

}

}

服务端收到请求后,先判断有无该请求类型关联的处理类,如果没有则告知客户端请求类型不支持。有的话则构建任务,提交给处理器关联的线程池处理(服务端NIO线程不处理业务)。提交的时候带上Channel信息,这样得到结果后,处理线程就可以直接通过channel将响应结果写回了。

//from NettyRemotingAbscract.java

public void processRequestCommand(final ChannelHandlerContext ctx, finalRemotingCommand cmd) {//查看有无该请求code相关的处理器

final Pair matched = this.processorTable.get(cmd.getCode());//如果没有,则使用默认处理器(可能没有默认处理器)

final Pair pair = null == matched ? this.defaultRequestProcessor : matched;final int opaque =cmd.getOpaque();if (pair != null) {//构建任务

Runnable run = newRunnable() {

@Overridepublic voidrun() {try{

doBeforeRpcHooks(RemotingHelper.parseChannelRemoteAddr(ctx.channel()), cmd);final RemotingResponseCallback callback = newRemotingResponseCallback() {

@Overridepublic voidcallback(RemotingCommand response) {

doAfterRpcHooks(RemotingHelper.parseChannelRemoteAddr(ctx.channel()), cmd, response);if (!cmd.isOnewayRPC()) {if (response != null) { //不为null,则由本类将响应值写会给请求方

response.setOpaque(opaque);

response.markResponseType();try{

ctx.writeAndFlush(response);

}catch(Throwable e) {

log.error("process request over, but response failed", e);

log.error(cmd.toString());

log.error(response.toString());

}

}else { //为null,意味着processor内部已经将响应处理了,这里无需再处理。

}

}

}

};if (pair.getObject1() instanceofAsyncNettyRequestProcessor) {

AsyncNettyRequestProcessor processor=(AsyncNettyRequestProcessor)pair.getObject1();

processor.asyncProcessRequest(ctx, cmd, callback);

}else{

NettyRequestProcessor processor=pair.getObject1();

RemotingCommand response=processor.processRequest(ctx, cmd);

doAfterRpcHooks(RemotingHelper.parseChannelRemoteAddr(ctx.channel()), cmd, response);

callback.callback(response);

}

}catch(Throwable e) {

log.error("process request exception", e);

log.error(cmd.toString());if (!cmd.isOnewayRPC()) {final RemotingCommand response =RemotingCommand.createResponseCommand(RemotingSysResponseCode.SYSTEM_ERROR,

RemotingHelper.exceptionSimpleDesc(e));

response.setOpaque(opaque);

ctx.writeAndFlush(response);

}

}

}

};if(pair.getObject1().rejectRequest()) {final RemotingCommand response =RemotingCommand.createResponseCommand(RemotingSysResponseCode.SYSTEM_BUSY,"[REJECTREQUEST]system busy, start flow control for a while");

response.setOpaque(opaque);

ctx.writeAndFlush(response);return;

}try{//将任务提交给处理器的线程池处理(NIO线程只提交任务,不处理业务)

final RequestTask requestTask = newRequestTask(run, ctx.channel(), cmd);

pair.getObject2().submit(requestTask);

}catch(RejectedExecutionException e) {if ((System.currentTimeMillis() % 10000) == 0) {

log.warn(RemotingHelper.parseChannelRemoteAddr(ctx.channel())+ ", too many requests and system thread pool busy, RejectedExecutionException "

+pair.getObject2().toString()+ " request code: " +cmd.getCode());

}if (!cmd.isOnewayRPC()) {final RemotingCommand response =RemotingCommand.createResponseCommand(RemotingSysResponseCode.SYSTEM_BUSY,"[OVERLOAD]system busy, start flow control for a while");

response.setOpaque(opaque);

ctx.writeAndFlush(response);

}

}

}else{

String error= " request type " + cmd.getCode() + " not supported";final RemotingCommand response =RemotingCommand.createResponseCommand(RemotingSysResponseCode.REQUEST_CODE_NOT_SUPPORTED, error);

response.setOpaque(opaque);

ctx.writeAndFlush(response);

log.error(RemotingHelper.parseChannelRemoteAddr(ctx.channel())+error);

}

}

我们再来看看查询消息的处理器是如何实现的。这个类是QueryMessageProcessor,它支持两种查询方式,我们这里使用的是根据msgId直接查询,故调用viewMessageById进行处理。

//from QueryMessageProcesor.java

@OverridepublicRemotingCommand processRequest(ChannelHandlerContext ctx, RemotingCommand request)throwsRemotingCommandException {switch(request.getCode()) {caseRequestCode.QUERY_MESSAGE:return this.queryMessage(ctx, request);case RequestCode.VIEW_MESSAGE_BY_ID: //通过msgId查询消息

return this.viewMessageById(ctx, request);default:break;

}return null;

}

viewMessageById内部,则是根据客户端提供的偏移量读取对应的消息。这里读取到消息内容后将构造一个RemotingCommand报文回送给客户端。

//from QueryMessageProcesor.java

publicRemotingCommand viewMessageById(ChannelHandlerContext ctx, RemotingCommand request)throwsRemotingCommandException {final RemotingCommand response = RemotingCommand.createResponseCommand(null);final ViewMessageRequestHeader requestHeader =(ViewMessageRequestHeader) request.decodeCommandCustomHeader(ViewMessageRequestHeader.class);

response.setOpaque(request.getOpaque());//getMessagetStore得到当前映射到内存中的CommitLog文件,然后根据偏移量取得数据

final SelectMappedBufferResult selectMappedBufferResult =

this.brokerController.getMessageStore().selectOneMessageByOffset(requestHeader.getOffset());if (selectMappedBufferResult != null) {

response.setCode(ResponseCode.SUCCESS);

response.setRemark(null);//将响应通过socket写回给客户端

try{//response对象的数据作为header//消息内容作为body

FileRegion fileRegion =

newOneMessageTransfer(response.encodeHeader(selectMappedBufferResult.getSize()),

selectMappedBufferResult);

ctx.channel().writeAndFlush(fileRegion).addListener(newChannelFutureListener() {

@Overridepublic void operationComplete(ChannelFuture future) throwsException {

selectMappedBufferResult.release();if (!future.isSuccess()) {

log.error("Transfer one message from page cache failed, ", future.cause());

}

}

});

}catch(Throwable e) {

log.error("", e);

selectMappedBufferResult.release();

}return null; //如果有值,则直接写回给请求方。这里返回null是不需要由外层处理响应。

} else{

response.setCode(ResponseCode.SYSTEM_ERROR);

response.setRemark("can not find message by the offset, " +requestHeader.getOffset());

}returnresponse;

}

接下来再来看看消息是如何从CommitLog文件中读取出来的。我们知道RocketMQ的CommitLog文件会通过内存映射的方式载入内存,故可以在内存中直接访问。看看代码是如何实现的。其中消息的存储条目中,前4个字节用来表示消息存储长度。故先读取一次得到长度信息,再完整取出。

//DefaultMessageStore.java

@Overridepublic SelectMappedBufferResult selectOneMessageByOffset(longcommitLogOffset) {//只要读前4个字节的信息,就可得到长度信息

SelectMappedBufferResult sbr = this.commitLog.getMessage(commitLogOffset, 4);if (null !=sbr) {try{//1 TOTALSIZE

int size =sbr.getByteBuffer().getInt();//得到长度信息后,在读取完整信息

return this.commitLog.getMessage(commitLogOffset, size);

}finally{

sbr.release();

}

}return null;

}

在getMessage实现中可以通过"消息偏移量"和"单个CommitLog文件的固定长度"确定两个信息。一个是消息所在的CommitLog文件,二是消息在该CommitLog文件中的相对偏移量。

//CommitLog.java

public SelectMappedBufferResult getMessage(final long offset, final intsize) {//获取commitLog文件的大小,默认是1G

int mappedFileSize = this.defaultMessageStore.getMessageStoreConfig().getMappedFileSizeCommitLog();//MappedFileQueue中存储映射的文件列表,这里可以通过消息的偏移量和CommitLog文件的大小,确定文件

MappedFile mappedFile = this.mappedFileQueue.findMappedFileByOffset(offset, offset == 0);if (mappedFile != null) {//得到相对偏移量(消息在该文件内部的偏移量)

int pos = (int) (offset %mappedFileSize);returnmappedFile.selectMappedBuffer(pos, size);

}return null;

}

确定完文件和相对偏移量之后,就可以直接读取数据了。这里值得注意的是,MappedByteBuffer的position始终为0。写出的索引信息单独存储在wrotePosition字段中,该字段的值会在重启的时候重新载入。(写入时也是基于切片处理的,不会影响position的值)

//from MappedFile

public SelectMappedBufferResult selectMappedBuffer(int pos, intsize) {//文件最大可读位置

int readPosition =getReadPosition();if ((pos + size) <=readPosition) {if (this.hold()) {//切片(数据范围:0~limit)

ByteBuffer byteBuffer = this.mappedByteBuffer.slice();//由于是切片(position,limit,capacity独立)故修改position,不会影响原position

byteBuffer.position(pos);//再次切片(数据范围:position~length)

ByteBuffer byteBufferNew =byteBuffer.slice();//设定limit(数据范围:position~position+size)

byteBufferNew.limit(size);return new SelectMappedBufferResult(this.fileFromOffset + pos, byteBufferNew, size, this);

}else{

log.warn("matched, but hold failed, request pos: " + pos + ", fileFromOffset: "

+ this.fileFromOffset);

}

}else{

log.warn("selectMappedBuffer request pos invalid, request pos: " + pos + ", size: " +size+ ", fileFromOffset: " + this.fileFromOffset);

}return null;

}

//todo 补充Rocket下一层的封包处理

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值