RocketMQ(2)---实现分布式事务原理

分布式事务(3)—RocketMQ实现分布式事务原理

之前讲过有关分布式事务2PC、3PC、TCC的理论知识,博客地址:

1、分布式事务(1)---2PC和3PC原理

2、分布式事务(2)---TCC原理

这篇讲有关RocketMQ实现分布式事务的理论知识,下篇也会示例 通过SpringCloud来实例RocketMQ实现分布式事务的项目。

一、举个分布式事务场景

列子:假设 A 给 B 转 100块钱,同时它们不是同一个服务上。

目标:就是 A 减100块钱,B 加100块钱。

实际情况可能有四种:

1)就是A账户减100 (成功),B账户加100 (成功)

2)就是A账户减100(失败),B账户加100 (失败)

3)就是A账户减100(成功),B账户加100 (失败)

4)就是A账户减100 (失败),B账户加100 (成功)

这里 第1和第2 种情况是能够保证事务的一致性的,但是 第3和第4 是无法保证事务的一致性的。

那我们来看下RocketMQ是如何来保证事务的一致性的。

 

二、RocketMQ实现分布式事务原理

RocketMQ虽然之前也支持分布式事务,但并没有开源,等到RocketMQ 4.3才正式开源。

1、基础概念

最终一致性

RocketMQ是一种最终一致性的分布式事务,就是说它保证的是消息最终一致性,而不是像2PC、3PC、TCC那样强一致分布式事务,至于为什么说它是最终一致性事务下面会详细说明。

Half Message(半消息)

是指暂不能被Consumer消费的消息。Producer 已经把消息成功发送到了 Broker 端,但此消息被标记为暂不能投递状态,处于该种状态下的消息称为半消息。需要 Producer

对消息的二次确认后,Consumer才能去消费它。

消息回查

由于网络闪段,生产者应用重启等原因。导致 Producer 端一直没有对 Half Message(半消息) 进行 二次确认。这是Brock服务器会定时扫描长期处于半消息的消息,会

主动询问 Producer端 该消息的最终状态(Commit或者Rollback),该消息即为 消息回查

2、分布式事务交互流程

理解这张阿里官方的图,就能理解RocketMQ分布式事务的原理了。

我们来说明下上面这张图

1、A服务先发送个Half Message给Brock端,消息中携带 B服务 即将要+100元的信息。

2、当A服务知道Half Message发送成功后,那么开始第3步执行本地事务。

3、执行本地事务(会有三种情况1、执行成功。2、执行失败。3、网络等原因导致没有响应)

4.1)、如果本地事务成功,那么Product像Brock服务器发送Commit,这样B服务就可以消费该message。

4.2)、如果本地事务失败,那么Product像Brock服务器发送Rollback,那么就会直接删除上面这条半消息。

4.3)、如果因为网络等原因迟迟没有返回失败还是成功,那么会执行RocketMQ的回调接口,来进行事务的回查。

从上面流程可以得知 只有A服务本地事务执行成功 ,B服务才能消费该message

然后我们再来思考几个问题?

为什么要先发送Half Message(半消息)

我觉得主要有两点

1)可以先确认 Brock服务器是否正常 ,如果半消息都发送失败了 那说明Brock挂了。

2)可以通过半消息来回查事务,如果半消息发送成功后一直没有被二次确认,那么就会回查事务状态。

什么情况会回查

也会有两种情况

1)执行本地事务的时候,由于突然网络等原因一直没有返回执行事务的结果(commit或者rollback)导致最终返回UNKNOW,那么就会回查。

2) 本地事务执行成功后,返回Commit进行消息二次确认的时候的服务挂了,在重启服务那么这个时候在brock端
   它还是个Half Message(半消息),这也会回查。

特别注意: 如果回查,那么一定要先查看当前事务的执行情况,再看是否需要重新执行本地事务。

想象下如果出现第二种情况而引起的回查,如果不先查看当前事务的执行情况,而是直接执行事务,那么就相当于成功执行了两个本地事务。

为什么说MQ是最终一致性事务

通过上面这幅图,我们可以看出,在上面举例事务不一致的两种情况中,永远不会发生

A账户减100 (失败),B账户加100 (成功)

因为:如果A服务本地事务都失败了,那B服务永远不会执行任何操作,因为消息压根就不会传到B服务。

那么 A账户减100 (成功),B账户加100 (失败) 会不会可能存在的。

答案是会的

