3.RocketMQ实战,show you the code(你懂RocketMQ吗?)

1,140 阅读13分钟

第一章为技术对比,第二章为特性简介,第三章纯干货show you the code,第四章为集群搭建。如果文章对您有帮助,请点赞文章,谢谢大家。

RocketMQ-Spring

作为我们的使用,我们没有必要去使用原生的RocketMQ,可以通过使用RocketMQ-spring框架快速的上手RocketMQ

配置文件及依赖管理:

pom.xml:

 <dependencies>
        <!--RocketMQ-spring依赖-->
        <dependency>
            <groupId>org.apache.rocketmq</groupId>
            <artifactId>rocketmq-spring-boot-starter</artifactId>
            <version>2.1.0</version>
        </dependency>
        <!--引入web依赖更加方便测试-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>cn.lookdoor.parent</groupId>
            <artifactId>lookdoor-common-client</artifactId>
            <version>1.0.0-SNAPSHOT</version>
            <scope>compile</scope>
        </dependency>
    </dependencies>

application.yml:

server:
  port: 8010
spring:
  application:
    name: rocketMQ
demo:
  rocketmq:
    topic:
      stringTopic: string-topic
      orderTopic: order-paid-topic
      msgExtTopic: message-ext-topic
      transTopic: spring-transaction-topic
      user: user-topic
      stringReplyTopic: string-reply-topic
      bytesRequestTopic: bytesRequestTopic
      objectReplyTopic: objectReplyTopic
      genericRequestTopic: genericRequestTopic
    tag:
       stringTag: strTag
    consumer:
      bytesRequestConsumer: bytesRequestConsumer
      stringRequestConsumer: stringRequestConsumer
      objectRequestConsumer: objectRequestConsumer
      genericRequestConsumer: genericRequestConsumer
    extNameServer: 127.0.0.1:9876 #自定义nameSrv
rocketmq:
  name-server: localhost:9876 #namesrv地址
  producer:
    group: test-group        
    sendMessageTimeout: 300000

同步发送消息(Sync Send Message)

这种可靠性同步地发送方式使用的比较广泛,比如:重要的消息通知,短信通知。

    /**
     * 同步发送消息
     * Consumer:SyncConsumer
     * 消息类型:String
     *
     * @author gongzheng
     * @date 2020/3/16
     */
    @RequestMapping("syncSend")
    public void syncSend() {
        SendResult sendResult = rocketMQTemplate.syncSend(syncTopic, "syncTest");
        System.out.println("-----sync send result is :" + sendResult.getSendStatus().toString());
    }
/**
 * 同步消费者
 *
 * @author GongZheng
 * 2020/5/19
 */
@Service
@RocketMQMessageListener(topic = "${demo.rocketmq.topic.syncTopic}", consumerGroup = "sync-consumer")
public class SyncConsumer implements RocketMQListener<String> {
    @Override
    public void onMessage(String message) {
        System.out.println("SyncConsumer receive msg:"+message);
    }
}
​
​

异步发送消息(Async Send Message)

异步消息通常用在对响应时间敏感的业务场景,即发送端不能容忍长时间地等待Broker的响应。

    /**
     * 异步发送消息
     * Consumer:asyncConsumer
     * 消息类型:自定义对象UserVo
     *
     * @author gongzheng
     * @date 2020/3/16
     */
    @RequestMapping("asyncSend")
    public void asyncSend() {
        rocketMQTemplate.asyncSend(asyncTopic, UserVo.builder()
                .age(18)
                .name("Tom")
                .build(), new SendCallback() {
            @Override
            public void onSuccess(SendResult sendResult) {
                System.out.println("-----async send result is :" + sendResult.getSendStatus().toString());
            }
​
            @Override
            public void onException(Throwable exc) {
                System.out.println("-----async onException Throwable :" + exc.getMessage());
            }
        });
    }
/**
 * AsyncConsumer
 *
 * @author gongzheng
 * @date 2020/3/16
 */
@Service
@RocketMQMessageListener(topic = "${demo.rocketmq.topic.asyncTopic}", consumerGroup = "async-consumer")
public class AsyncConsumer implements RocketMQListener<UserVo> {
    @Override
    public void onMessage(UserVo userVo) {
        System.out.printf(">>>>>> async_consumer received: %s ; age: %s ; name: %s \n", userVo, userVo.getAge(), userVo.getName());
    }
}
​

单工消息(OneWay Message)

