RocketMQ源码学习(五) —— 其它

70 阅读33分钟

5.消息过滤FilterServer

本章主要分析基于类模式的消息过滤机制,主要内容如下:

  • ClassFilter运行机制
  • FilterClass订阅信息注册
  • FilterServer注册剖析
  • 消息拉取(拉模式)

5.1ClassFilter运行机制

基于类模式过滤是指在Broker端运行一个或多个消息过滤服务器(FilterServer),RocketMQ允许消息消费者自定义消息过滤实现类并将其代码上传到FilterServer上,消息消费者向FilterServer拉取消息,FilterServer将消息消费者的拉取命令转发到Broker,然后对返回的消息执行消息过滤逻辑,最终将消息返回给消费端,其工作原理如图:

image.png

  1. Broker进程所在的服务器会启动多个FilterServer进程
  2. 消费者在订阅消息主题时会上传一个自定义的消息过滤实现类,FilterServer加载并实例化
  3. 消息消费者(Consume)向FilterServer发送消息拉取请求,FilterServer接受到消息消费者拉取请求后,FilterServer将消息拉取请求转发给Broker,Broker返回消息后在FilterServer端执行消息过滤逻辑,然后返回符合订阅信息的消息给消息消费者进行消费

通常消息消费者是直接向Broker订阅主题然后从Broker上拉取消息,类模式的一个特别之处在于消息消费者是从FilterServer拉取消息

5.2FilterServer注册剖析

FilterServer会在启动时创建一个定时调度任务,每隔10s向Broker注册自己

  • Step1:FilterServer从配置文件中获取Broker地址,然后将FilterServer所在机器的IP与监听端口发送到Broker服务器,请求命令类型为RequestCode.REGISTER_FILTER_SERVER
  • Step2:在Broker端处理REGISTER_FILTER_SERVER命令的核心实现为FilterServerManager。其实现过程是先从filterServerTable中以网络通道为key获取FilterServerInfo,如果不等于空,则更新上次更新时间为当前时间,否则创建一个新的FilterServerInfo对象并加入到filterServerTable路由表中
public void registerFilterServer(final Channel channel, final String filterServerAddr) {
    /**
    * static class FilterServerInfo {
    *	 private String filterServerAddr;	// filterServer服务器地址
   	*	 private long lastUpdateTimestamp;	// filterServer上次发送心跳包的时间
   	* }
    */
    FilterServerInfo filterServerInfo = this.filterServerTable.get(channel);
    if (filterServerInfo != null) {
        filterServerInfo.setLastUpdateTimestamp(System.currentTimeMillis());
    } else {
        filterServerInfo = new FilterServerInfo();
        filterServerInfo.setFilterServerAddr(filterServerAddr);
        filterServerInfo.setLastUpdateTimestamp(System.currentTimeMillis());
        this.filterServerTable.put(channel, filterServerInfo);
        log.info("Receive a New Filter Server<{}>", filterServerAddr);
    }
}

FilterServer与Broker端通过心跳维持FilterServer在Broker端的注册,同样在Broker每隔10s扫描一下该注册表,如果30s内未收到FilterServer的注册信息,将关闭Broker与FilterServer的连接。

Broker为了避免Broker端FilterServer的异常退出导致FilterServer进程越来越少,同样提供一个定时任务每30s检测一下当前存活的FilterServer进程的个数,如果当前存活的FilterServer进程个数小于配置的数量,则自动创建一个FilterServer进程,其实现过程如下:

public void createFilterServer() {
    int more =
        this.brokerController.getBrokerConfig().getFilterServerNums() - this.filterServerTable.size();
    String cmd = this.buildStartCommand();
    for (int i = 0; i < more; i++) {
        // 利用Runtime.getRuntime().exec(cmdArray)直接指向shell脚本
        FilterServerUtil.callShell(cmd, log);
    }
}

Broker中保存了FilterServer的信息,然后Broker每隔30s会向所有NameServer发送心跳包,完成FilterServer信息从Broker端向NameServer端的传输

5.3类过滤模式订阅机制

// DefaultMQPushConsumerImpl#subscribe

/**
 * 
 * @param topic 消费组订阅的消息主题
 * @param fullClassName 类过滤全路径名
 * @param filterClassSource 类过滤源代码字符串
 * @throws MQClientException
 */
public void subscribe(String topic, String fullClassName, String filterClassSource) throws MQClientException {
    try {
        // 构建订阅信息
        SubscriptionData subscriptionData = FilterAPI.buildSubscriptionData(topic, "*");
        subscriptionData.setSubString(fullClassName);
        subscriptionData.setClassFilterMode(true);
        subscriptionData.setFilterClassSource(filterClassSource);
        // 将该订阅信息添加到RebalanceImpl中,主要目标是RebalanceImpl会对订阅信息表中的主题进行消息队列的负载,创建消息拉取任务,以便PullMessageService线程拉取消息
        this.rebalanceImpl.getSubscriptionInner().put(topic, subscriptionData);
        if (this.mQClientFactory != null) {
            this.mQClientFactory.sendHeartbeatToAllBrokerWithLock();
        }

    } catch (Exception e) {
        throw new MQClientException("subscription exception", e);
    }
}

根据订阅的主题获取该主题的路由信息,如果该主题路由信息的FilterServer缓存表不为空,则需要将过滤类发送到FilterServer上

// TopicRouteData
private HashMap<String/* brokerAddr */, List<String>/* Filter Server */> filterServerTable;

遍历主题路由表中的filterServerTable,向缓存中所有的FilterServer上传消息过滤代码,FilterServer端处理FilterClass上传并将其源码编译

根据消息消费组与主题名称构建filterClassTable缓存key,从缓存表中尝试获取过滤类型信息FilterClassInfo。如果缓存表不包含FIlterClassInfo则表示第一次注册,设置registerNew为true;如果FIlterClassInfo不为空,说明该消息消费组不是第一次注册。

如果消息消费端上传过滤类,则需要将该类型强制类型转换为MessageFilter,即自定义的消息过滤类必须实现MessageFilter接口

如果忽略消息消费者上传的过滤类源代码,FilterServer会开启一个定时任务从配置好的远程服务器去获取过滤类的源码,再将其编译与实例化

  • Step1:如果FIlterServer不允许消息消费者上传类,会开启一个定时任务,每隔1分钟从远程服务器下载源代码并编译
  • Step2:从远程服务器根据消息主题、消息消费组名称、过滤类名称获取,其URL地址通过filterClassRepertoryUrl配置属性指定
  • Step3:获取到的过滤类源码将其编译并创建MessageFilter实例且更新FilterClassInfo

5.4消息拉取

在FilterServer过滤模式下,PullMessageService线程是如何将拉取地址由原来的Broker地址转换成FilterServer地址呢?

  • Step1:在消息拉取时,如果发现PullSysFlag标志是classFilter,将拉取消息服务器地址由原来的Broker地址转换成该Broker服务器所对应的FilterServer
// PullAPIWrapper#pullKernelImpl
if (PullSysFlag.hasClassFilterFlag(sysFlagInner)) {
    brokerAddr = computePullFromWhichFilterServer(mq.getTopic(), brokerAddr);
}
  • Step2:选择FilterServer,发送拉取请求到该FilterServer上
private String computePullFromWhichFilterServer(final String topic, final String brokerAddr)
    throws MQClientException {
    // 获取该消息主题的路由信息
    ConcurrentMap<String, TopicRouteData> topicRouteTable = this.mQClientFactory.getTopicRouteTable();
    if (topicRouteTable != null) {
        TopicRouteData topicRouteData = topicRouteTable.get(topic);
        // 获取Broker对应的FilterServer列表
        List<String> list = topicRouteData.getFilterServerTable().get(brokerAddr);

        if (list != null && !list.isEmpty()) {	// 如果过滤列表不为空,则随机从列表中选择一个FilterServer
            return list.get(randomNum() % list.size());
        }
    }

    throw new MQClientException("Find Filter Server Failed, Broker Addr: " + brokerAddr + " topic: "
        + topic, null);
}