因为A服务只负责当我消息执行成功了,保证消息能够送达到B,至于B服务接到消息后最终执行结果A并不管。

那B服务失败怎么办?

如果B最终执行失败,几乎可以断定就是代码有问题所以才引起的异常,因为消费端RocketMQ有重试机制,如果不是代码问题一般重试几次就能成功。

如果是代码的原因引起多次重试失败后,也没有关系,将该异常记录下来,由人工处理,人工兜底处理后,就可以让事务达到最终的一致性。

 

RocketMQ实现分布式事务

有关RocketMQ实现分布式事务前面写了一篇博客

1、RocketMQ实现分布式事务原理

下面就这个项目做个整体简单介绍,并在文字最下方附上项目Github地址。

一、项目概述

1、技术架构

项目总体技术选型

SpringCloud(Finchley.RELEASE) + SpringBoot2.0.4 + Maven3.5.4 + RocketMQ4.3 +MySQL + lombok(插件)

有关SpringCloud主要用到以下四个组建

Eureka Server +config-server(配置中心)+ Eureka Client + Feign(服务间调用) 

配置中心是用MySQL存储数据。

2、项目整体结构

config-service  # 配置中心
eureka          # 注册中心
service-order   #订单微服务
service-produce #商品微服务

各服务的启动顺序就安装上面的顺序启动。

大致流程

启动后,配置中心、订单微服务、商品微服务都会将信息注册到注册中心。

如果访问:localhost:7001(注册中心地址),以上服务都出现说明启动成功。

3、分布式服务流程

用户在订单微服务下单后,会去回调商品微服务去减库存。这个过程需要事务的一致性。

4、测试流程

页面输入:

http://localhost:9001/api/v1/order/save?userId=1&productId=1&total=4	

订单微服务执行情况(订单服务事务执行成功)

商品微服务执行情况(商品服务事务执行成功)

当然你也可以通过修改参数来模拟分布式事务出现的各种情况。

 

二、MQ中生产者核心代码

这里展示下,生产者发送消息核心代码。

@Slf4j
@Component
public class TransactionProducer {

    /**
     * 需要自定义事务监听器 用于 事务的二次确认 和 事务回查
     */
    private TransactionListener transactionListener ;
    /**
     * 这里的生产者和之前的不一样
     */
    private TransactionMQProducer producer = null;
    /**
     * 官方建议自定义线程 给线程取自定义名称 发现问题更好排查
     */
    private ExecutorService executorService = new ThreadPoolExecutor(2, 5, 100, TimeUnit.SECONDS,
            new ArrayBlockingQueue<Runnable>(2000), new ThreadFactory() {
        @Override
        public Thread newThread(Runnable r) {
            Thread thread = new Thread(r);
            thread.setName("client-transaction-msg-check-thread");
            return thread;
        }
    });

    public TransactionProducer(@Autowired Jms jms, @Autowired ProduceOrderService produceOrderService) {
        transactionListener = new TransactionListenerImpl(produceOrderService);
        // 初始化 事务生产者
        producer = new TransactionMQProducer(jms.getOrderTopic());
        // 添加服务器地址
        producer.setNamesrvAddr(jms.getNameServer());
        // 添加事务监听器
        producer.setTransactionListener(transactionListener);
        // 添加自定义线程池
        producer.setExecutorService(executorService);

        start();
    }

    public TransactionMQProducer getProducer() {
        return this.producer;
    }

    /**
     * 对象在使用之前必须要调用一次,只能初始化一次
     */
    public void start() {
        try {
            this.producer.start();
        } catch (MQClientException e) {
            e.printStackTrace();
        }
    }

    /**
     * 一般在应用上下文,使用上下文监听器,进行关闭
     */
    public void shutdown() {
        this.producer.shutdown();
    }
}

/**
 * @author xub
 * @Description: 自定义事务监听器
 * @date 2019/7/15 下午12:20
 */
@Slf4j
class TransactionListenerImpl implements TransactionListener {

    @Autowired
    private ProduceOrderService produceOrderService ;

    public TransactionListenerImpl( ProduceOrderService produceOrderService) {
        this.produceOrderService = produceOrderService;
    }