发送特点为发送方只负责发送消息,不等待服务器回应且没有回调函数触发,即只发送请求不等待应答。 此方式发送消息的过程耗时非常短,一般在微秒级别。

  /**
     * oneWay发送消息
     * Consumer:oneWayConsumer
     * 消息类型:SpringFrameWork消息体
     *
     * @author gongzheng
     * @date 2020/3/16
     */
@RequestMapping("oneWaySend")
public void oneWaySend() {
    Message message = MessageBuilder.withPayload("oneWayMessage")
            .setHeader(RocketMQHeaders.MESSAGE_ID, null)
            .build();
   rocketMQTemplate.sendOneWay(oneWayTopic,message);
}
/**
 * OneWayConsumer
 *
 * @author GongZheng
 * 2020/5/21
 */@Service
@RocketMQMessageListener(topic = "${demo.rocketmq.topic.oneWayTopic}", consumerGroup = "oneWay-consumer")
public class OneWayConsumer implements RocketMQListener<MessageExt> {
    @Override
    public void onMessage(MessageExt message) {
        System.out.printf(">>>>>> oneWay_consumer received: received message, msgId: %s, body:%s \n", message.getMsgId(), new String(message.getBody()));
    }
}
​

消息过滤(Filter Message)

Tag过滤

RocketMQ的最佳实践中推荐:一个应用尽可能用一个Topic,消息子类型用tags来标识,tags可以由应用自由设置。 在使用rocketMQTemplate发送消息时,通过设置发送方法的destination参数来设置消息的目的地,destination的格式为topicName:tagName:前面表示topic的名称,后面表示tags名称。

注意:

tags从命名来看像是一个复数,但发送消息时,destination只能指定一个topic下的一个tag,不能指定多个。

  /**
     * 消息过滤:tag模式
     * Consumer:FilterByTagConsumer
     * 消息类型:SpringFrameWork消息体
     *
     * @author gongzheng
     * @date 2020/3/16
     */
    @RequestMapping("filterByTag")
    public void filterByTag() {
        Message tag1Message = MessageBuilder.withPayload("tag1Message")
                .setHeader(RocketMQHeaders.MESSAGE_ID, null)
                .build();
        rocketMQTemplate.syncSend(tagTopic + ":tag1", tag1Message);
        System.out.println("-----sync onSuccess filter is tag1");
​
        Message tag2Message = MessageBuilder.withPayload("tag2Message")
                .setHeader(RocketMQHeaders.MESSAGE_ID, null)
                .build();
        rocketMQTemplate.convertAndSend(tagTopic+":tag2",tag2Message);
        System.out.println("-----converAndSend onSuccess filter is tag2");
        
        Message tag3Message = MessageBuilder.withPayload("tag3Message")
                .setHeader(RocketMQHeaders.MESSAGE_ID, null)
                .build();
​
        rocketMQTemplate.asyncSend(tagTopic + ":tag3", tag3Message, new SendCallback() {
            @Override
            public void onSuccess(SendResult sendResult) {
                System.out.println("-----async onSuccess SendResult:" + sendResult.getSendStatus().toString() + "and filter is tag3");
            }
​
            @Override
            public void onException(Throwable throwable) {
                System.out.println("-----async onException Throwable :" + throwable.getMessage());
            }
        });
    }
/**
 * Tag消息过滤consumer
 *
 * @author GongZheng
 * 2020/5/19
 */
@Service
@RocketMQMessageListener(topic = "${demo.rocketmq.topic.tagTopic}",selectorExpression = " tag1 || tag2",consumerGroup ="tag-consumer")
public class FilterByTagConsummer  implements RocketMQListener<MessageExt> {
    @Override
    public void onMessage(MessageExt message) {
        System.out.printf("------- MessageExtConsumer received message, msgId: %s, body:%s \n", message.getMsgId(), new String(message.getBody()));
    }
}

代码细节:细心的同学大概可以注意到这个Demo里我们用到了一个发送消息的方法,convertAndSend,这个与syncSend,asyncSend有什么区别呢?

关于convertAndSend方法我们看源码可以看到

/**
 * Convert the given Object to serialized form, possibly using a
 * {@link MessageConverter}, wrap it as a message with the given
 * headers and apply the given post processor.
 * @param payload the Object to use as payload
 * @param headers headers for the message to send
 * @param postProcessor the post processor to apply to the message
 * @return the converted message
 */
 protected Message<?> doConvert(Object payload, @Nullable Map<String, Object> headers,
    @Nullable MessagePostProcessor postProcessor) {...}

在调用这个方法是会执行doConvert方法,这个方法的作用就是将我们发送的payload转换成Message体发送出去,关于Message为SpringFrameWork的消息体,在rocketMQ中会将spring message与rocketmq message进行转换