5.5总结

类过滤模式:允许消息消费者在订阅主题消息时上传消息过滤类到过滤服务器,在过滤服务器将消息过滤后再返回给消息消费者

相比TAG模式进行消息过滤有如下优势:

  • 基于TAG模式消息过滤,由于在消息服务端进行消息过滤是匹配消息TAG的hahcode,导致服务端过滤并不十分准确,从服务端返回的消息最终并不一定是消息消费者订阅的消息,造成网络带宽的浪费。而类模式的消息过滤所有的过滤操作全部在FilterServer端进行
  • 由于FIlterServer与Broker运行在同一台机器上,消息的传输是通过本地回环通信,不会浪费Broker端的网络资源

6.RocketMQ主从同步(HA)机制

为了提高消息消费的高可用性,避免Broker发生单点故障引起存储在Broker上的消息无法及时消费,RocketMQ引入了Broker主备机制,即消息消费到达主服务器后需要将消息同步到消息从服务器

1.RocketMQ主从复制原理

RocketMQ HA由7个核心类实现:

  • HAService:RocketMQ主从同步核心实现类
  • HAService$AcceptSocketService:HA Master端监听客户端连接实现类
  • HAService$GroupTransferService:主从同步通知实现类
  • HAService$HAClient:HA Client端实现类
  • HAConnection:HA Master服务端HA连接对象的封装,与Broker从服务器的网络读写实现类
  • HAConnection$ReadSocketService:HA Master网络读实现类
  • HAConnection$WriteSocketService:HA Master网络写实现类
// HAService#start
public void start() throws Exception {
    this.acceptSocketService.beginAccept();
    this.acceptSocketService.start();
    this.groupTransferService.start();
    this.haClient.start();
}

RocketMQ HA实现原理如下:

1、主服务器启动,并在特定端口上监听从服务器的连接

2、从服务器主动连接主服务器,主服务器接收客户端的连接,并建立相关TCP连接

3、从服务器主动向主服务器发送待拉取消息偏移量,主服务器解析请求并返回消息给从服务器

4、从服务器保存消息并继续发送新的消息同步请求

AcceptSocketService实现原理

AcceptSocketService作为HAService的内部类,实现Master端监听Slave连接

