Rocketmq发送消息原理(含事务消息)

在消息队列中,生产者负责发送消息到broker,本文讲解Rocketmq发送消息的实现原理以及一些注意的事项。

一、生产者端的发送流程

大致步骤

1、根据topic从nameserver或本地获取路由信息,队列信息,broker信息等

2、根据重试次数,循环发送消息

3、消息内容组装成RemotingCommand对象,包括请求头和请求体

4、分oneway,sync,async的方式进行发送

5、如果是async,oneway会获取令牌再发送

6、组装请求头,调用netty组件发送消息

7、结果处理 

二、broker端接收发送消息请求与处理流程 

源码入口

class NettyServerHandler extends SimpleChannelInboundHandler<RemotingCommand> {        //netty监听客户端消息        @Override        protected void channelRead0(ChannelHandlerContext ctx, RemotingCommand msg) throws Exception {            processMessageReceived(ctx, msg);        }    }

发送消息的请求处理器

public class SendMessageProcessor extends AbstractSendMessageProcessor implements NettyRequestProcessor {    private List<ConsumeMessageHook> consumeMessageHookList;    public SendMessageProcessor(final BrokerController brokerController) {        super(brokerController);    }    @Override    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;        }    }}

获取消息存储路径

// 用户目录+store+commitlogprivate String storePathCommitLog = System.getProperty("user.home") + File.separator + "store"        + File.separator + "commitlog";            // CommitLog file size,default is 1Gprivate int mapedFileSizeCommitLog = 1024 * 1024 * 1024;    //路径String nextFilePath = this.storePath + File.separator + UtilAll.offset2FileName(createOffset);            String nextNextFilePath = this.storePath + File.separator                + //commitlog文件名    开始偏移量+文件大小 1g=1073741824         UtilAll.offset2FileName(createOffset + this.mappedFileSize);            MappedFile mappedFile = null;

文件存储

//存储文件中,新建文件,使用nio读写文件private void init(final String fileName, final int fileSize) throws IOException {        this.fileName = fileName;        this.fileSize = fileSize;        this.file = new File(fileName);        this.fileFromOffset = Long.parseLong(this.file.getName());        boolean ok = false;        ensureDirOK(this.file.getParent());        try {            this.fileChannel = new RandomAccessFile(this.file, "rw").getChannel();            this.mappedByteBuffer = this.fileChannel.map(MapMode.READ_WRITE, 0, fileSize);            TOTAL_MAPPED_VIRTUAL_MEMORY.addAndGet(fileSize);            TOTAL_MAPPED_FILES.incrementAndGet();            ok = true;        } catch (FileNotFoundException e) {            log.error("create file channel " + this.fileName + " Failed. ", e);            throw e;        } catch (IOException e) {            log.error("map file " + this.fileName + " Failed. ", e);            throw e;        } finally {            if (!ok && this.fileChannel != null) {                this.fileChannel.close();            }        }    }//最终调用bytebuffer将消息写入缓冲区,并没有调用刷缓冲到磁盘的方法            messagesByteBuff.position(0);            messagesByteBuff.limit(totalMsgLen);            byteBuffer.put(messagesByteBuff);            messageExtBatch.setEncodedBuff(null);            AppendMessageResult result = new AppendMessageResult(AppendMessageStatus.PUT_OK, wroteOffset, totalMsgLen, msgIdBuilder.toString(),                messageExtBatch.getStoreTimestamp(), beginQueueOffset, CommitLog.this.defaultMessageStore.now() - beginTimeMills);            result.setMsgNum(msgNum);            CommitLog.this.topicQueueTable.put(key, queueOffset);            return result;

判断刷盘方式,如果是同步刷盘,立即刷新缓冲数据到磁盘