converAndSend走的发送方式是syncSend

ps:个人理解,如果有不对的地方欢迎同学指出

SQL表达式过滤

Notice

RocketMQ的Broker默认并不支持SQL表达式过滤消息sh bin/mqadmin updateBrokerConfig -b localhost:10911 -n localhost:9876 -k enablePropertyFilter -v true。执行这条命令,使Broker能够支持SQL消息过滤

Grammars

RocketMQ只定义了一些基础的SQL语法来支持过滤模式

  • 数字类型的比较: > , >=, < , <=, BETWEEN, =

    • eg:a between and 3
  • 字符类型的比较: =, <> , IN

    • eg: a = 'abc' (必须使用单引号)
  • IS NULL or IS NOT NULL;

    • eg:a is null
  • 逻辑运算符 AND, OR, NOT,TRUE or FALSE

    /**
     * 消息过滤:SQL92模式
     * Consumer:FilterBySqlConsumer
     * 消息类型:SpringFrameWork消息体
     *
     * @author gongzheng
     * @date 2020/3/23
     */
    @RequestMapping("msgFilterBySQL")
    public void msgFilterBySQL(@RequestParam int filter) throws Exception {
        Map queryMap = new HashMap<String, Object>();
        queryMap.put("a", filter);
        Message msg = MessageBuilder.withPayload(UserVo.builder()
                .age(18)
                .name("Tom")
                .build()).setHeader(RocketMQHeaders.MESSAGE_ID, null).build();
        rocketMQTemplate.convertAndSend(sqlTopic, msg, queryMap);
        System.out.println("syncSend topic :" + sqlTopic + "and sql filter is :a = " + filter);
    }
/**
 * Sql过滤consumer
 *
 * @author gongzheng
 * @date 2020/3/23
 */
@Service
@RocketMQMessageListener(topic = "${demo.rocketmq.topic.sqlTopic}",selectorType = SelectorType.SQL92,selectorExpression = "a between 0 and 3",consumerGroup ="sql-consumer")
public class FilterBySqlConsumer implements RocketMQListener<MessageExt> {
​
    @Override
    public void onMessage(MessageExt message) {
        System.out.printf("------- MsgSQLConsumer received message, msgId: %s, body:%s \n", message.getMsgId(), new String(message.getBody()));
    }
}

顺序消息(Orderly Message)

消息有序指的是可以按照消息的发送顺序来消费(FIFO)。。

顺序消费的原理解析,在默认的情况下消息发送会采取Round Robin轮询方式把消息发送到不同的queue(分区队列);而消费消息的时候从多个queue上拉取消息,这种情况发送和消费是不能保证顺序。

但是如果控制发送的顺序消息只依次发送到同一个queue中,消费的时候只从这个queue上依次拉取,则就保证了顺序。

RocketMQ可以严格的保证消息有序,有如下两类

  • 全局有序:在一个topic中,发送和消费参与的queue只有一个
  • 分区有序:在一个topic中,发送和消费消息的过程有多个queue参与
   /**
     * 同步顺序发送消息
     * Consumer:OrderlyConsumer
     * 消息类型:SpringFrameWork消息体
     *
     * @author gongzheng
     * @date 2020/3/24
     */
    @RequestMapping("syncOrderly")
    public void syncOrderly() throws Exception {
        String[] tags = new String[]{"TagA", "TagC", "TagD"};
        for (int i = 0; i < 10; i++) {
            String body = " Hello RocketMQ - " + i + tags[i % tags.length];
            Message msg = MessageBuilder.withPayload(body).setHeader(RocketMQHeaders.MESSAGE_ID, null).build();
            String orderly = String.valueOf(i);
            //全区有序
            rocketMQTemplate.syncSendOrderly(orderlyTopic, msg, "tagB");
            //分区有序
            rocketMQTemplate.syncSendOrderly(orderlyTopic, msg, orderly);
        }
    }
/**
 * 消费顺序消费者
 *
 * @author gongzheng
 * @date 2020/3/24
 */
@Service
@RocketMQMessageListener(topic = "${demo.rocketmq.topic.orderlyTopic}", consumerGroup = "message-orderly-consumer", consumeMode = ConsumeMode.ORDERLY)
public class OrderlyConsumer implements RocketMQListener<MessageExt> {
    @Override
    public void onMessage(MessageExt message) {
        
        System.out.printf("------- OrderlyConsumer received message, msgId: %s, body:%s \n", message.getMsgId(), new String(message.getBody()));
    }
}
​

异步顺序消息

消息批处理(Batch Send Message)