class AcceptSocketService extends ServiceThread {
    private final SocketAddress socketAddressListen;	// Broker服务监听套接字(本地IP + 端口号)
    private ServerSocketChannel serverSocketChannel;	// 服务端Socket通道,基于NIO
    private Selector selector;	// 事件选择器,基于NIO
// HAService$AcceptSocketService#beginAccept
public void beginAccept() throws Exception {
    this.serverSocketChannel = ServerSocketChannel.open();	// 创建ServerSocketChannel
    this.selector = RemotingUtil.openSelector();			// 创建Selector
    this.serverSocketChannel.socket().setReuseAddress(true);// 设置TCP reuseAddress
    this.serverSocketChannel.socket().bind(this.socketAddressListen);	// 绑定监听端口
    this.serverSocketChannel.configureBlocking(false);	// 设置为非阻塞模式
    this.serverSocketChannel.register(this.selector, SelectionKey.OP_ACCEPT);	// 注册OP_ACCEPT(连接事件)
}


@Override
public void run() {
    while (!this.isStopped()) {
        try {
            this.selector.select(1000);	// 选择器每1s处理一次连接就绪事件
            Set<SelectionKey> selected = this.selector.selectedKeys();
            if (selected != null) {
                for (SelectionKey k : selected) {
                    if ((k.readyOps() & SelectionKey.OP_ACCEPT) != 0) {
                        // 连接事件就绪后,调用accept()方法创建SocketChannel
                        SocketChannel sc = ((ServerSocketChannel) k.channel()).accept();
                        if (sc != null) {
                                // 为每一个连接创建一个HAConnection对象,负责M-S数据同步逻辑
                                HAConnection conn = new HAConnection(HAService.this, sc);
                                conn.start();
                                HAService.this.addConnection(conn);
                        }
                selected.clear();
            }
        }
    }
}

GroupTransferService实现原理

GroupTransferService主从同步阻塞实现,如果是同步主从模式,消息发送者将消息刷写到磁盘后,需要继续等待新数据被传输到从服务器,从服务器数据的复制是在另外一个线程HAConnection中去拉取,所以消息发送者在这里需要等待数据传输的结果,GroupTransferService就是实现该功能,其职责就是负责当主从同步复制结束后通知由于等待HA同步结果而阻塞的消息发送者线程

// HAService$GroupTransferService#doWaitTransfer
private void doWaitTransfer() {
    if (!this.requestsRead.isEmpty()) {
        for (CommitLog.GroupCommitRequest req : this.requestsRead) {
            // 判断主从同步是否完成的依据是Slave中已成功复制的最大偏移量是否 >= 消息生产者发送消息后消息服务端返回下一条消息的起始偏移量
            boolean transferOK = HAService.this.push2SlaveMaxOffset.get() >= req.getNextOffset();
            long deadLine = req.getDeadLine();
            while (!transferOK && deadLine - System.nanoTime() > 0) {
                // 如果主从同步复制未完成,等待1s后再次判断
                this.notifyTransferObject.waitForRunning(1000);
                transferOK = HAService.this.push2SlaveMaxOffset.get() >= req.getNextOffset();
            }
			// 如果主从同步复制已经完成,唤醒消息发送线程
            req.wakeupCustomer(transferOK ? PutMessageStatus.PUT_OK : PutMessageStatus.FLUSH_SLAVE_TIMEOUT);
        }

        this.requestsRead = new LinkedList<>();
    }
}

GroupTransferService通知主从复制的实现如下:

// HAService$GroupTransferService##notifyTransferSome
// 该方法在Master收到从服务器的拉取消息请求后被调用,表示从服务器当前已同步的偏移量,既然收到从服务器的反馈信息,需要唤醒某些消息发送者线程。如果从服务器收到的确认偏移量大于push2SlaveMaxOffset,则更新,然后唤醒GroupTransferService线程
public void notifyTransferSome(final long offset) {
    for (long value = this.push2SlaveMaxOffset.get(); offset > value; ) {
        boolean ok = this.push2SlaveMaxOffset.compareAndSet(value, offset);
        if (ok) {
            this.groupTransferService.notifyTransferSome();
            break;
        } else {
            value = this.push2SlaveMaxOffset.get();
        }
    }
}

HAClient实现原理

HAClient是主从同步Slave端的核心实现类,

HAClient类是RocketMQ HA模块中负责监控主从节点状态、执行主从切换,并保证数据同步的关键组件。它确保了RocketMQ在高可用性方面的优势,提供了稳定可靠的消息传输服务。

class HAClient extends ServiceThread {
    // Socket读缓冲区大小
    private static final int READ_MAX_BUFFER_SIZE = 1024 * 1024 * 4;
    // master地址
    private final AtomicReference<String> masterAddress = new AtomicReference<>();
    // Slave向Master发起主从同步的拉取偏移量
    private final ByteBuffer reportOffset = ByteBuffer.allocate(8);
    // NIO事件选择器
    private final Selector selector;
    // 网络传输通道
    private SocketChannel socketChannel;
    // 上次写入时间戳
    private long lastWriteTimestamp = System.currentTimeMillis();
	// 反馈Slave当前的复制进度,commitlog文件最大偏移量
    private long currentReportedOffset = 0;
    // 本次已处理读缓冲区的指针
    private int dispatchPosition = 0;
    // 读缓冲区,大小为4M
    private ByteBuffer byteBufferRead = ByteBuffer.allocate(READ_MAX_BUFFER_SIZE);
    // 读缓冲区备份,与BufferRead进行交换
    private ByteBuffer byteBufferBackup = ByteBuffer.allocate(READ_MAX_BUFFER_SIZE);

HAClient的工作原理:

  • Step1:Slave服务器连接Master服务器。如果socketChannel为空,则尝试连接Master。如果master为空,返回false;如果master地址不为空,则建立到Master的TCP连接,然后注册OP_READ(网络读事件),初始化currentReportedOffset为commitlog文件的最大偏移量、lastWriteTimestamp为当前时间戳,并返回true。
  • Step2:判断是否需要向Master反馈当前待拉取偏移量,Master与Slave的HA心跳发送间隔默认为5s
  • Step3:向Master服务器反馈拉取偏移量。对Slave端来说,是发送下次待拉取消息偏移量;对Master端来说,是Slave本次请求拉取的消息偏移量,也是为Slave的消息同步ACK确认消息
  • Step4:进行事件选择,其执行间隔为1s
  • Step5:处理网络读请求,即处理从Master服务器返回的消息数据。将读取到的所有信息全部追加到内存映射文件中,然后再次反馈拉取进度给服务器。当连续3次从网络通道读取到0个字节时,则结束本次读,返回true

HAConnection实现原理

Master服务器在收到从服务器的连接请求后,会将主从服务器的连接SocketChannel封装成HAConnection对象,实现主服务器与从服务器的读写操作

public class HAConnection {
    private static final InternalLogger log = InternalLoggerFactory.getLogger(LoggerName.STORE_LOGGER_NAME);
    private final HAService haService;	// HAService对象
    private final SocketChannel socketChannel;	// 网络socket通道
    private final String clientAddr;	// 客户端连接地址
    private final WriteSocketService writeSocketService;	// 服务端向从服务器写数据服务类
    private final ReadSocketService readSocketService;		// 服务端向从服务器读数据服务类

    private volatile long slaveRequestOffset = -1;	// 从服务器请求拉取数据的偏移量
    private volatile long slaveAckOffset = -1;		// 从服务器反馈已拉取完成的数据偏移量

HAConnection的网络读请求是由其内部类ReadSocketService线程来实现:

class ReadSocketService extends ServiceThread {
    private static final int READ_MAX_BUFFER_SIZE = 1024 * 1024;	// 网络读缓存区大小,默认为1M
    private final Selector selector;	// NIO网络事件选择器
    private final SocketChannel socketChannel;	// 网络通道,用于读写的socket通道
    // 网络读写缓存区,默认为1M
    private final ByteBuffer byteBufferRead = ByteBuffer.allocate(READ_MAX_BUFFER_SIZE);
    private int processPosition = 0;	// byteBuffer当前处理指针
    private volatile long lastReadTimestamp = System.currentTimeMillis();	// 上次读取数据的时间戳

通过观察其run方法,每隔1s处理一次读就绪事件,每次读请求调用其processReadEvent来解析从服务器的拉取请求

private boolean processReadEvent() {
    int readSizeZeroTimes = 0;

    if (!this.byteBufferRead.hasRemaining()) {	// 如果byteBufferRead没有剩余空间
        this.byteBufferRead.flip();
        this.processPosition = 0;
    }

    while (this.byteBufferRead.hasRemaining()) {
        try {
            // 处理网络读
            int readSize = this.socketChannel.read(this.byteBufferRead);
            if (readSize > 0) {
                readSizeZeroTimes = 0;
                this.lastReadTimestamp = HAConnection.this.haService.getDefaultMessageStore().                                                                                           getSystemClock().now();
                // 如果读取的字节大于0并且本次读取到的内容大于等于8,说明收到了从服务器一条拉取消息的请求
                if ((this.byteBufferRead.position() - this.processPosition) >= 8) {                  
                    // 计算出一个对齐位置(pos),用于确保读取位置是8的倍数
                    int pos = this.byteBufferRead.position() - (this.byteBufferRead.position() % 8);
                    // 获取主从同步的读取位置(readOffset)
                    // 通过this.byteBufferRead.getLong(pos - 8)获取在对齐位置之前8个字节的数据,将其作为读取位置。
                    long readOffset = this.byteBufferRead.getLong(pos - 8);
                    // 将处理位置(processPosition)设置为对齐位置(pos),代表已经处理到了该位置。
                    this.processPosition = pos;

                    HAConnection.this.slaveAckOffset = readOffset;
                    if (HAConnection.this.slaveRequestOffset < 0) {
                        HAConnection.this.slaveRequestOffset = readOffset;
                        log.info("slave[" + HAConnection.this.clientAddr + "] request offset " + readOffset);
                    } else if (HAConnection.this.slaveAckOffset > HAConnection.this.haService.getDefaultMessageStore().getMaxPhyOffset()) {
                        log.warn("slave[{}] request offset={} greater than local commitLog offset={}. ",
                                HAConnection.this.clientAddr,
                                HAConnection.this.slaveAckOffset,
                                HAConnection.this.haService.getDefaultMessageStore().getMaxPhyOffset());
                        return false;
                    }

                    HAConnection.this.haService.notifyTransferSome(HAConnection.this.slaveAckOffset);
                }
            } else if (readSize == 0) {	// 如果读取到的字节数等于0的次数 >= 3,则结束本次读请求处理
                if (++readSizeZeroTimes >= 3) {
                    break;
                }
            } else {
                log.error("read socket[" + HAConnection.this.clientAddr + "] < 0");
                return false;	// 关闭该连接
            }
        } catch (IOException e) {
            log.error("processReadEvent exception", e);
            return false;
        }
    }

    return true;
}

HAConnection的网络写请求是由其内部类WriteSocketService线程来实现:

class WriteSocketService extends ServiceThread {
    private final Selector selector;	// NIO网络事件选择器
    private final SocketChannel socketChannel;	// 网络socket通道

    private final int headerSize = 8 + 4;	// 消息头长度,消息物理偏移量+消息长度
    private final ByteBuffer byteBufferHeader = ByteBuffer.allocate(headerSize);
    private long nextTransferFromWhere = -1;	// 下一次传输的物理偏移量
    private SelectMappedBufferResult selectMappedBufferResult;	// 根据偏移量查找消息的结果
    private boolean lastWriteOver = true;	// 上一次数据是否传输完毕
    private long lastWriteTimestamp = System.currentTimeMillis();	// 上次写入的时间戳

分析其实现原理,重点关注run方法

@Override
public void run() {
    HAConnection.log.info(this.getServiceName() + " service started");

    while (!this.isStopped()) {
        try {
            this.selector.select(1000);
			// 说明Master还未收到从服务器的拉取请求,放弃本次事件处理。slaveRequestOffset在收到从服务器拉取请求时更新
            if (-1 == HAConnection.this.slaveRequestOffset) {
                Thread.sleep(10);
                continue;
            }

            if (-1 == this.nextTransferFromWhere) {	// 表示初次进行数据传输,计算待传输的物理偏移量
                if (0 == HAConnection.this.slaveRequestOffset) {	// 从当前commitlog文件最大偏移量开始传输
                    long masterOffset = HAConnection.this.haService.getDefaultMessageStore().getCommitLog().getMaxOffset();
                    masterOffset =
                        masterOffset
                            - (masterOffset % HAConnection.this.haService.getDefaultMessageStore().getMessageStoreConfig()
                            .getMappedFileSizeCommitLog());

                    if (masterOffset < 0) {
                        masterOffset = 0;
                    }

                    this.nextTransferFromWhere = masterOffset;
                } else { // 否则根据从服务器的拉取请求偏移量开始传输
                    this.nextTransferFromWhere = HAConnection.this.slaveRequestOffset;
                }
            }

            if (this.lastWriteOver) {	// 判断上次写事件是否已将消息全部写入客户端

                // 计算当前系统时间与上次最后写入的时间间隔
                long interval =
                    HAConnection.this.haService.getDefaultMessageStore().getSystemClock().now() - this.lastWriteTimestamp;
				// 如果该间隔大于HA心跳检测时间(默认5s),则发送一个心跳包
                if (interval > HAConnection.this.haService.getDefaultMessageStore().getMessageStoreConfig()
                    .getHaSendHeartbeatInterval()) {

                    // 构建心跳包,长度12字节(从服务器待拉取偏移量 + size)
                    this.byteBufferHeader.position(0);
                    this.byteBufferHeader.limit(headerSize);
                    // 下一次传输的物理偏移量
                    this.byteBufferHeader.putLong(this.nextTransferFromWhere);
                    // 消息长度默认为0,避免长连接由于空闲而被关闭
                    this.byteBufferHeader.putInt(0);
                    this.byteBufferHeader.flip();

                    this.lastWriteOver = this.transferData();
                    if (!this.lastWriteOver)
                        continue;
                }
            } else {	// 如果上次写事件数据未写完,则先传输上次的数据
                this.lastWriteOver = this.transferData();
                if (!this.lastWriteOver)	// 如果消息还是未全部传输,则结束本次事件处理
                    continue;
            }

            SelectMappedBufferResult selectResult = HAConnection.this.haService.getDefaultMessageStore().                                                             getCommitLogData(this.nextTransferFromWhere);
            if (selectResult != null) {
                int size = selectResult.getSize();
                // 如果消息大小 > 32KB,一次消息传输最大为32KB
                // 意味着HA客户端收到的消息会包含不完整的消息
                if (size > HAConnection.this.haService.getDefaultMessageStore(). 	                                                                   getMessageStoreConfig().getHaTransferBatchSize()) {
                    size = HAConnection.this.haService.getDefaultMessageStore().                                                                       getMessageStoreConfig().getHaTransferBatchSize();
                }

                long thisOffset = this.nextTransferFromWhere;
                this.nextTransferFromWhere += size;	// 更新下一传输物理偏移量

                selectResult.getByteBuffer().limit(size);
                this.selectMappedBufferResult = selectResult;

                // Build Header
                this.byteBufferHeader.position(0);
                this.byteBufferHeader.limit(headerSize);
                this.byteBufferHeader.putLong(thisOffset);
                this.byteBufferHeader.putInt(size);
                this.byteBufferHeader.flip();

                this.lastWriteOver = this.transferData();
            } else {
                HAConnection.this.haService.getWaitNotifyObject().allWaitForRunning(100);
            }
        } catch (Exception e) {
            HAConnection.log.error(this.getServiceName() + " service has exception.", e);
            break;
        }
    }
}

RocketMQ HA交互类图:

image.png

2.RocketMQ读写分离机制

RocketMQ根据MessageQueue查找Broker地址的唯一根据是brokerName,从RocketMQ的Broker组织结构中得知同一组Broker(M-S)服务器,他们的brokerName相同,但brokerId不同,主服务器的brokerId为0,从服务器的brokerId大于0,RocketMQ提供MQClientFactory.findBrokerAddressInSubscribe来实现根据brokerName、brokerId查找Broker地址

/**
 * 
 * @param brokerName Broker名称
 * @param brokerId BrokerId
 * @param onlyThisBroker 是否必须返回brokerId的Broker对应的服务器信息
 * @return
 */
public FindBrokerResult findBrokerAddressInSubscribe(
    final String brokerName,
    final long brokerId,
    final boolean onlyThisBroker
) {
    String brokerAddr = null;
    boolean slave = false;
    boolean found = false;
	// 从地址缓存表中根据brokerName获取所有的Broker信息
    HashMap<Long/* brokerId */, String/* address */> map = this.brokerAddrTable.get(brokerName);
    if (map != null && !map.isEmpty()) {
        brokerAddr = map.get(brokerId);	// 根据brokerId从主从缓存表中获取指定Broker地址
        slave = brokerId != MixAll.MASTER_ID;
        found = brokerAddr != null;

        if (!found && slave) {
            brokerAddr = map.get(brokerId + 1);
            found = brokerAddr != null;
        }

        if (!found && !onlyThisBroker) {
            // 如果未找到,并且onlyThisBroker为false,则随机返回Broker中任意一个Broker,否则返回Null
            Entry<Long, String> entry = map.entrySet().iterator().next();
            brokerAddr = entry.getValue();
            slave = entry.getKey() != MixAll.MASTER_ID;
            found = true;
        }
    }

    if (found) {
        return new FindBrokerResult(brokerAddr, slave, findBrokerVersion(brokerName, brokerAddr));
    }

    return null;
}

根据消息消费队列获取brokerId的实现如下:

// PullAPIWrapper#recalculatePullFromWhichNode

/**
* 消息消费者线程在收到消息后,会根据主服务器的建议拉取brokerId来更新pullFromWhichNodeTable
* private ConcurrentMap<MessageQueue, AtomicLong/* brokerId */> pullFromWhichNodeTable =
*/        										new ConcurrentHashMap<MessageQueue, AtomicLong>(32);
public long recalculatePullFromWhichNode(final MessageQueue mq) {
    if (this.isConnectBrokerByUser()) {
        return this.defaultBrokerId;
    }
	// 从缓存表中获取该消息消费队列的brokerId
    AtomicLong suggest = this.pullFromWhichNodeTable.get(mq);
    if (suggest != null) {	// 如果不为空,直接返回
        return suggest.get();
    }
	// 如果为空,否则返回brokerName的主节点
    return MixAll.MASTER_ID;
}

消息服务端是根据哪种规则来建议哪个消息消费队列从哪台Broker服务器上拉取消息呢?

在DefaultMessageStore#getMessage中,有一个memory变量,表示RocketMQ消息常驻内存的大小,超过该大小,会将旧的消息置换回磁盘,如果需要拉取的消息已经超过了常驻内存的大小,表示主服务器繁忙,此时建议从从服务器拉取,默认从brokerId=1中拉取。如果Master拥有多台Slave服务器,参与消息拉取负载的从服务器只会是其中一个。

3.总结

RocketMQ的HA机制,其核心实现是从服务器在启动时主动向主服务器建立TCP长连接,然后获取服务器的commitlog最大偏移量,以此偏移量向主服务器主动拉取消息,主服务器根据偏移量,与自身commitlog文件最大的文件偏移量进行比较,如果大于从服务器的commitlog偏移量,主服务器将向从服务器返回一定数量的消息,该过程循环进行,达到主从服务器数据同步

RocketMQ读写分离与其它中间件的实现方式完全不同,RocketMQ是消费者首先向主服务器发起拉取消息请求,然后主服务器返回一批消息,然后根据主服务器负载压力与主从同步情况,向从服务器建议下次消息拉取是从主服务器还是从从服务器拉取

7.RocketMQ事务消息

1.事务消息实现思想

RocketMQ事务消息的实现原理基于两阶段提交和定时事务状态回查来决定消息最终是提交还是回滚

image.png

1)应用程序在事务内完成相关业务数据落库后,需要同步调用RocketMQ消息发送接口,发送状态为prepare的消息。消息发送成功后,RocketMQ服务器会回调RocketMQ消息发送者的事件监听程序,记录消息的本地事务状态,该相关标记与本地业务操作同属一个事务,确保消息发送与本地事务的原子性。

2)RocketMQ在收到类型为prepare的消息时,会首先备份消息的原主题与原消息消费队列,然后将消息存储在主题为RMQ_SYS_TRANS_HALF_TOPIC的消息消费队列中。

3)RocketMQ消息服务端开启一个定时任务,消费RMQ_SYS_TRANS_HALF_TOPIC的消息,向消息发送端(应用程序)发起消息事务状态回查,应用程序根据保存的事务状态回馈消息服务器事务的状态(提交、回滚、未知),如果是提交或回滚,则消息服务器提交或回滚消息,如果是未知,待下一次回查,RocketMQ允许设置一次消息的回查间隔与回查次数,如果在超过回查次数后依然无法获知消息的事务状态,则默认回滚消息。

2.事务消息发送流程

RocketMQ事务消息发送者:org.apache.rocketmq.client.producer.TransactionMQProducer

public class TransactionMQProducer extends DefaultMQProducer {
    private int checkThreadPoolMinSize = 1;
    private int checkThreadPoolMaxSize = 1;
    private int checkRequestHoldMax = 2000;
	// 事务状态回查异步执行线程池
    private ExecutorService executorService;
	// 事务监听器,主要定义实现本地事务状态执行、本地事务状态回查两个接口
    private TransactionListener transactionListener;
}

public interface TransactionListener {
	// 执行本地事务
    LocalTransactionState executeLocalTransaction(final Message msg, final Object arg);
	// 事务消息状态回查
    LocalTransactionState checkLocalTransaction(final MessageExt msg);
}

事务消息发送流程:

@Override
public TransactionSendResult sendMessageInTransaction(final Message msg,
    final Object arg) throws MQClientException {
    if (null == this.transactionListener) {	// 如果事务监听器为空,直接抛出异常
        throw new MQClientException("TransactionListener is null", null);
    }

    msg.setTopic(NamespaceUtil.wrapNamespace(this.getNamespace(), msg.getTopic()));
    // 最终调用DefaultMQProducerImpl的sendMessageInTransaction方法
    return this.defaultMQProducerImpl.sendMessageInTransaction(msg, null, arg);
}
  • Step1:首先为消息添加属性,TRAN_MSG和PGROUP,分别表示消息为prepare消息、消息所属消息生产组。
// DefaultMQProducerImpl#sendMessageInTransaction
SendResult sendResult = null;
// 表示该消息为prepare消息
MessageAccessor.putProperty(msg, MessageConst.PROPERTY_TRANSACTION_PREPARED, "true");
// 设置消息生产者组:在查询事务消息本地事务状态时,从该生产者组中随机选择一个消息生产者即可
MessageAccessor.putProperty(msg, MessageConst.PROPERTY_PRODUCER_GROUP,                                                                                                  this.defaultMQProducer.getProducerGroup());
try {
    // 通过同步调用方式向RocketMQ发送消息
    sendResult = this.send(msg);
} catch (Exception e) {
    throw new MQClientException("send message Exception", e);
}
  • Step2:根据消息发送结果执行相应的操作
// DefaultMQProducerImpl#sendMessageInTransaction
LocalTransactionState localTransactionState = LocalTransactionState.UNKNOW;
Throwable localException = null;
switch (sendResult.getSendStatus()) {
    case SEND_OK: {	// 如果消息发送成功
        try {
            if (sendResult.getTransactionId() != null) {
                msg.putUserProperty("__transactionId__", sendResult.getTransactionId());
            }
            String transactionId = msg.getProperty(MessageConst.PROPERTY_UNIQ_CLIENT_MESSAGE_ID_KEYIDX);
            if (null != transactionId && !"".equals(transactionId)) {
                msg.setTransactionId(transactionId);
            }
            if (null != localTransactionExecuter) {
                localTransactionState = localTransactionExecuter.executeLocalTransactionBranch(msg, arg);
            } else if (transactionListener != null) {
                log.debug("Used new transaction API");
                // 执行TransactionListener.executeLocalTransaction方法
                // 记录事务消息的本地事务状态,为后续的事务状态回查提供唯一依据
                localTransactionState = transactionListener.executeLocalTransaction(msg, arg);
            }
            if (null == localTransactionState) {
                localTransactionState = LocalTransactionState.UNKNOW;
            }

            if (localTransactionState != LocalTransactionState.COMMIT_MESSAGE) {
                log.info("executeLocalTransactionBranch return {}", localTransactionState);
                log.info(msg.toString());
            }
        } catch (Throwable e) {
            log.info("executeLocalTransactionBranch exception", e);
            log.info(msg.toString());
            localException = e;
        }
    }
    break;
    case FLUSH_DISK_TIMEOUT:
    case FLUSH_SLAVE_TIMEOUT:
    case SLAVE_NOT_AVAILABLE:
        // 如果消息发送失败,设置本次事务状态为回滚消息
        localTransactionState = LocalTransactionState.ROLLBACK_MESSAGE;
        break;
    default:
        break;
}
  • Step3:结束事务。根据第二步返回的事务状态执行提交、回滚或暂时不处理事务

    • LocalTransactionState.COMMIT_MESSAGE:提交事务
    • LocalTransactionState.ROLLBACK_MESSAGE:回滚事务
    • LocalTransactionState.UNKNOW:结束事务,但不做任何处理
// DefaultMQProducerImpl#sendMessageInTransaction
try {
    // 第三节重点分析该方法
    this.endTransaction(msg, sendResult, localTransactionState, localException);
} catch (Exception e) {
    log.warn("local transaction execute " + localTransactionState + ", but end broker transaction failed", e);
}

如果是事务消息则备份消息的原主题与原消息消费队列,然后将主题变更为RMQ_SYS_TRANS_HALF_TOPIC,消费队列变更为0,然后消息按照普通消息存储在commitlog文件进而转发到RMQ_SYS_TRANS_HALF_TOPIC主题对应的消息消费队列。

也就是说,事务消息在未提交之前并不会存入消息原有主题,自然也不会被消费者消费。既然变更了主题,RocketMQ通常会采用定时任务(单独的线程)去消费该主题,然后将该消息在满足特定条件下恢复消息主题,进而被消费者消费

TransactionMQproducer事务发送流程:

image.png

3.提交或回滚事务

// DefaultMQProducerImpl#endTransaction
String transactionId = sendResult.getTransactionId();
// 根据消息所属的消息队列的broker名称获取IP与端口信息
final String brokerAddr = this.mQClientFactory.                                                                                         findBrokerAddressInPublish(sendResult.getMessageQueue().getBrokerName());
EndTransactionRequestHeader requestHeader = new EndTransactionRequestHeader();
requestHeader.setTransactionId(transactionId);
requestHeader.setCommitLogOffset(id.getOffset());
// 根据本地执行事务的壮观发送结束事务命令
switch (localTransactionState) {
    case COMMIT_MESSAGE:
        requestHeader.setCommitOrRollback(MessageSysFlag.TRANSACTION_COMMIT_TYPE);
        break;
    case ROLLBACK_MESSAGE:
        requestHeader.setCommitOrRollback(MessageSysFlag.TRANSACTION_ROLLBACK_TYPE);
        break;
    case UNKNOW:
        requestHeader.setCommitOrRollback(MessageSysFlag.TRANSACTION_NOT_TYPE);
        break;
    default:
        break;
}

Broker服务端的结束事务处理器为:EndTransactionProcessor

事务回滚无须将消息恢复原主题,直接删除prepare消息即可

// EndTransactionProcessor#processRequest
// 如果结束事务为提交事务
if (MessageSysFlag.TRANSACTION_COMMIT_TYPE == requestHeader.getCommitOrRollback()) {
    // 首先从结束事务请求命令中获取消息的物理偏移量
    result = this.brokerController.getTransactionalMessageService().commitMessage(requestHeader);
    if (result.getResponseCode() == ResponseCode.SUCCESS) {
        RemotingCommand res = checkPrepareMessage(result.getPrepareMessage(), requestHeader);
        if (res.getCode() == ResponseCode.SUCCESS) {
            // 恢复消息的主题、消费队列,构建新的消息对象
            MessageExtBrokerInner msgInner = endMessageTransaction(result.getPrepareMessage());
            msgInner.setSysFlag(MessageSysFlag.resetTransactionValue(msgInner.getSysFlag(), requestHeader.getCommitOrRollback()));
            msgInner.setQueueOffset(requestHeader.getTranStateTableOffset());
            msgInner.setPreparedTransactionOffset(requestHeader.getCommitLogOffset());
            msgInner.setStoreTimestamp(result.getPrepareMessage().getStoreTimestamp());
            MessageAccessor.clearProperty(msgInner, MessageConst.PROPERTY_TRANSACTION_PREPARED);
            // 将消息再次存储在commitlog文件中,此时的消息主题为业务方发送的消息,将被转发到对应的消息消费队列,被消费
            RemotingCommand sendResult = sendFinalMessage(msgInner);
            if (sendResult.getCode() == ResponseCode.SUCCESS) {
                // 消息存储后,删除prepare消息,并没有真正删除,而是将prepare消息存储到RMQ_SYS_TRANS_HALF_TOPIC主题,表示该事务消息已经处理过(提交或回滚),为未处理的事务进行事务回查提供查找依据
                this.brokerController.getTransactionalMessageService().                                                                                           deletePrepareMessage(result.getPrepareMessage());
            }
            return sendResult;
        }
        return res;
    }
}

4.事务消息回查事务状态

事务消息存储在消息服务器时主题被替换为RMQ_SYS_TRANS_HALF_TOPIC,执行完本次事务返回本次事务状态为UN_KNOW时,结束事务时将不做任何处理,而是通过事务状态定时回查以期得到发送端明确的事务操作(提交事务或回滚事务)。

RocketMQ通过TransactionMessageCheckService线程定时去检测RMQ_SYS_TRANS_HALF_TOPIC主题中的消息,回查消息的事务状态。检测频率默认为1分钟

  • 只有当消息的存储时间+过期时间大于系统当前时间时,才会对消息执行事务状态回查,否则在下一次周期中执行事务回查操作
  • 如果超过事务回查最大检测次数还是无法获知消息的事务状态,RocketMQ将不会继续对消息进行事务状态回查,而是直接丢弃即相当于回滚事务

事务消息的处理设计如下两个主题:

  • RMQ_SYS_TRANS_HALF_TOPIC:prepare消息的主题,事务消息首先进入到该主题
  • RMQ_SYS_TRANS_OP_HALF_TOPIC:当消息服务器收到事务消息的提交或回滚请求后,会将消息存储在该主题下

下面重点分析回查的实现逻辑:org.apache.rocketmq.broker.transaction.queue.TransactionalMessageServiceImpl#check

在执行事务消息回查之前,先通过putBackHalfMsgQueue方法把该消息存储在commitlog文件,新的消息设置为最新的物理偏移量,这么做的目的:

  • 为了简化prepare消息队列和处理队列的消息消费进度处理,先存储,然后消费进度向前推动,重复发送的消息在事务回查之前会判断是否处理过(通过fillOpRemoveMap方法实现)
  • 需要修改消息的检查次数,RocketMQ的存储设计采用顺序写,去修改已存储的消息,其性能无法得到保障
// TransactionalMessageServiceImpl#check
@Override
public void check(long transactionTimeout, int transactionCheckMax,
    AbstractTransactionalMessageCheckListener listener) {
    try {
        String topic = TopicValidator.RMQ_SYS_TRANS_HALF_TOPIC;
        // 获取RMQ_SYS_TRANS_HALF_TOPIC主题下的所有消息队列,然后依次处理
        Set<MessageQueue> msgQueues = transactionalMessageBridge.fetchMessageQueues(topic);
        if (msgQueues == null || msgQueues.size() == 0) {
            log.warn("The queue of topic is empty :" + topic);
            return;
        }
		// 对获取到的消息队列进行依次处理
        for (MessageQueue messageQueue : msgQueues) {
            long startTime = System.currentTimeMillis();
            MessageQueue opQueue = getOpQueue(messageQueue);
            // 根据消息队列获取与之对应的消费队列
            long halfOffset = transactionalMessageBridge.fetchConsumeOffset(messageQueue);
            long opOffset = transactionalMessageBridge.fetchConsumeOffset(opQueue);
            if (halfOffset < 0 || opOffset < 0) {
                log.error("MessageQueue: {} illegal offset read: {}, op offset: {},skip this queue", messageQueue,
                    halfOffset, opOffset);
                continue;
            }

            List<Long> doneOpOffset = new ArrayList<>();
            HashMap<Long, Long> removeMap = new HashMap<>();
            // 根据当前处理进度依次从已处理队列拉取32条,方便判断当前处理的消息是否已经被处理过
            // 如果处理过则无须再次发送事务状态回查请求,避免重复发送事务回查请求
            PullResult pullResult = fillOpRemoveMap(removeMap, opQueue, opOffset, halfOffset, doneOpOffset);
            if (null == pullResult) {
                log.error("The queue={} check msgOffset={} with opOffset={} failed, pullResult is null",
                    messageQueue, halfOffset, opOffset);
                continue;
            }
            // single thread
            int getMessageNullCount = 1;	// 获取空消息的次数
            long newOffset = halfOffset;	// 当前处理RMQ_SYS_TRANS_HALF_TOPIC#queueId的最新进度
            long i = halfOffset;			// 当前处理消息的队列偏移量
            while (true) {
                // 如果处理该任务时长超过60s,则需要等待下次任务调度
                if (System.currentTimeMillis() - startTime > MAX_PROCESS_TIME_LIMIT) {
                    log.info("Queue={} process time reach max={}", messageQueue, MAX_PROCESS_TIME_LIMIT);
                    break;
                }
                if (removeMap.containsKey(i)) {	// 如果该消息已被处理,则继续处理下一条消息
                    log.debug("Half offset {} has been committed/rolled back", i);
                    Long removedOpOffset = removeMap.remove(i);
                    doneOpOffset.add(removedOpOffset);
                } else {
                    GetResult getResult = getHalfMsg(messageQueue, i);	// 根据消息队列偏移量i从消费队列中获取消息
                    MessageExt msgExt = getResult.getMsg();
                    // 从待处理任务队列中拉取消息,如果未拉取到消息,允许重复次数进行操作,最大重试一次
                    if (msgExt == null) {	
                        // 如果超过重试次数,直接跳出,结束该消息队列的事务状态回查
                        if (getMessageNullCount++ > MAX_RETRY_COUNT_WHEN_HALF_NULL) {
                            break;
                        }
                        // 如果是由于没有新消息而返回空(拉取状态为PullStatus.NO_NEW_MSG),结束该消息队列的事务状态回查
                        if (getResult.getPullResult().getPullStatus() == PullStatus.NO_NEW_MSG) {
                                messageQueue, getMessageNullCount, getResult.getPullResult());
                            break;
                        } else { // 其它原因,设置偏移量i,重新拉取
                            i = getResult.getPullResult().getNextBeginOffset();
                            newOffset = i;
                            continue;
                        }
                    }

                    // 判断该消息是否需要discard(吞没、丢弃、不处理)或skip(跳过)
                    // needDiscard:如果该消息回查次数超过最大回查次数(15次),则被丢弃
                    // 每回查一次,MessageConst.PROPERTY_TRANSACTION_CHECK_TIMES+1
                    // needSkip:如果事务消息超过文件的过期时间,默认为72小时,则跳过该消息
                    if (needDiscard(msgExt, transactionCheckMax) || needSkip(msgExt)) {
                        listener.resolveDiscardMsg(msgExt);
                        newOffset = i + 1;
                        i++;
                        continue;
                    }
                    if (msgExt.getStoreTimestamp() >= startTime) {
                        log.debug("Fresh stored. the miss offset={}, check it later, store={}", i,
                            new Date(msgExt.getStoreTimestamp()));
                        break;
                    }
					// 消息已存储的时间,为系统当前时间 - 消息存储的时间戳
                    long valueOfCurrentMinusBorn = System.currentTimeMillis() - msgExt.getBornTimestamp();
                    // 立即检测事务消息的时间,即假设事务消息发送成功后应用程序事务提交的时间,在这段时间内,事务未提交,不应该在该时间段向应用程序发送回查请求
                    long checkImmunityTime = transactionTimeout;
                    
                    String checkImmunityTimeStr = msgExt.getUserProperty  // 事务消息回查请求最晚时间
                        					(MessageConst.PROPERTY_CHECK_IMMUNITY_TIME_IN_SECONDS);
                    if (null != checkImmunityTimeStr) {	// 如果消息指定了事务消息过期时间属性
                        checkImmunityTime = getImmunityTime(checkImmunityTimeStr, transactionTimeout);
                        if (valueOfCurrentMinusBorn < checkImmunityTime) {
                            if (checkPrepareQueueOffset(removeMap, doneOpOffset, msgExt)) {
                                newOffset = i + 1;
                                i++;
                                continue;
                            }
                        }
                    } else {	// 如果当前时间还未过应用程序事务结束时间,则跳出本次处理,等下一次再试
                        if ((0 <= valueOfCurrentMinusBorn) && (valueOfCurrentMinusBorn < checkImmunityTime)) {
                            log.debug("New arrived, the miss offset={}, check it later checkImmunity={}, born={}", i,
                                checkImmunityTime, new Date(msgExt.getBornTimestamp()));
                            break;
                        }
                    }
                    List<MessageExt> opMsg = pullResult.getMsgFoundList();
                    // 如果操作队列没有已处理消息并且已经超过事务超时时间
                    boolean isNeedCheck = (opMsg == null && valueOfCurrentMinusBorn > checkImmunityTime)
                        // 如果操作队列不为空并且最后一条消息的存储时间已经超过事务超时时间
                        || (opMsg != null && (opMsg.get(opMsg.size() - 1).getBornTimestamp() - startTime > transactionTimeout))
                        || (valueOfCurrentMinusBorn <= -1);
					// 判断是否需要发送事务回查消息
                    if (isNeedCheck) {
                        // 如果需要发送事务回查消息,则先将消息再次发送到RMQ_SYS_TRANS_HALF_TOPIC主题中,发送成功返回真
                        // 使用线程池异步发送回查消息,为了回查消费进度保存的简化,只要发送回查消息,当前回查进度就会向前推动
                        if (!putBackHalfMsgQueue(msgExt, i)) {
                            continue;
                        }
                        listener.resolveHalfMsg(msgExt);
                    } else {
                        // 如果无法判断是否发送回查消息,则加载更多的已处理消息进行筛选
                        pullResult = fillOpRemoveMap(removeMap, opQueue, pullResult.getNextBeginOffset(), halfOffset, doneOpOffset);
                        log.debug("The miss offset:{} in messageQueue:{} need to get more opMsg, result is:{}", i,
                            messageQueue, pullResult);
                        continue;
                    }
                }
                newOffset = i + 1;
                i++;
            }
            if (newOffset != halfOffset) {	// 保存(Prepare)消息队列的回查进度
                transactionalMessageBridge.updateConsumeOffset(messageQueue, newOffset);
            }
            long newOpOffset = calculateOpOffset(doneOpOffset, opOffset);
            if (newOpOffset != opOffset) {	// 保存处理队列(OP)的进度
                transactionalMessageBridge.updateConsumeOffset(opQueue, newOpOffset);
            }
        }
    } catch (Throwable e) {
        log.error("Check error", e);
    }

}

通过异步方式发送消息回查的实现过程:

  • 首先构建事务状态回查请求消息,核心参数包括消息offsetId、消息ID(索引)、消息事务ID、事务消息队列中的偏移量、消息主题、消息队列。
  • 然后根据消息的生产者组,从中随机选择一个消息发送者
  • 最后向消息发送者发送事务回查命令

事务状态回查流程图:

image.png

5.总结

RocketMQ的事务消息基于两阶段提交和事务主题回查机制来实现

两阶段提交:即首先发送prepare消息,待事务提交或回滚发送commit、rollback命令。再结合定时任务,RocketMQ使用专门的线程以特定的频率对RocketMQ服务器上的prepare消息进行处理,向发送端查询事务消息的状态来决定是否提交或回滚消息

8.RocketMQ实战

1.消息批量发送

RocketMQ批量发送是将同一主题的多条消息一起打包发送到消息服务单,减少网络调用次数,提高网络传输效率

import org.apache.rocketmq.client.producer.DefaultMQProducer;
import org.apache.rocketmq.client.producer.SendResult;
import org.apache.rocketmq.client.producer.SendStatus;
import org.apache.rocketmq.common.message.Message;
​
import java.util.ArrayList;
import java.util.List;
​
public class RocketMQBatchProducer {
    public static void main(String[] args) throws Exception {
        // 设置生产者组名
        DefaultMQProducer producer = new DefaultMQProducer("your_producer_group_name");
        // 设置Namesrv地址
        producer.setNamesrvAddr("your_namesrv_address");
        // 启动生产者
        producer.start();
​
        try {
            // 创建消息列表
            List<Message> messageList = new ArrayList<>();
​
            // 循环添加消息到列表
            for (int i = 0; i < 10; i++) {
                // 创建消息对象,参数分别为:topic、tags、keys、body
                Message message = new Message("your_topic", "your_tags", "your_keys_" + i, ("Hello RocketMQ " + i).getBytes());
                // 将消息添加到列表中
                messageList.add(message);
            }
​
            // 批量发送消息
            SendResult sendResult = producer.send(messageList);
​
            // 检查发送结果
            if (sendResult.getSendStatus() == SendStatus.SEND_OK) {
                System.out.println("消息发送成功");
            } else {
                System.out.println("消息发送失败");
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
​
        // 关闭生产者
        producer.shutdown();
    }
}
​

2.消息发送队列自选择

消息发送默认根据主题的路由信息(主题消息队列)进行负载均衡,负载均衡策略为轮询策略

在发送消息时,我们指定了一个消息队列选择器,该选择器根据订单ID(作为参数传递)选择要发送消息的消息队列。在示例代码中,我们简单地使用订单ID对消息队列进行取模来进行选择

场景示例: 消息队列选择器可以用于根据业务需求将特定类型的消息发送到特定的消息队列。

例如,在电商系统中,可以根据订单ID将订单相关的消息发送到对应的消息队列,以便在消费者端按照订单进行有序处理实现相同订单的不同消息能统一发送到同一个消息消费队列上,避免引入分布式锁

另外,也可以根据其他条件如用户ID、地理位置等进行消息队列选择,以满足个性化的业务需求。

import org.apache.rocketmq.client.producer.DefaultMQProducer;
import org.apache.rocketmq.client.producer.MessageQueueSelector;
import org.apache.rocketmq.client.producer.SendResult;
import org.apache.rocketmq.common.message.Message;
import org.apache.rocketmq.common.message.MessageQueue;
​
import java.util.List;
​
public class RocketMQQueueSelectorProducer {
    public static void main(String[] args) throws Exception {
        // 设置生产者组名
        DefaultMQProducer producer = new DefaultMQProducer("your_producer_group_name");
        // 设置Namesrv地址
        producer.setNamesrvAddr("your_namesrv_address");
        // 启动生产者
        producer.start();
​
        try {
            // 创建消息对象,参数分别为:topic、tags、keys、body
            Message message = new Message("your_topic", "your_tags", "your_keys", "Hello RocketMQ".getBytes());
​
            // 发送消息,并指定消息队列选择器
            SendResult sendResult = producer.send(message, new MessageQueueSelector() {
                @Override
                public MessageQueue select(List<MessageQueue> list, Message message, Object arg) {
                    // 根据业务逻辑选择要发送的消息队列
                    int orderId = (int) arg;
                    int index = orderId % list.size();
                    return list.get(index);
                }
            }, 1234);  // 将订单ID作为参数传递给消息队列选择器
​
            // 检查发送结果
            if (sendResult.getSendStatus() == SendStatus.SEND_OK) {
                System.out.println("消息发送成功");
            } else {
                System.out.println("消息发送失败");
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
​
        // 关闭生产者
        producer.shutdown();
    }
}
​

股票交易场景使用

在股票交易场景下,消息队列选择器可以用于将交易相关的消息发送到特定的消息队列,以实现一些特殊需求和业务逻辑。以下是一些可能的使用场景:

  1. 交易匹配:股票交易通常包括买入和卖出两个操作,而且需要匹配买家和卖家的交易订单。你可以使用消息队列选择器,根据订单的类型(买入/卖出)选择相应的消息队列来处理订单匹配逻辑。例如,将买入订单发送到一个专门负责匹配卖出订单的消息队列,确保交易的顺序和匹配逻辑。
  2. 交易排序:在高并发的股票交易系统中,交易订单可能会同时到达,为了保证交易的有序性,你可以使用消息队列选择器将交易订单按照特定的规则(例如时间戳、优先级等)发送到不同的消息队列中进行排序。消费者可以从指定消息队列中按照顺序接收并处理交易订单。
  3. 交易分片:对于大型股票交易系统,可能存在多个交易服务器或集群来处理交易请求。你可以使用消息队列选择器将交易订单根据某种规则分发到不同的交易服务器上,实现负载均衡和并行处理。例如,可以根据股票代码的哈希值或者其他规则来选择消息队列,将同一支股票的交易订单发送到同一个交易服务器。
  4. 异常处理:在股票交易系统中,可能会出现一些异常情况,如订单超时、交易失败等。你可以使用消息队列选择器将这些异常订单发送到专门的异常处理消息队列中,由专门的异常处理服务消费并进行相应的处理逻辑,例如退款、发送提示信息等。

总的来说,消息队列选择器在股票交易场景下可以用于交易匹配、交易排序、交易分片和异常处理等方面,以实现一些特殊需求和业务逻辑,提供更稳定、高效的股票交易系统。

3.消息过滤

TAG模式过滤

消息发送时,可以为每一条消息设置一个TAG标签,消息消费者订阅自己感兴趣的TAG

一般的使用场景是:对于同一类的功能创建一个主题,但对该主题下的数据,可能不同的系统关心的数据不一样,但基础数据各个系统都需要同步,设置标签为TOPIC_TAG_ALL,而订单数据只有订单下游子系统关心,其它系统并不关心,则设置标签为TOPIC_TAG_ORD,库存子系统则关注库存相关的数据,设置标签为TOPIC_TAG_CARACITY

import org.apache.rocketmq.client.consumer.DefaultMQPushConsumer;
import org.apache.rocketmq.client.consumer.listener.ConsumeConcurrentlyContext;
import org.apache.rocketmq.client.consumer.listener.ConsumeConcurrentlyStatus;
import org.apache.rocketmq.client.consumer.listener.MessageListenerConcurrently;
import org.apache.rocketmq.common.message.MessageExt;
​
import java.util.List;
​
public class RocketMQTagFilterConsumer {
    public static void main(String[] args) throws Exception {
        // 设置消费者组名
        DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("your_consumer_group_name");
        // 设置Namesrv地址
        consumer.setNamesrvAddr("your_namesrv_address");
​
        // 订阅要消费的主题和标签(使用“*”表示订阅所有标签)
        consumer.subscribe("your_topic", "TagA || TagB");
​
        // 注册消息监听器
        consumer.registerMessageListener(new MessageListenerConcurrently() {
            @Override
            public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> list, ConsumeConcurrentlyContext context) {
                for (MessageExt message : list) {
                    System.out.println("Received message: " + new String(message.getBody()));
                }
                return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
            }
        });
​
        // 启动消费者
        consumer.start();
​
        System.out.println("Consumer started.");
    }
}
​

SQL表达模式过滤

import org.apache.rocketmq.client.consumer.DefaultMQPushConsumer;
import org.apache.rocketmq.client.consumer.listener.ConsumeConcurrentlyContext;
import org.apache.rocketmq.client.consumer.listener.ConsumeConcurrentlyStatus;
import org.apache.rocketmq.client.consumer.listener.MessageListenerConcurrently;
import org.apache.rocketmq.common.message.MessageExt;
​
import java.util.List;
​
public class RocketMQSqlFilterConsumer {
    public static void main(String[] args) throws Exception {
        // 设置消费者组名
        DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("your_consumer_group_name");
        // 设置Namesrv地址
        consumer.setNamesrvAddr("your_namesrv_address");
​
        // 订阅要消费的主题和标签
        // 发送的消息msg需要设置属性,msg.putUserProperty("your_property", 11);
        consumer.subscribe("your_topic", MessageSelector.bySql("your_property > 10"));
​
        // 注册消息监听器
        consumer.registerMessageListener(new MessageListenerConcurrently() {
            @Override
            public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> list, ConsumeConcurrentlyContext context) {
                for (MessageExt message : list) {
                    System.out.println("Received message: " + new String(message.getBody()));
                }
                return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
            }
        });
​
        // 启动消费者
        consumer.start();
​
        System.out.println("Consumer started.");
    }
}
​

类模式过滤

需要实现自定义消息过滤器,实现MessageFilter接口,重写match方法,然后消费者订阅消息主题,入参填写该实现类

股票交易场景使用

在股票交易场景下,可以使用消息过滤来实现一些功能。以下是一些可能使用消息过滤的情况:

  1. 行情筛选:股票交易中,需要及时获取到特定股票的行情信息。你可以使用消息过滤机制,在消息生产者端根据股票代码设置消息属性,然后在消费者端使用消息过滤功能,只选择感兴趣的股票代码进行消费。
  2. 交易通知:在进行股票交易时,一方发起交易后,需要将交易通知发送给相关方。通过设置消息属性,可以使用消息过滤筛选出特定的交易类型或交易对象,然后将通知只发送给符合条件的消费者。
  3. 风险控制:股票交易中存在各种风险,如价格波动、交易异常等。通过设置消息属性,可以针对异常情况发送特定的风控信息,使用消息过滤筛选出对应的风控信息并进行相应的处理。

这些仅是一些示例用途,具体使用消息过滤要根据实际需求和业务场景进行设计和实现。使用消息过滤可以帮助提高系统的性能和效率,减少不必要的消息传递和处理,同时提供更加精确的消息选择和消费能力。