SpringBoot整合RocketMq

465 阅读5分钟

SpringBoot整合RocketMq

简单在SpringBoot整合一下整合RocketMq

版本 JDK11
RocketMq 4.7.1
SpringBoot 2.0.3

添加依赖

<properties>
        <java.version>11</java.version>
        <rocketmq-spring-boot-starter-version>2.0.4</rocketmq-spring-boot-starter-version>
    </properties>
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>

        <dependency>
            <groupId>org.apache.rocketmq</groupId>
            <artifactId>rocketmq-spring-boot-starter</artifactId>
            <version>${rocketmq-spring-boot-starter-version}</version>
        </dependency>

        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <version>1.18.6</version>
        </dependency>

    </dependencies>

配置文件

# application.properties
rocketmq.name-server=localhost:9876
rocketmq.producer.group=my-group

启动类

@SpringBootApplication
public class DemoApplication {

    public static void main(String[] args) {
        SpringApplication.run(DemoApplication.class, args);
    }

}

生产者

@RestController
public class produce {


    @Autowired
    private RocketMQTemplate rocketMQTemplate;

    @RequestMapping("produce")
    public void test1() {
        rocketMQTemplate.convertAndSend("test-mq", "我是消息 哈哈哈哈哈 rocketmq");
    }
}

推消息消费
消息监听器


@Component
@RocketMQMessageListener(topic = "test-mq", consumerGroup = "springboot-mq-consumer-1")
public class Consume implements RocketMQListener<String> {

    @Override
    public void onMessage(String message) {
        System.out.println(("Receive message:" + message));
    }
}
发送消息
 String springTopic="TestTopic";
        //发送字符消息
        SendResult sendResult = rocketMQTemplate.syncSend(springTopic, "Hello, World!");
        System.out.printf("发送字符消息 syncSend1 to topic %s sendResult=%s %n", springTopic, sendResult);

        sendResult = rocketMQTemplate.syncSend(springTopic, new User().setUserAge((byte) 18).setUserName("Kitty"));
        System.out.printf("发送字符消息 syncSend1 to topic %s sendResult=%s %n", springTopic, sendResult);

        sendResult = rocketMQTemplate.syncSend(springTopic, MessageBuilder.withPayload(
                new User().setUserAge((byte) 21).setUserName("Lester")).setHeader(MessageHeaders.CONTENT_TYPE, MimeTypeUtils.APPLICATION_JSON_VALUE).build());
        System.out.printf("发送字符消息 syncSend1 to topic %s sendResult=%s %n", springTopic, sendResult);

        //发送对象消息
        rocketMQTemplate.asyncSend(springTopic, new OrderPaidEvent("T_001", new BigDecimal("88.00")), new SendCallback() {
            @Override
            public void onSuccess(SendResult var1) {
                System.out.printf("async onSucess SendResult=%s %n", var1);
            }

            @Override
            public void onException(Throwable var1) {
                System.out.printf("async onException Throwable=%s %n", var1);
            }
        });

        //发送指定TAG的消息
        rocketMQTemplate.convertAndSend(springTopic + ":tag0", "I'm from tag0");  // tag0 will not be consumer-selected
        System.out.printf("发送指定TAG的消息 syncSend topic %s tag %s %n", springTopic, "tag0");

        rocketMQTemplate.convertAndSend(springTopic + ":tag1", "I'm from tag1");
        System.out.printf("发送指定TAG的消息 syncSend topic %s tag %s %n", springTopic, "tag1");

        //同步发送消息并且返回一个String类型的结果。
        String replyString = rocketMQTemplate.sendAndReceive(springTopic, "request string", String.class);
        System.out.printf("同步发送消息并且返回一个String类型的结果。 send %s and receive %s %n", "request string", replyString);

        //同步发送消息并且返回一个Byte数组类型的结果。
        rocketMQTemplate.send(springTopic,MessageBuilder.withPayload("request byte[]").build());
        //System.out.printf("send %s and receive %s %n", "request byte[]", new String(replyBytes));

        //同步发送一个带hash参数的请求(排序消息),并返回一个User类型的结果
        User requestUser = new User().setUserAge((byte) 91).setUserName("requestUserName+创建");
        User requestUser2 = new User().setUserAge((byte) 92).setUserName("requestUserName+支付");
        User requestUser3 = new User().setUserAge((byte) 93).setUserName("requestUserName+检票");
        User requestUser4 = new User().setUserAge((byte) 94).setUserName("requestUserName+完成");
        
        User replyUser = rocketMQTemplate.sendAndReceive(springTopic, requestUser, User.class, "order-id");
        User replyUser2 = rocketMQTemplate.sendAndReceive(springTopic, requestUser2, User.class, "order-id");
        User replyUser3 = rocketMQTemplate.sendAndReceive(springTopic, requestUser3, User.class, "order-id");
        User replyUser4 = rocketMQTemplate.sendAndReceive(springTopic, requestUser4, User.class, "order-id");
        System.out.printf("同步发送一个带hash参数的请求(排序消息) send %s and receive %s %n", requestUser, replyUser);
        
        //同步发送一个带延迟级别的消息(延迟消息),并返回一个泛型结果
        ProductWithPayload<String> replyGenericObject = rocketMQTemplate.sendAndReceive(springTopic, "request generic",
                new TypeReference<ProductWithPayload<String>>() {
                }.getType(), 40000, 2);
        System.out.printf("/同步发送一个带延迟级别的消息 send %s and receive %s %n", "request generic", replyGenericObject);

        
        //异步发送消息,返回String类型结果。
        rocketMQTemplate.sendAndReceive(springTopic, "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();
            }
        });
        
        
        //异步发送消息,并返回一个User类型的结果。
        rocketMQTemplate.sendAndReceive(springTopic, new User().setUserAge((byte) 9).setUserName("requestUserName"), 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();
            }
        }, 50000);
        
        //发送批量消息
        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, "KEY_" + i).build());
        }

        SendResult sr = rocketMQTemplate.syncSend(springTopic, msgs, 60000);

        System.out.printf("--- 发送批量消息 send result :" + sr);