    @Override
    public LocalTransactionState executeLocalTransaction(Message msg, Object arg) {
        log.info("=========本地事务开始执行=============");
        String message = new String(msg.getBody());
        JSONObject jsonObject = JSONObject.parseObject(message);
        Integer productId = jsonObject.getInteger("productId");
        Integer total = jsonObject.getInteger("total");
        int userId = Integer.parseInt(arg.toString());
        //模拟执行本地事务begin=======
        /**
         * 本地事务执行会有三种可能
         * 1、commit 成功
         * 2、Rollback 失败
         * 3、网络等原因服务宕机收不到返回结果
         */
        log.info("本地事务执行参数,用户id={},商品ID={},销售库存={}",userId,productId,total);
        int result = produceOrderService.save(userId, productId, total);
        //模拟执行本地事务end========
        //TODO 实际开发下面不需要我们手动返回,而是根据本地事务执行结果自动返回
        //1、二次确认消息,然后消费者可以消费
        if (result == 0) {
            return LocalTransactionState.COMMIT_MESSAGE;
        }
        //2、回滚消息,Broker端会删除半消息
        if (result == 1) {
            return LocalTransactionState.ROLLBACK_MESSAGE;
        }
        //3、Broker端会进行回查消息
        if (result == 2) {
            return LocalTransactionState.UNKNOW;
        }
        return LocalTransactionState.COMMIT_MESSAGE;
    }

    /**
     * 只有上面接口返回 LocalTransactionState.UNKNOW 才会调用查接口被调用
     *
     * @param msg 消息
     * @return
     */
    @Override
    public LocalTransactionState checkLocalTransaction(MessageExt msg) {
        log.info("==========回查接口=========");
        String key = msg.getKeys();
        //TODO 1、必须根据key先去检查本地事务消息是否完成。
        /**
         * 因为有种情况就是:上面本地事务执行成功了,但是return LocalTransactionState.COMMIT_MESSAG的时候
         * 服务挂了,那么最终 Brock还未收到消息的二次确定,还是个半消息 ,所以当重新启动的时候还是回调这个回调接口。
         * 如果不先查询上面本地事务的执行情况 直接在执行本地事务,那么就相当于成功执行了两次本地事务了。
         */
        // TODO 2、这里返回要么commit 要么rollback。没有必要在返回 UNKNOW
        return LocalTransactionState.COMMIT_MESSAGE;
    }
}

 

三、MQ消费端核心代码

这里展示下,消费端消费消息核心代码。消费端和普通消费一样。

@Slf4j
@Component
public class OrderConsumer {
    private DefaultMQPushConsumer consumer;
    private String consumerGroup = "produce_consumer_group";

    public OrderConsumer(@Autowired Jms jms,@Autowired ProduceService produceService) throws MQClientException {
        //设置消费组
        consumer = new DefaultMQPushConsumer(consumerGroup);
        // 添加服务器地址
        consumer.setNamesrvAddr(jms.getNameServer());
        // 添加订阅号
        consumer.subscribe(jms.getOrderTopic(), "*");
        // 监听消息
        consumer.registerMessageListener((MessageListenerConcurrently) (msgs, context) -> {
            MessageExt msg = msgs.get(0);
            String message = new String(msgs.get(0).getBody());
            JSONObject jsonObject = JSONObject.parseObject(message);
            Integer productId = jsonObject.getInteger("productId");
            Integer total = jsonObject.getInteger("total");
            String key = msg.getKeys();
            log.info("消费端消费消息,商品ID={},销售数量={}",productId,total);
            try {
                produceService.updateStore(productId, total, key);
                return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
            } catch (Exception e) {
                log.info("消费失败,进行重试,重试到一定次数 那么将该条记录记录到数据库中,进行如果处理");
                e.printStackTrace();
                return ConsumeConcurrentlyStatus.RECONSUME_LATER;
            }
        });
        consumer.start();
        System.out.println("consumer start ...");
    }
}

至于完整的项目地址见GitHub。

 

发送普通消息(三种方式)

RocketMQ 发送普通消息有三种实现方式:可靠同步发送可靠异步发送单向(Oneway)发送

注意 :顺序消息只支持可靠同步发送

GitHub地址https://github.com/yudiandemingzi/SpringBootBlog

一、概念

1、可靠同步发送

原理:同步发送是指消息发送方发出数据后,会在收到接收方发回响应之后才发下一个数据包的通讯方式。

应用场景:此种方式应用场景非常广泛,例如重要通知邮件、报名短信通知、营销短信系统等。

2、可靠异步发送

原理:异步发送是指发送方发出数据后,不等接收方发回响应,接着发送下个数据包的通讯方式。 消息队列 RocketMQ 的异步发送,需要用户实现异步发送回调接口(SendCallback)。