批量发送消息:

  • 因为引入了rocketmq-spring,并不能友好的支持Message分片
  • 仅支持syncSend
 /**
     * RocketMQ发送消息批处理
     * Consumer:BatchConsumer
     * 消息类型:SpringFrameWork消息体
     *
     * @author gongzheng
     * @date 2020/3/16
     */
    @RequestMapping("BatchSend")
    public void batchSend() {
        List<Message> msgs = new ArrayList<Message>();
        for (int i = 0; i < 10; i++) {
            msgs.add(MessageBuilder.withPayload("Hello RocketMQ Batch Msg#" + i)
                    .setHeader(RocketMQHeaders.KEYS, null)
                    .build());
        }
        SendResult sr = rocketMQTemplate.syncSend(msgBatchTopic, msgs);
        System.out.println("--- Batch messages send result :" + sr + "\n");
    }
​
@Service
@RocketMQMessageListener(topic = "${demo.rocketmq.topic.batchTopic}",consumerGroup ="message-batch-consumer")
public class BatchConsumer implements RocketMQListener<MessageExt> {
    @Override
    public void onMessage(MessageExt message) {
        System.out.printf("------- MessageBatchConsumer received message, msgId: %s, body:%s \n", message.getMsgId(), new String(message.getBody()));
    }
}
​

定时消息(Schedule Message)

定时消息(延迟队列)是指消息发送到broker后,不会立即被消费,等待特定时间投递给真正的topic。 broker有配置项messageDelayLevel

默认值为“1s 5s 10s 30s 1m 2m 3m 4m 5m 6m 7m 8m 9m 10m 20m 30m 1h 2h”,18个level

可以配置自定义messageDelayLevel。

注意,messageDelayLevel是broker的属性,不属于某个topic。发消息时,设置delayLevel等级即可。level有以下三种情况:

  • level == 0,消息为非延迟消息
  • 1<=level<=maxLevel,消息延迟特定时间,例如level==1,延迟1s
  • level > maxLevel,则level== maxLevel,例如level==20,延迟2h

定时消息会暂存在名为SCHEDULE_TOPIC_XXXX的topic中,并根据delayTimeLevel存入特定的queue,queueId = delayTimeLevel – 1,即一个queue只存相同延迟的消息,保证具有相同发送延迟的消息能够顺序消费。broker会调度地消费SCHEDULE_TOPIC_XXXX,将消息写入真实的topic。

需要注意的是,定时消息会在第一次写入和调度写入真实topic时都会计数,因此发送数量、tps都会变高

/**
     * 定时消息
     * Consumer:SchduleConsumer
     * 消息类型:SpringFrameWork消息体
     *
     * @author gongzheng
     * @date 2020/3/25
     */
    @RequestMapping("scheduleMessage")
    public void scheduleMessage() throws MessagingException {
        UserVo userVo = UserVo.builder()
                .age(18)
                .name("Kitty")
                .build();
        Message msg = MessageBuilder.withPayload(userVo).setHeader(RocketMQHeaders.MESSAGE_ID, "messageId1").build();
        //Sync
        SendResult sendResult = rocketMQTemplate.syncSend(scheduleTopic, msg, 30000, 3);
        System.out.printf("syncSend1 to topic %s sendResult=%s %n", msgExtTopic, sendResult);
        //Async
        rocketMQTemplate.asyncSend(scheduleTopic, msg, new SendCallback() {
            @Override
            public void onSuccess(SendResult sendResult) {
                System.out.printf("async onSuccess SendResult=%s %n", sendResult);
            }
​
            @Override
            public void onException(Throwable exc) {
                System.out.printf("async onException Throwable=%s %n", exc);
            }
        }, 30000, 3);
​
    }
/**
 * 定时消息Conumser
 *
 * @author gongzheng
 * @date 2020/3/16
 */
@Service
@RocketMQMessageListener(topic = "${demo.rocketmq.topic.scheduleTopic}",consumerGroup ="schedule-consumer")
public class ScheduleConsumer implements RocketMQListener<MessageExt> {
​
    @Override
    public void onMessage(MessageExt messageExt) {
        System.out.printf("------- MessageExtConsumer received message, msgId: %s, body:%s \n", messageExt.getMsgId(), new String(messageExt.getBody()));
    }
}
​

延迟消息细节

  • 延迟消息发送时一定要配置timeout参数,自己测试下来不能配置为0,否则会报错,会报发送消息超时的异常,如果timeout时间过短,消息还没被消费到也会报这个异常

    org.apache.rocketmq.remoting.exception.RemotingTooMuchRequestException: sendDefaultImpl call timeout

  • 延迟消息的消息体,只能使用org.springframework.messaging.Message.class,因为Message被认为是不可修改的,所以消息的构建一定要使用MessageBuilder方法来构建,其中payload参数(有效负载)也就是我们要传的消息内容

  • Headers的作用