顺序消费

与原生 API 不同的是这里 ,会根据他的hash值计算发送到哪一个队列,用的是同一个值order,那么他们的hash一样就可以保证发送到同一个队列里

同步顺序发送

       String springTopic = "TestTopic";
        for (int j = 0; j < 5; j++) {
            String orderNo = "orderNo-" + j;
            //同步发送一个带hash参数的请求(排序消息),并返回一个User类型的结果
            User requestUser1 = new User().setUserAge((byte) 1).setUserName(orderNo + " 创建" + j);
            User requestUser2 = new User().setUserAge((byte) 2).setUserName(orderNo + " 支付" + j);
            User requestUser3 = new User().setUserAge((byte) 3).setUserName(orderNo + " 检票" + j);
            User requestUser4 = new User().setUserAge((byte) 4).setUserName(orderNo + " 完成" + j);
            List<User> users = Arrays.asList(requestUser1, requestUser2, requestUser3, requestUser4);
            for (int i = 0; i < 4; i++) {
                //同步发 一个一个发
                SendResult sendResult = rocketMQTemplate.syncSendOrderly(springTopic, users.get(i), "order-id");
            }
        }

异步顺序发送

         String springTopic = "TestTopic";
        for (int j = 0; j < 5; j++) {
            String orderNo = "orderNo-" + j;
            //同步发送一个带hash参数的请求(排序消息),并返回一个User类型的结果
            User requestUser1 = new User().setUserAge((byte) 1).setUserName(orderNo + " 创建" + j);
            User requestUser2 = new User().setUserAge((byte) 2).setUserName(orderNo + " 支付" + j);
            User requestUser3 = new User().setUserAge((byte) 3).setUserName(orderNo + " 检票" + j);
            User requestUser4 = new User().setUserAge((byte) 4).setUserName(orderNo + " 完成" + j);
            List<User> users = Arrays.asList(requestUser1, requestUser2, requestUser3, requestUser4);
            for (int i = 0; i < 4; i++) {
                //异步发
                int finalI = i;
                int finalI1 = i;
                //  异步发  发送的先后不保证 但消费那边还是按照顺序消费的,只不过异步发送不保证代码逻辑的顺序.
                rocketMQTemplate.asyncSendOrderly(springTopic, users.get(i), orderNo, new SendCallback() {
                    @Override
                    public void onSuccess(SendResult sendResult) {
                        System.out.println(" " + users.get(finalI).getUserName() + " --- > " + finalI1);
                        System.out.println("onSuccess:" + sendResult);
                    }

                    @Override
                    public void onException(Throwable throwable) {
                        System.out.println(throwable);
                    }
                });

            }
        }