public void handleDiskFlush(AppendMessageResult result, PutMessageResult putMessageResult, MessageExt messageExt) {        // 同步刷盘 Synchronization flush        if (FlushDiskType.SYNC_FLUSH == this.defaultMessageStore.getMessageStoreConfig().getFlushDiskType()) {            final GroupCommitService service = (GroupCommitService) this.flushCommitLogService;            if (messageExt.isWaitStoreMsgOK()) {                GroupCommitRequest request = new GroupCommitRequest(result.getWroteOffset() + result.getWroteBytes());                service.putRequest(request);                //立即刷盘,等待时间是5s                boolean flushOK = request.waitForFlush(this.defaultMessageStore.getMessageStoreConfig().getSyncFlushTimeout());                if (!flushOK) {                    log.error("do groupcommit, wait for flush failed, topic: " + messageExt.getTopic() + " tags: " + messageExt.getTags()                        + " client address: " + messageExt.getBornHostString());                    putMessageResult.setPutMessageStatus(PutMessageStatus.FLUSH_DISK_TIMEOUT);                }            } else {                service.wakeup();            }        }        //异步刷盘 Asynchronous flush        else {            //异步刷盘 默认等待200ms            if (!this.defaultMessageStore.getMessageStoreConfig().isTransientStorePoolEnable()) {                flushCommitLogService.wakeup();            } else {                commitLogService.wakeup();            }        }    }

最终调用

java.nio.MappedByteBuffer#force0 或sun.nio.ch.FileDispatcherImpl#force0 从缓冲写入磁盘

public int flush(final int flushLeastPages) {        if (this.isAbleToFlush(flushLeastPages)) {            if (this.hold()) {                int value = getReadPosition();                try {                    //We only append data to fileChannel or mappedByteBuffer, never both.                    if (writeBuffer != null || this.fileChannel.position() != 0) {                    //刷新到磁盘                                            this.fileChannel.force(false);                    } else {                     //刷新到磁盘                      this.mappedByteBuffer.force();                    }                } catch (Throwable e) {                    log.error("Error occurred when force data to disk.", e);                }                this.flushedPosition.set(value);                this.release();            } else {                log.warn("in flush, hold failed, flush offset = " + this.flushedPosition.get());                this.flushedPosition.set(getReadPosition());            }        }        return this.getFlushedPosition();    }

通过同步树刷盘异步刷盘可用在一定程度上保证消息不丢失,rocketmq还支持集群模式,主从同步模式支持同步或异步,实现数据在多个节点上备份。

//等5s,如果slave未返回,则超时private int syncFlushTimeout = 1000 * 5;public void handleHA(AppendMessageResult result, PutMessageResult putMessageResult, MessageExt messageExt) {            //同步复制到slave            if (BrokerRole.SYNC_MASTER == this.defaultMessageStore.getMessageStoreConfig().getBrokerRole()) {            HAService service = this.defaultMessageStore.getHaService();            if (messageExt.isWaitStoreMsgOK()) {                // Determine whether to wait                if (service.isSlaveOK(result.getWroteOffset() + result.getWroteBytes())) {                    GroupCommitRequest request = new GroupCommitRequest(result.getWroteOffset() + result.getWroteBytes());                    service.putRequest(request);                    service.getWaitNotifyObject().wakeupAll();                    boolean flushOK =                        request.waitForFlush(this.defaultMessageStore.getMessageStoreConfig().getSyncFlushTimeout());                    if (!flushOK) {                        log.error("do sync transfer other node, wait return, but failed, topic: " + messageExt.getTopic() + " tags: "                            + messageExt.getTags() + " client address: " + messageExt.getBornHostNameString());                        putMessageResult.setPutMessageStatus(PutMessageStatus.FLUSH_SLAVE_TIMEOUT);                    }                }                // Slave problem                else {                    // Tell the producer, slave not available                    putMessageResult.setPutMessageStatus(PutMessageStatus.SLAVE_NOT_AVAILABLE);                }            }        }    }

每个broker有三种broker角色,

broker端接收发送消息请求后的总体处理流程图如下 

总的来说包括如下步骤:

1、netty接收到请求,转发到SendMessageProcessor处理器

2、消息头解码

3、判断是是重试消息,并且判断是否达到最大重试次数,如果到了则转换topic和队列,加入死信队列

4、是否是事务消息,转换内置的事务消息topic和queueId

5、不是事务消息,判断是否是延迟消息,是延迟消息,转换成延迟消息topic(SCHEDULE_TOPIC_XXXX)和队列(延迟等级-1,延迟等级从1开始到18)

6、创建或获取消息文件,bytebuffer

7、通过bytebuffer写入缓冲

8、如果是SYNC_FLUSH刷盘方式,立即刷盘 ,刷盘类型有同步和异步两种

public enum FlushDiskType {    SYNC_FLUSH,    ASYNC_FLUSH}

如果是事务消息,流程是怎样的?

在Rocketmq中,事务消息是用来保证本地事务和发送消息逻辑同时成功的一种机制。

事务消息标记存在消息的properties,第一步是将properties解码 

public static final String PROPERTY_TRANSACTION_PREPARED = "TRAN_MSG";

然后将消息发送到一个内置的topic里

private MessageExtBrokerInner parseHalfMessageInner(MessageExtBrokerInner msgInner) {//存储真实topic和队列id到properties        MessageAccessor.putProperty(msgInner, MessageConst.PROPERTY_REAL_TOPIC, msgInner.getTopic());        MessageAccessor.putProperty(msgInner, MessageConst.PROPERTY_REAL_QUEUE_ID,            String.valueOf(msgInner.getQueueId()));        msgInner.setSysFlag(            MessageSysFlag.resetTransactionValue(msgInner.getSysFlag(), MessageSysFlag.TRANSACTION_NOT_TYPE));            //topic 转换成内置事务消息topic 默认RMQ_SYS_TRANS_HALF_TOPIC        msgInner.setTopic(TransactionalMessageUtil.buildHalfTopic());        //默认都是第1个队列        msgInner.setQueueId(0);        msgInner.setPropertiesString(MessageDecoder.messageProperties2String(msgInner.getProperties()));        return msgInner;    }

在客户端发送事务消息 

首先需要实现一个事务消息监听器,实现TransactionListener接口,分别实现本地事务逻辑,检查本地事务状态的逻辑

public class TransactionListenerImpl implements TransactionListener {    private AtomicInteger transactionIndex = new AtomicInteger(0);    private ConcurrentHashMap<String, Integer> localTrans = new ConcurrentHashMap<>();    //本地事务逻辑    @Override    public LocalTransactionState executeLocalTransaction(Message msg, Object arg) {        int value = transactionIndex.getAndIncrement();        int status = value % 3;        localTrans.put(msg.getTransactionId(), status);        return LocalTransactionState.UNKNOW;    }    //查询本地事务    @Override    public LocalTransactionState checkLocalTransaction(MessageExt msg) {        System.out.println("check msg" + msg.getMsgId() +"===" + LocalDateTime.now().format( DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")));        Integer status = localTrans.get(msg.getTransactionId());        if (null != status) {            switch (status) {                case 0:                    return LocalTransactionState.UNKNOW;                case 1:                    return LocalTransactionState.COMMIT_MESSAGE;                case 2:                    return LocalTransactionState.ROLLBACK_MESSAGE;                default:                    return LocalTransactionState.COMMIT_MESSAGE;            }        }        return LocalTransactionState.COMMIT_MESSAGE;    }}

然后通过事务消息发送器发送消息

public class TransactionProducer {    public static void main(String[] args) throws MQClientException, InterruptedException {        TransactionMQProducer producer = new TransactionMQProducer("please_rename_unique_group_name");        ExecutorService executorService = new ThreadPoolExecutor(2, 5, 100, TimeUnit.SECONDS, new ArrayBlockingQueue<Runnable>(2000), new ThreadFactory());        producer.setExecutorService(executorService);        //指定事务消息发送监听器        TransactionListener transactionListener = new TransactionListenerImpl();        producer.setTransactionListener(transactionListener);        producer.start();        Message msg =  new Message("test", "TAG", "KEY",                            ("Hello RocketMQ ").getBytes(RemotingHelper.DEFAULT_CHARSET));            SendResult sendResult = producer.sendMessageInTransaction(msg, null);        producer.shutdown();    }}

在发送事务消息流程图如下

事务消息流程总结

1、客户端使用同步发送半消息,broker将半消息存储到内置的事务消息topic和队列,这个时候事务消息不能被消费者消费到。

2、客户端接收发送消息返回,执行本地事务,执行成功,发送本地事务执行结果到broker

3、broker如果发现成功,将半消息转移到真实topic和队列,删除半消息,这个时候事务消息可用被消费者消费,如果回滚,直接删除半消息

4、broker启用一个线程,扫描事务消息topic里的队列里面的消息,判断是否需要检查事务状态(最大检查15次)

5、通过oneway方式向客户端发起查询事务状态请求,客户端查询状态,客户端通过oneway发送事务状态到broker,broker执行第3步骤。 

本文介绍了消息发送和broker处理消息发送请求的实现,得出结论

1、生产者发送消息是会发送到指定的topic队列,默认采用轮训算法实现发送的负载均衡

2、发送消息类型有3种,分别是同步,异步,单次(oneway),其中同步会重试3次。

3、broker存储消息采用mmap机制,刷盘机制支持同步刷盘和异步刷盘

4、broker主从复制模式支持同步复制

5、事务消息采用内置topic+消息回查机制实现本地事务和发送逻辑的事务一致性。

  • 1
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

服务端技术栈

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值