应用场景:异步发送一般用于链路耗时较长,对 RT 响应时间较为敏感的业务场景,例如批量发货等操作。

3、单向(Oneway)发送

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

应用场景:适用于某些耗时非常短,但对可靠性要求并不高的场景,例如日志收集。

4、三种对比

下表概括了三者的特点和主要区别。

发送方式发送 TPS发送结果反馈可靠性
同步发送 不丢失
异步发送 不丢失
单向发送 最快 可能丢失

 

二、代码示例

1、三种方式代码示例

@Slf4j
@RestController
public class Controller {
    /**
     * 生产者组
     */
    private static String PRODUCE_RGROUP = "test_producer";
    /**
     * 创建生产者对象
     */
    private static DefaultMQProducer producer = null;

    static {
        producer = new DefaultMQProducer(PRODUCE_RGROUP);
        //不开启vip通道 开通口端口会减2
        producer.setVipChannelEnabled(false);
        //绑定name server
        producer.setNamesrvAddr("47.99.03.25:9876");
        try {
            producer.start();
        } catch (MQClientException e) {
            e.printStackTrace();
        }

    }

    @GetMapping("/message")
    public  void  message() throws Exception {
        //1、同步
        sync();
        //2、异步
        async();
        //3、单项发送
        oneWay();
    }
    /**
     * 1、同步发送消息
     */
    private  void sync() throws Exception {
        //创建消息
        Message message = new Message("topic_family", ("  同步发送  ").getBytes());
        //同步发送消息
        SendResult sendResult = producer.send(message);
        log.info("Product-同步发送-Product信息={}", sendResult);
    }
    /**
     * 2、异步发送消息
     */
    private  void async() throws Exception {
        //创建消息
        Message message = new Message("topic_family", ("  异步发送  ").getBytes());
        //异步发送消息
        producer.send(message, new SendCallback() {
            @Override
            public void onSuccess(SendResult sendResult) {
                log.info("Product-异步发送-输出信息={}", sendResult);
            }
            @Override
            public void onException(Throwable e) {
                e.printStackTrace();
                //补偿机制,根据业务情况进行使用,看是否进行重试
            }
        });
    }
    /**
     * 3、单项发送消息
     */
    private  void oneWay() throws Exception {
        //创建消息
        Message message = new Message("topic_family", (" 单项发送 ").getBytes());
        //同步发送消息
        producer.sendOneway(message);
    }
}

2、测试结果

这里消费者代码就不贴出来了。

通过这个很明显可以看出三种方式都被 Consumer 消费了。只不过对于 Product 同步和异步发送是有返回信息的,单项发送是没有返回信息的。

 

三、SendStatus状态

当Product发送消息的时候,会返回SendResult对象,该对象又包含了一个SendStatus对象。

package org.apache.rocketmq.client.producer;
public enum SendStatus {
    SEND_OK,
    FLUSH_DISK_TIMEOUT,
    FLUSH_SLAVE_TIMEOUT,
    SLAVE_NOT_AVAILABLE,
}

下面对这几种状态进行说明

SEND_OK

代表发送成功!但并不保证它是可靠的。要确保不会丢失任何消息,还应启用SYNC_MASTER或SYNC_FLUSH。

SLAVE_NOT_AVAILABLE

如果Broker的角色是SYNC_MASTER(同步复制)(默认为异步),但没有配置Slave Broker,将获得此状态。

FLUSH_DISK_TIMEOUT

如果Broker设置为 SYNC_FLUSH(同步刷盘)(默认为ASYNC_FLUSH),并且Broker的syncFlushTimeout(默认为5秒)内完成刷新磁盘,将获得此状态。

FLUSH_SLAVE_TIMEOUT

如果Broker的角色是SYNC_MASTER(同步复制)(默认为ASYNC_MASTER),并且从属Broker的syncFlushTimeout(默认为5秒)内完成与主服务器的同步,将获得此状态。

RocketMQ顺序消费

如果要保证顺序消费,那么他的核心点就是:生产者有序存储消费者有序消费

一、概念

1、什么是无序消息

无序消息 无序消息也指普通的消息,Producer 只管发送消息,Consumer 只管接收消息,至于消息和消息之间的顺序并没有保证。

举例 Producer 依次发送 orderId 为 1、2、3 的消息,Consumer 接到的消息顺序有可能是 1、2、3,也有可能是 2、1、3 等情况,这就是普通消息。

2、什么是全局顺序

