RocketMQ幂等性顺序性实战

1. rocketmq源码安装
参考官方文档:http://rocketmq.apache.org/docs/quick-start/
安装好jdk和maven rocketmq安装包:https://pan.baidu.com/s/1I3CqWaxFnxtUX1kJpIJkcQ 密码: vu5m

代码:https://github.com/2466845324/repository/tree/master/rocketmq

1. 解压源码包 unzip rocketmq-all-4.4.0-source-release.zip
2. 重命名并进目录 mv rocketmq-all-4.4.0 rocketmq && cd rocketmq
3. 编译打包(会下载很多依赖,大概10分钟) mvn -Prelease-all -DskipTests clean install -U
4. 启动nameserver服务 cd distribution/target/apache-rocketmq/bin && sh mqnamesrv
      * 默认配置是4g, 如果你的服务器配置较低则会报错, 则要修改
      vim runbroker.sh 和 vim runserver.sh 这两个文件,具体参数自己设定。
      例如配置:JAVA_OPTAVA_OPT="${JAVA_OPT} -server -Xms256m -Xmx256m -Xmn256m -XX:PermSize=128m -XX:MaxPermSize=320m"
5. 后台启动 nohup sh mqnamesrv & tail -f nohup.out
6. 启动brokerserver服务 nohup sh mqbroker -n localhost:9876 &
7. 查看状态 jps

rocketmq控制台安装
1. 解压安装包 unzip rocketmq-externals-master.zip && cd rocketmq-externals-master
2. 修改pom文件版本号 cd rocketmq-console && vim pom.xml 修改为 <rocketmq.version>4.4.0</rocketmq.version>
3. 指定机器地址 vim src/main/resources/application.properties 修改为rocketmq.config.namesrvAddr=127.0.0.1:9876
4. 在rocketmq-console目录下编译 mvn clean package -Dmaven.test.skip=true
5. 启动 cd target && nohup java -jar rocketmq-console-ng-1.0.0.jar &
6. 查看日志 tail -f nohup.out 启动测试 192.168.100.100:8080

2. 消息幂等性

  生产者发送消息之后,为了确保消费者消费成功 我们通常会采用手动签收方式确认消费,MQ就是使用了消息超时、重传、确认机制来保证消息必达。

场景:
 1. 订单服务(生产者),点击结算订单之后需要付款,这时就会发送一条“结算”的消息到mq的broker中。
 2. 此时支付服务(消费者)监听到这条消息之后就会处理结算扣款的逻辑,然后手动签收订单告诉mq我已经消费完成了。
 3. 如果在结算的过程中程序出现了异常,我们就返回mq一个消费失败的状态,此时mq就会重新发送这条消息;
  或者是由于网络波动支付服务一直没有响应消息的消费状态,mq也照样会重新发送这条消息。
 4. 那么这种情况下,支付服务(消费者)就会重复收到这条消息,如果不做任何判断就有可能会重复消费出现多次扣款的情况。
解决方案:
  在发送消息的时候,我们可以生成一个唯一ID标识每一条消息,将消息处理成功和去重日志通过事物的形式写入去重表或缓存中。每次消费之前都先查一遍,如果存在就说明消费过了直接返回消费成功。

3. 消息顺序性

  消息有序是指按照消息的发送顺序来消费。例如我们有这样一个需求:我们通过读取sql的日志文件得到它的所有sql,然后通过发送消息给其他服务去执行这些sql来同步数据。这个时候顺序性就显得尤为重要了。比如我们先修改这条数据,然后删除数据。倘若消费的时候先删除了再修改这时候得到的数据就不一致了。

  

解决方案:
  我们往一个topic里面发送消息时,它默认会维护几个队列,是随机发送到这些队列里面的。消费者集群消费时,实际上是一个服务监听一个队列。我们发送消息时,对于同个数据的操作指定发送到一个队列就好了,这时消费者就是按照顺序来消费的。

4. 顺序幂等案例实战

   比如我们现在我们有下面9条消息要发送,分别是对用户、订单、商品的操作,最终的结果应该是“用户被删除”,“订单已完成”,“商品被下架”。如果就条消息随机指定队列发送的话,就可能用户先被删除了,然后进行修改;也有可能支付订单的过程比较慢一直没反馈,从而收到多条消息而重复支付。

生产者对同一数据的操作发送到同一队列