public interface Message<T> {
​
   /**
    * Return the message payload.
    */
   T getPayload();
​
   /**
    * Return message headers for the message (never {@code null} but may be empty).
    */
   MessageHeaders getHeaders();
​
}
  • 消费者配置!非常重要!非常重要!非常重要!消费者在配置时需要使用MessageExt这个类来自于org.apache.rocketmq.common.message,这个类里有一条消息的详细信息,包括messageId,topic之类的,如果不用这个类作为Message的消费者,一定会报错!一定会报错!一定会报错!会报jackson cann't convert json的错,所以这里请务必注意,使用Message作为消息体,需要使用MessageExt作为消费者的监听泛型

回溯消费(BackTrack Consume)

broker中的消息即使被消费成功后,也不会立即删除,而是要到了broker设置的时间才会删除,因此rocketMQ就可以实现回溯消费。所谓回溯消费说穿了就是consumer指定从Broker拉取消息的时间,例如一个小时前的消息,则可以得到已经消费过的消息,实现回溯消费

/**
 * 回溯消费
 * 消息类型:自定义对象
 * topic:syncTopic
 *
 * @author gongzheng
 * @date 2020/5/21
 */
@Service
@RocketMQMessageListener(topic = "${demo.rocketmq.topic.syncTopic}", consumerGroup = "backTrack-consumer")
public class BackTrackConsumer implements RocketMQListener<String>, RocketMQPushConsumerLifecycleListener {
    @Override
    public void onMessage(String message) {
        System.out.println("BackTrackConsumer receive msg:" + message);
    }
​
    @Override
    public void prepareStart(DefaultMQPushConsumer consumer) {
        consumer.setConsumeFromWhere(ConsumeFromWhere.CONSUME_FROM_TIMESTAMP);
        Calendar calendar = Calendar.getInstance();
        calendar.set(Calendar.HOUR_OF_DAY, calendar.get(Calendar.HOUR_OF_DAY) - 1);
        consumer.setConsumeTimestamp(UtilAll.timeMillisToHumanString3(calendar.getTimeInMillis()));
    }
}
​
​

广播消息(Broadcasting Message)

如果我们有一个特别重要的消息,需要每一个订阅了当前Topic的消费者都可以消费到该消息,我们可以使用广播消息。

使用广播消息,我们需要搭建一个小集群,idea支持我们同时启动多个应用,只需要改一下application.yml里的端口号即可实现

/**
 * 广播消息
 * 消息类型:string
 * topic:syncTopic
 *
 * 注释:messageModel默认值为MessageModel.CLUSTERING,即为集群负载均衡消息
 * 将之改为MessageModel.BROADCASTING即可让所有集群的消费者都消费到这条消息
 *
 * @author gongzheng
 * @date 2020/5/21
 */
@Service
@RocketMQMessageListener(topic = "${demo.rocketmq.topic.syncTopic}", consumerGroup = "board_consumer",messageModel = MessageModel.BROADCASTING)
public class BoardMsgConsumer implements RocketMQListener<String> {
    @Override
    public void onMessage(String message) {
        System.out.printf("-------BoardMsgConsumer received: %s \n", message);
    }
}

高可靠保障

最终一致性的保障之一:事务消息(Transaction Message)[重要]

RocketMQ满足了AP(最终一致性)性致,事务消息的feature在这里是一个很大的体现

RocketMQ事务消息(Transactional Message)是指应用本地事务和发送消息操作可以被定义到全局事务中,要么同时成功,要么同时失败。

CAP介绍