对于指定的一个 Topic,所有消息按照严格的先入先出(FIFO)的顺序进行发布和消费

举例 比如 Producer 发送orderId 1,3,2 的消息, 那么 Consumer 也必须要按照 1,3,2 的顺序进行消费。

3、局部顺序

在实际开发有些场景中,我并不需要消息完全按照完全按的先进先出,而是某些消息保证先进先出就可以了。

就好比一个订单涉及 订单生成订单支付订单完成。我不用管其它的订单,只保证同样订单ID能保证这个顺序就可以了。

 

二、实现原理

我们知道 生产的message最终会存放在Queue中,如果一个Topic关联了16个Queue,如果我们不指定消息往哪个队列里放,那么默认是平均分配消息到16个queue,

好比有100条消息,那么这100条消息会平均分配在这16个Queue上,那么每个Queue大概放5~6个左右。这里有一点很重的是:

同一个queue,存储在里面的message 是按照先进先出的原则

这个时候思路就来了,好比有orderId=1的3条消息,分别是 订单生产订单付款订单完成。只要保证它们放到同一个Queue那就保证消费者先进先出了。

这就保证局部顺序了,即同一订单按照先后顺序放到同一Queue,那么取消息的时候就可以保证先进先取出。

那么全局消息呢?

这个就简单啦,你把所有消息都放在一个Queue里,这样不就保证全局消息了。

就这么简单

当然不是,这里还有很关键的一点,好比在一个消费者集群的情况下,消费者1先去Queue拿消息,它拿到了 订单生成,它拿完后,消费者2去queue拿到的是 订单支付

拿的顺序是没毛病了,但关键是先拿到不代表先消费完它。会存在虽然你消费者1先拿到订单生成,但由于网络等原因,消费者2比你真正的先消费消息。这是不是很尴尬了。

订单付款还是可能会比订单生成更早消费的情况。那怎么办。

分布式锁来了

Rocker采用的是分段锁,它不是锁整个Broker而是锁里面的单个Queue,因为只要锁单个Queue就可以保证局部顺序消费了。

所以最终的消费者这边的逻辑就是

消费者1去Queue拿 订单生成,它就锁住了整个Queue,只有它消费完成并返回成功后,这个锁才会释放。

然后下一个消费者去拿到 订单支付 同样锁住当前Queue,这样的一个过程来真正保证对同一个Queue能够真正意义上的顺序消费,而不仅仅是顺序取出。

全局顺序与分区顺序对比

消息类型对比

发送方式对比

其它的注意事项

1、顺序消息暂不支持广播模式。
2、顺序消息不支持异步发送方式,否则将无法严格保证顺序。
3、建议同一个 Group ID 只对应一种类型的 Topic,即不同时用于顺序消息和无序消息的收发。
4、对于全局顺序消息,建议创建实例个数 >=2。

 

一、RocketMq有3中消息类型

1.普通消费

2. 顺序消费

3.事务消费

  • 顺序消费场景

在网购的时候,我们需要下单,那么下单需要假如有三个顺序,第一、创建订单 ,第二:订单付款,第三:订单完成。也就是这个三个环节要有顺序,这个订单才有意义。RocketMQ可以保证顺序消费。

  • rocketMq实现顺序消费的原理

 produce在发送消息的时候,把消息发到同一个队列(queue)中,消费者注册消息监听器为MessageListenerOrderly,这样就可以保证消费端只有一个线程去消费消息

注意:是把把消息发到同一个队列(queue),不是同一个topic,默认情况下一个topic包括4个queue

单个节点(Producer端1个、Consumer端1个)

三、代码示例

这里保证两点

1、生产端 同一orderID的订单放到同一个queue。

2、消费端 同一个queue取出消息的时候锁住整个queue,直到消费后再解锁。

1、ProductOrder实体

@AllArgsConstructor
@Data
@ToString
public class ProductOrder {
    /**
     * 订单编号
     */
    private String orderId;

    /**
     * 订单类型(订单创建、订单付款、订单完成)
     */
    private String type;
}

2、Product(生产者)

生产者和之前发送普通消息最大的区别,就是针对每一个message都手动通过MessageQueueSelector选择好queue。