而消费端一样需要使用顺序消费

默认是并发消费

@Component
@RocketMQMessageListener(consumerGroup = "MyConsumerGroup", topic = "TestTopic",consumeMode= ConsumeMode.CONCURRENTLY) //并发消费
public class SpringConsumer implements RocketMQListener<String> {
    @Override
    public void onMessage(String message) {
        System.out.println("Received message : "+ message);
    }
}

这里我们需要顺序消费

@Component
@RocketMQMessageListener(consumerGroup = "MyConsumerGroup", topic = "TestTopic",consumeMode= ConsumeMode.ORDERLY) // 顺序消费
public class SpringOrderLyConsumer implements RocketMQListener<String> {
    @Override
    public void onMessage(String message) {
        System.out.println("Received message : "+ message);
    }
}
事务

事务消息是在分布式系统中保证最终一致性的两阶段提交的消息实现。他可以保证本地事务执行与消息发送两个操作的原子性,也就是这两个操作一起成功或者一起失败。

  • 1、事务消息不支持延迟消息和批量消息。
  • 2、为了避免单个消息被检查太多次而导致半队列消息累积,我们默认将单个消息的检查次数限制为 15 次,但是用户可以通过 Broker 配置文件的transactionCheckMax参数来修改此限制。如果已经检查某条消息超过 N 次的话( N = transactionCheckMax ) 则 Broker 将丢弃此消息,并在默认情况下同时打印错误日志。用户可以通过重写 AbstractTransactionCheckListener 类来修改这个行为。
  • 3、事务消息将在 Broker 配置文件中的参数 transactionMsgTimeout 这样的特定时间长度之后被检查。当发送事务消息时,用户还可以通过设置用户属性CHECK_IMMUNITY_TIME_IN_SECONDS 来改变这个限制,该参数优先于transactionMsgTimeout 参数。
  • 4、事务性消息可能不止一次被检查或消费。
  • 5、提交给用户的目标主题消息可能会失败,目前这依日志的记录而定。它的高可用性通过 RocketMQ 本身的高可用性机制来保证,如果希望确保事务消息不丢失、并且事务完整性得到保证,建议使用同步的双重写入机制。
  • 6、事务消息的生产者 ID 不能与其他类型消息的生产者 ID 共享。与其他类型的消息不同,事务消息允许反向查询、MQ服务器能通过它们的生产者 ID 查询到消费者。

实现机制

在这里插入图片描述

事务消息机制的关键是在发送消息时,会将消息转为一个half半消息,并存入RocketMQ内部的一个 RMQ_SYS_TRANS_HALF_TOPIC 这个Topic,这样对消费者是不可见的。再经过一系列事务检查通过后,再将消息转存到目标Topic,这样对消费者就可见了。

发送消息


    @Resource
    private RocketMQTemplate rocketMQTemplate;
    
    public void sendMessageInTransaction(String topic,String msg) throws InterruptedException {
        String[] tags = new String[] {"TagA", "TagB", "TagC", "TagD", "TagE"};
        for (int i = 0; i < 10; i++) {
            //尝试在Header中加入一些自定义的属性。
            Message<String> message = MessageBuilder.withPayload(msg)
                    .setHeader(RocketMQHeaders.TRANSACTION_ID,"TransID_"+i)
                    //发到事务监听器里后,这个自己设定的TAGS属性会丢失。但是上面那个属性不会丢失。
                    .setHeader(RocketMQHeaders.TAGS,tags[i % tags.length])
                    //MyProp在事务监听器里也能拿到,为什么就单单这个RocketMQHeaders.TAGS拿不到?这只能去调源码了。
                    .setHeader("MyProp","MyProp_"+i)
                    .build();
            String destination =topic+":"+tags[i % tags.length];
            //这里发送事务消息时,还是会转换成RocketMQ的Message对象,再调用RocketMQ的API完成事务消息机制。
            SendResult sendResult = rocketMQTemplate.sendMessageInTransaction(destination, message,destination);
            System.out.printf("%s%n", sendResult);

            Thread.sleep(10);
        }
    }