在理论计算机科学中,CAP定理(CAP theorem),又被称作布鲁尔定理(Brewer's theorem),它指出对于一个分布式计算系统来说,不可能同时满足以下三点:

  • 一致性(Consistency) (等同于所有节点访问同一份最新的数据副本)
  • 可用性(Availability)(每次请求都能获取到非错的响应——但是不保证获取的数据为最新数据)
  • 分区容错性(Partition tolerance)(以实际效果而言,分区相当于对通信的时限要求。系统如果不能在时限内达成数据一致性,就意味着发生了分区的情况,必须就当前操作在C和A之间做出选择)

事务消息实现原理

1.事务消息在一阶段对用户不可见

在RocketMQ事务消息的主要流程中,一阶段的消息如何对用户不可见。其中,事务消息相对普通消息最大的特点就是一阶段发送的消息对用户是不可见的。那么,如何做到写入消息但是对用户不可见呢?RocketMQ事务消息的做法是:如果消息是half消息,将备份原消息的主题与消息消费队列,然后改变主题为RMQ_SYS_TRANS_HALF_TOPIC。由于消费组未订阅该主题,故消费端无法消费half类型的消息,然后RocketMQ会开启一个定时任务,从Topic为RMQ_SYS_TRANS_HALF_TOPIC中拉取消息进行消费,根据生产者组获取一个服务提供者发送回查事务状态请求,根据事务状态来决定是提交或回滚消息。

在RocketMQ中,消息在服务端的存储结构如下,每条消息都会有对应的索引信息,Consumer通过ConsumeQueue这个二级索引来读取消息实体内容,其流程如下:

RocketMQ的具体实现策略是:写入的如果事务消息,对消息的Topic和Queue等属性进行替换,同时将原来的Topic和Queue信息存储到消息的属性中,正因为消息主题被替换,故消息并不会转发到该原主题的消息消费队列,消费者无法感知消息的存在,不会消费。其实改变消息主题是RocketMQ的常用“套路”,回想一下延时消息的实现机制。

2.Commit和Rollback操作以及Op消息的引入

在完成一阶段写入一条对用户不可见的消息后,二阶段如果是Commit操作,则需要让消息对用户可见;如果是Rollback则需要撤销一阶段的消息。

对于Rollback,本身一阶段的消息对用户是不可见的,其实不需要真正撤销消息(实际上RocketMQ也无法去真正的删除一条消息,因为是顺序写文件的)。但是区别于这条消息没有确定状态(Pending状态,事务悬而未决),需要一个操作来标识这条消息的最终状态。RocketMQ事务消息方案中引入了Op消息的概念,用Op消息标识事务消息已经确定的状态(Commit或者Rollback)。如果一条事务消息没有对应的Op消息,说明这个事务的状态还无法确定(可能是二阶段失败了)。引入Op消息后,事务消息无论是Commit或者Rollback都会记录一个Op操作。Commit相对于Rollback只是在写入Op消息前创建Half消息的索引。

3.Op消息的存储和对应关系

RocketMQ将Op消息写入到全局一个特定的Topic中通过源码中的方法—TransactionalMessageUtil.buildOpTopic();这个Topic是一个内部的Topic(像Half消息的Topic一样),不会被用户消费。Op消息的内容为对应的Half消息的存储的Offset,这样通过Op消息能索引到Half消息进行后续的回查操作。

4.Half消息的索引构建

在执行二阶段Commit操作时,需要构建出Half消息的索引。一阶段的Half消息由于是写到一个特殊的Topic,所以二阶段构建索引时需要读取出Half消息,并将Topic和Queue替换成真正的目标的Topic和Queue,之后通过一次普通消息的写入操作来生成一条对用户可见的消息。所以RocketMQ事务消息二阶段其实是利用了一阶段存储的消息的内容,在二阶段时恢复出一条完整的普通消息,然后走一遍消息写入流程。

5.如何处理二阶段失败的消息?

如果在RocketMQ事务消息的二阶段过程中失败了,例如在做Commit操作时,出现网络问题导致Commit失败,那么需要通过一定的策略使这条消息最终被Commit。RocketMQ采用了一种补偿机制,称为“回查”。Broker端对未确定状态的消息发起回查,将消息发送到对应的Producer端(同一个Group的Producer),由Producer根据消息来检查本地事务的状态,进而执行Commit或者Rollback。Broker端通过对比Half消息和Op消息进行事务消息的回查并且推进CheckPoint(记录那些事务消息的状态是确定的)。

值得注意的是,rocketmq并不会无休止的的信息事务状态回查,默认回查15次,如果15次回查还是无法得知事务状态,rocketmq默认回滚该消息。

事务消息使用事项(自翻译,可能有出入)

  1. 事务消息不能延时发送,也不能批处理发送
  2. 如果事务消息的状态是UNKNOWN,RocketMQ会去检查事务消息,会检查15次,如果想要更改检查次数,可以通过修改Broker参数"transactionCheckBox"的值来改变,如果一条消息被检查次数超过transactionCheckBox的值,则这条消息会被丢掉,并打一条错误日志出来
  3. 一条事务消息被确定为UNKNOWN的检查开始时间为transactionTimeOut,默认为6000ms,每次检查的间隔为60000ms,参数为transactionCheckInterval=60000。这些参数我们都可以通过broker的配置来修改
  4. 一条事务消息或许会被检查/消费不止一次 (如果消费不止一次会怎样)
  5. 提交的消息放置到用户的topic可能会失败,事实上,它依赖于日志记录,高可用的保证是rocketMQ高可用的机制,如果我们要确保事务消息不会丢失以及事务完整性的保证,推荐使用同步复写(这里我个人的理解是,将RocketMQLocalTransactionListener接口的方法都实现,即一定要有检查机制)
  6. 事务消息不同于别的消息类型,事务消息允许在后台被查找,通过Producer的IDs,所以它的IDs不能被分享给其他类型的消息。
  7. 不支持延迟发送和批量发送

Usage Constraint

(1) Messages of the transactional have no schedule and batch support. (2) In order to avoid a single message being checked too many times and lead to half queue message accumulation, we limited the number of checks for a single message to 15 times by default, but users can change this limit by change the “transactionCheckMax” parameter in the configuration of the broker, if one message has been checked over “transactionCheckMax” times, broker will discard this message and print an error log at the same time by default. Users can change this behavior by override the “AbstractTransactionCheckListener” class. (3) A transactional message will be checked after a certain period of time that determined by parameter “transactionTimeout” in the configuration of the broker. And users also can change this limit by set user property “CHECK_IMMUNITY_TIME_IN_SECONDS” when sending transactional message, this parameter takes precedence over the “transactionMsgTimeout” parameter. (4) A transactional message maybe checked or consumed more than once. (5) Committed message reput to the user’s target topic may fail. Currently, it depends on the log record. High availability is ensured by the high availability mechanism of RocketMQ itself. If you want to ensure that the transactional message isn’t lost and the transaction integrity is guaranteed, it is recommended to use synchronous double write. mechanism. (6) Producer IDs of transactional messages cannot be shared with producer IDs of other types of messages. Unlike other types of message, transactional messages allow backward queries. MQ Server query clients by their Producer IDs.

在这个例子里 官方demo太过抽象,使用了简书上的一个demo

 /**
     * RocketMQ Transaction producer
     *
     * @author gongzheng
     * @date 2020/3/16
     */
    @RequestMapping("TransactionSend")
    public void TransactionSend() throws MessagingException {
        String[] tags = new String[] {"TagA", "TagB", "TagC", "TagD", "TagE"};
        for (int i = 0; i < 10; i++) {
            try {
                Message msg = MessageBuilder.withPayload("rocketMQTemplate transactional message " + i).
                        setHeader(RocketMQHeaders.TRANSACTION_ID, "KEY_" + i).build();
                TransactionSendResult sendResult = rocketMQTemplate.sendMessageInTransaction(
                        springTransTopic + ":" + tags[i % tags.length], msg,null);
//                System.out.printf("------rocketMQTemplate send Transactional msg body = %s , sendResult=%s %n",
//                        msg.getPayload(), sendResult.getSendStatus());
//                Thread.sleep(10);
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }
@Component
@Slf4j
@RocketMQTransactionListener(txProducerGroup = "producer_group_txmsg_bank1")
public class ProducerTxmsgListener implements RocketMQLocalTransactionListener 
  //在这里注入Service和Dao
    @Autowired
    AccountInfoService accountInfoService;
    @Autowired
    AccountInfoDao accountInfoDao;
    //消息发送成功回调此方法,此方法执行本地事务
    @Override
    @Transactional
    public RocketMQLocalTransactionState executeLocalTransaction(Message message, Object arg) {
    //解析消息内容 
    try {
        String jsonString = new String((byte[]) message.getPayload()); 
        JSONObject jsonObject = JSONObject.parseObject(jsonString); AccountChangeEvent accountChangeEvent =
        JSONObject.parseObject(jsonObject.getString("accountChange"), AccountChangeEvent.class); 
        //CRUD操作
        accountInfoService.doUpdateAccountBalance(accountChangeEvent);
        return RocketMQLocalTransactionState.COMMIT; 
      } catch (Exception e) {
        log.error("executeLocalTransaction 事务执行失败",e); e.printStackTrace();
        return RocketMQLocalTransactionState.ROLLBACK;
      } 
    }
    //此方法检查事务执行状态
    @Override
    public RocketMQLocalTransactionState checkLocalTransaction(Message message) {
        RocketMQLocalTransactionState state;
        final JSONObject jsonObject = JSON.parseObject(new String((byte[]) message.getPayload()));
        AccountChangeEvent accountChangeEvent =                                          
        JSONObject.parseObject(jsonObject.getString("accountChange"),AccountChangeEvent.class);
        //判断事务id是否存在,在数据库中的相关业务表增加字段事务编号,每次事务操作都会记录新的事务编号,根据事务编号判断上次事务是否成功
        String txNo = accountChangeEvent.getTxNo();
        int isexistTx = accountInfoDao.isExistTx(txNo);
        log.info("回查事务,事务号: {} 结果: {}", accountChangeEvent.getTxNo(),isexistTx); 
        if(isexistTx>0){
          state= RocketMQLocalTransactionState.COMMIT; 
        }else{
          state= RocketMQLocalTransactionState.UNKNOWN; 
        }
        return state;
      } 
}
/**
 * Dao层代码,主要是看检查机制中的isExistTx方法,通过查询是否有新增的记录判断上次事务执行是否成功
 */@Mapper
@Component
public interface AccountInfoDao {
    @Update("update account_info set account_balance=account_balance+#{amount} where account_no=# {accountNo}")
    int updateAccountBalance(@Param("accountNo") String accountNo, @Param("amount") Double amount);
    @Select("select count(1) from de_duplication where tx_no = #{txNo}") int isExistTx(String txNo);
    @Insert("insert into de_duplication values(#{txNo},now());") int addTx(String txNo);
}

原贴地址

最终一致性的保障之二:RPC消息(Request&Reply Message)[重要]

应用场景:跨网段

Request&Reply消息更类似于我们的接口访问,生产者像消费者发送消息,看起来就像一个request请求一样,消费者接收到消息后也会回发一个对象,类似于一个response

sync producer:

 /**
     * ReplyMessageSync Producer 
     *
     * @author gongzheng
     * @date 2020/3/17z
     */
    @RequestMapping("ReplyMessageSync")
    public void ReplyMessageSync() throws MessagingException {
        //reply type:String.class
        // Send request in sync mode and receive a reply of String type.
        String replyString = rocketMQTemplate.sendAndReceive(stringReplyTopic, "request string", String.class);
        System.out.printf("send %s and receive %s %n", "request string", replyString);
        //reply type:User.class,reply timeout:30000,delayLevel:2
        User requestUser = new User().setUserAge((byte) 9).setUserName("requestUserName");
        User replyUser = rocketMQTemplate.sendAndReceive(objectReplyTopic, requestUser, User.class, "order-id", 30000, 2);
        System.out.printf("send %s and receive %s %n", requestUser, replyUser);
    }

async producer:

 /**
     * ReplyMessageAsync
     *
     * @author gongzheng
     * @date 2020/3/17z
     */
    @RequestMapping("ReplyMessageAsync")
    public void ReplyMessageAsync() throws MessagingException {
        User requestUser = new User().setUserAge((byte) 9).setUserName("requestUserName");
        Message message=MessageBuilder.withPayload(requestUser).build();
        // Send request in async mode and receive a reply of String type.
        rocketMQTemplate.sendAndReceive(stringReplyTopic, "request string", new RocketMQLocalRequestCallback<String>() {
            @Override
            public void onSuccess(String message) {
                System.out.printf("send %s and receive %s %n", "request string", message);
            }
​
            @Override
            public void onException(Throwable e) {
                e.printStackTrace();
            }
        });
        // Send request in async mode and receive a reply of User type.
        rocketMQTemplate.sendAndReceive(objectReplyTopic, message, new RocketMQLocalRequestCallback<User>() {
            @Override
            public void onSuccess(User message) {
                System.out.printf("send user object and receive %s %n", message.toString());
            }
​
            @Override
            public void onException(Throwable e) {
                e.printStackTrace();
            }
        }, 300000, 2);
    }
/**
 * The msg consumer which replying spec object 
 */
@Service
@RocketMQMessageListener(topic = "msgTopicReplyUser", consumerGroup = "${demo.rocketmq.consumer.genericRequestConsumer}")
public class MsgConsumerReplyUser implements RocketMQReplyListener<MessageExt, User> {
​
    @Override
    public User onMessage(MessageExt message) {
        User user= JSON.parseObject(new String(message.getBody()),User.class);
        user.setUserName("张三");
        user.setUserAge((byte)13);
        return user;
    }
}
/**
 * The consumer which replying String
 */
@Service
@RocketMQMessageListener(topic = "${demo.rocketmq.topic.stringReplyTopic}", consumerGroup = "${demo.rocketmq.consumer.stringRequestConsumer}")
public class StringConsumerReplyString implements RocketMQReplyListener<String, String> {
​
    @Override
    public String onMessage(String message) {
        System.out.printf("------- StringConsumerWithReplyString received: %s \n", message);
        return "reply string";
    }
}