public List<Data> getOrderList(){
        List<Data> list = new ArrayList<Data>();
        list.add(new Data(1,"注册用户"));
        list.add(new Data(2,"创建订单"));
        list.add(new Data(1,"修改用户"));
        list.add(new Data(2,"支付订单"));
        list.add(new Data(1,"删除用户"));
        list.add(new Data(3,"商品上架"));
        list.add(new Data(2,"完成订单"));
        list.add(new Data(3,"商品打折"));
        list.add(new Data(3,"商品下架"));
        return list;
    }
    
    /**
     * @Title:顺序发送
     * @author:吴磊  
     * @date:2019年5月4日 下午10:28:31
     */
    @RequestMapping("/send")
    public Object send() throws Exception{
        // 1. 获取操作数据
        List<Data> list = getOrderList();
        
        Message message = null;
        for(int i=0; i<list.size(); i++) {
            Data data = list.get(i);
            // keys是唯一标识, 用作重试幂等判断.
            String keys = data.getId()+"";
            // 2. 装载消息 (topic, 标签, key, 消息体)
            message = new Message(PayOrderlyProducer.TOPIC,"test_tag",keys,data.getType().getBytes());
            /* 3. 投递消息
             *       每一个topic默认为4个queue, 我们可以指订队列发送.
             *   send(Message msg, MessageQueueSelector selector, Object arg),会将arg作为队列下标传
             *   给MessageQueueSelector中MessageQueue的arg,从而选出具体的queue.
             */
            SendResult sendResult =  payOrderlyProducer.getProducer().send(message, new MessageQueueSelector() {
                @Override
                public MessageQueue select(List<MessageQueue> mqs, Message msg, Object arg) {
                    int id = (int) arg;
                    int index = id % mqs.size();//对queue总数取模
                    return mqs.get(index);
                }
            },data.getId());
         System.out.printf("发送结果=%s, sendResult=%s ,orderid=%s, type=%s\n", sendResult.getSendStatus(), sendResult.toString(),data.getId(),data.getType());
        }
        return "OK";
    }
View Code

消费者手动签收消息,如果消费失败就重复消费,如果重试次数过多就通知运营人员手动处理。

public PayOrderlyConsumer() throws MQClientException {

        consumer = new DefaultMQPushConsumer(consumerGroup);
        // 指定mq服务器地址
        consumer.setNamesrvAddr(NAME_SERVER);
        // 指定消费策略:这里是从最后一条消费
        consumer.setConsumeFromWhere(ConsumeFromWhere.CONSUME_FROM_LAST_OFFSET);

        //默认是集群方式,可以更改为广播,但是广播方式不支持重试
        //consumer.setMessageModel(MessageModel.CLUSTERING);
        consumer.subscribe(TOPIC, "test_tag");

        /**
         * MessageListenerOrderly是单线程的消费
         */
        consumer.registerMessageListener( new MessageListenerOrderly() {
            @Override
            public ConsumeOrderlyStatus consumeMessage(List<MessageExt> msgs, ConsumeOrderlyContext context) {
                MessageExt msg = msgs.get(0);
                int times = msg.getReconsumeTimes();
                try {
                    System.out.printf("%s Receive New Messages: %s %n", Thread.currentThread().getName(), new String(msgs.get(0).getBody()));
                    //模拟异常
                    String str = new String(msgs.get(0).getBody());
                    if(str.contains("订单")) {
                        int i=1/0;
                    }
                    
                    //做业务逻辑操作
                    System.out.println("消费成功");
                    return ConsumeOrderlyStatus.SUCCESS;
    
                } catch (Exception e) {
                    System.out.println("重试次数"+times);
                    //如果重试2次不成功,则记录,人工介入
                    if(times >= 2){
                        System.out.println("重试次数大于2,记录数据库,发短信通知开发人员或者运营人员");
                        return ConsumeOrderlyStatus.SUCCESS;
                    }
                    e.printStackTrace();
                    return ConsumeOrderlyStatus.SUSPEND_CURRENT_QUEUE_A_MOMENT;
                }
            }
        });
        consumer.start();
        System.out.println("consumer start ...");
    }
View Code

执行结果:可以看到id相同的数据(对同一数据的操作),都发送到同一队列了,然后消费者对每一个队列进行顺序消费,消费失败就会触发重试机制。

posted @ 2019-04-08 00:05  吴磊的  阅读(7010)  评论(0编辑  收藏  举报
//生成目录索引列表