执行本地事务


//@RocketMQTransactionListener(txProducerGroup = "springBootGroup2")
@RocketMQTransactionListener(rocketMQTemplateBeanName = "rocketMQTemplate")
public class MyTransactionImpl implements RocketMQLocalTransactionListener {

    private ConcurrentHashMap<Object, Message> localTrans = new ConcurrentHashMap<>();
    @Override
    public RocketMQLocalTransactionState executeLocalTransaction(Message msg, Object arg) {
        Object transId = msg.getHeaders().get(RocketMQHeaders.PREFIX+RocketMQHeaders.TRANSACTION_ID);
        String destination = arg.toString();
        localTrans.put(transId,msg);
        //这个msg的实现类是GenericMessage,里面实现了toString方法
        //在Header中自定义的RocketMQHeaders.TAGS属性,到这里就没了。但是RocketMQHeaders.TRANSACTION_ID这个属性就还在。
        //而message的Header里面会默认保存RocketMQHeaders里的属性,但是都会加上一个RocketMQHeaders.PREFIX前缀
        System.out.println("executeLocalTransaction msg = "+msg);
        //转成RocketMQ的Message对象
        org.apache.rocketmq.common.message.Message message = RocketMQUtil.convertToRocketMessage(new StringMessageConverter(),"UTF-8",destination, msg);
        String tags = message.getTags();
        //如果是 TagA 则提交
        if(StringUtils.contains(tags,"TagA")){
            return RocketMQLocalTransactionState.COMMIT;
            //回滚
        }else if(StringUtils.contains(tags,"TagB")){
            return RocketMQLocalTransactionState.ROLLBACK;
        }else{
            //其他的则继续broken 继续反查(下面 checkLocalTransaction )  直到超过 15 次 或者设置的次数后丢弃
            return RocketMQLocalTransactionState.UNKNOWN;
        }
    }

    //延迟检查的时间间隔要有点奇怪。
    @Override
    public RocketMQLocalTransactionState checkLocalTransaction(Message msg) {
        String transId = msg.getHeaders().get(RocketMQHeaders.PREFIX+RocketMQHeaders.TRANSACTION_ID).toString();
        Message originalMessage = localTrans.get(transId);
        //这里能够获取到自定义的transaction_id属性
        System.out.println("checkLocalTransaction msg = "+originalMessage);
        //获取标签时,自定义的RocketMQHeaders.TAGS拿不到,但是框架会封装成一个带RocketMQHeaders.PREFIX的属性
//        String tags = msg.getHeaders().get(RocketMQHeaders.TAGS).toString();
        String tags = msg.getHeaders().get(RocketMQHeaders.PREFIX+RocketMQHeaders.TAGS).toString();
        if(StringUtils.contains(tags,"TagC")){
            return RocketMQLocalTransactionState.COMMIT;
        }else if(StringUtils.contains(tags,"TagD")){
            return RocketMQLocalTransactionState.ROLLBACK;
        }else{
            return RocketMQLocalTransactionState.UNKNOWN;
        }
    }
}

消费消息

@Component
@RocketMQMessageListener(consumerGroup = "MyConsumerGroup", topic = "TestTopic",consumeMode= ConsumeMode.CONCURRENTLY) //并发消费
public class SpringConsumer implements RocketMQListener<String> {
    @Override
    public void onMessage(String message) {
        System.out.println("Received message : "+ message);
    }
}

关于@RocketMQTransactionListener 这个注解,有点奇怪。
2.0.4版本中,是需要指定txProducerGroup指向一个消息发送者组。不同的组可以有不同的事务消息逻辑。
但是到了2.1.1版本,只能指定rocketMQTemplateBeanMame,也就是说如果你有多个发送者组需要有不同的事务消息逻辑,那就需要定义多个RocketMQTemplate。

@ExtRocketMQTemplateConfiguration()
public class ExtRocketMQTemplate extends RocketMQTemplate {
}