@RestController
public class Product {
    private static List<ProductOrder> orderList = null;
    private static String producerGroup = "test_producer";
    /**
     * 模拟数据
     */
    static {
        orderList = new ArrayList<>();
        orderList.add(new ProductOrder("XXX001", "订单创建"));
        orderList.add(new ProductOrder("XXX001", "订单付款"));
        orderList.add(new ProductOrder("XXX001", "订单完成"));
        orderList.add(new ProductOrder("XXX002", "订单创建"));
        orderList.add(new ProductOrder("XXX002", "订单付款"));
        orderList.add(new ProductOrder("XXX002", "订单完成"));
        orderList.add(new ProductOrder("XXX003", "订单创建"));
        orderList.add(new ProductOrder("XXX003", "订单付款"));
        orderList.add(new ProductOrder("XXX003", "订单完成"));
    }

    @GetMapping("message")
    public  void sendMessage() throws Exception {
        //示例生产者
        DefaultMQProducer producer = new DefaultMQProducer(producerGroup);
        //不开启vip通道 开通口端口会减2
        producer.setVipChannelEnabled(false);
        //绑定name server
        producer.setNamesrvAddr("IP:9876");
        producer.start();
        for (ProductOrder order : orderList) {
            //1、生成消息
            Message message = new Message(JmsConfig.TOPIC, "", order.getOrderId(), order.toString().getBytes());
            //2、发送消息是 针对每条消息选择对应的队列
            SendResult sendResult = producer.send(message, new MessageQueueSelector() {
                @Override
                public MessageQueue select(List<MessageQueue> mqs, Message msg, Object arg) {
                    //3、arg的值其实就是下面传入 orderId
                    String orderid = (String) arg;
                    //4、因为订单是String类型,所以通过hashCode转成int类型
                    int hashCode = orderid.hashCode();
                    //5、因为hashCode可能为负数 所以取绝对值
                    hashCode = Math.abs(hashCode);
                    //6、保证同一个订单号 一定分配在同一个queue上
                    long index = hashCode % mqs.size();
                    return mqs.get((int) index);
                }
            }, order.getOrderId(),50000);

            System.out.printf("Product:发送状态=%s, 存储queue=%s ,orderid=%s, type=%s\n", sendResult.getSendStatus(), 
                                      sendResult.getMessageQueue().getQueueId(), order.getOrderId(), order.getType());
        }
        producer.shutdown();
    }
}

看看生产者有没有把相同订单指定到同一个queue

通过测试结果可以看出:相同订单已经存到同一queue中了

3、Consumer(消费者)

上面说过,消费者真正要达到消费顺序,需要分布式锁,所以这里需要将MessageListenerOrderly替换之前的MessageListenerConcurrently,因为它里面实现了分布式锁。

@Slf4j
@Component
public class Consumer {
    
    /**
     * 消费者实体对象
     */
    private DefaultMQPushConsumer consumer;
    /**
     * 消费者组
     */
    public static final String CONSUMER_GROUP = "consumer_group";
    /**
     * 通过构造函数 实例化对象
     */
    public Consumer() throws MQClientException {
        consumer = new DefaultMQPushConsumer(CONSUMER_GROUP);
        consumer.setNamesrvAddr("IP:9876");
        //TODO 这里真的是个坑,我product设置VipChannelEnabled(false),但消费者并没有设置这个参数,之前发送普通消息的时候也没有问题。能正常消费。
        //TODO 但在顺序消息时,consumer一直不消费消息了,找了好久都没有找到原因,直到我这里也设置为VipChannelEnabled(false),竟然才可以消费消息。
        consumer.setVipChannelEnabled(false);
        //订阅主题和 标签( * 代表所有标签)下信息
        consumer.subscribe(JmsConfig.TOPIC, "*");
            //注册消费的监听 这里注意顺序消费为MessageListenerOrderly 之前并发为ConsumeConcurrentlyContext
        consumer.registerMessageListener((MessageListenerOrderly) (msgs, context) -> {
            //获取消息
            MessageExt msg = msgs.get(0);
            //消费者获取消息 这里只输出 不做后面逻辑处理
            log.info("Consumer-线程名称={},消息={}", Thread.currentThread().getName(), new String(msg.getBody()));
            return ConsumeOrderlyStatus.SUCCESS;
        });
        consumer.start();
    }
}

看看消费结果是不是我们需要的结果

通过测试结果我们看出

1、消费消息的顺序并没有完全按照之前的先进先出,即没有满足全局顺序。
2、同一订单来讲,订单的 订单生成、订单支付、订单完成 消费顺序是保证的。

这是局部保证顺序消费就已经满足我们当前实际开发中的需求了。

 

posted @ 2022-02-21 22:30  hanease  阅读(237)  评论(0编辑  收藏  举报