RocketMQ4.3.X笔记(5):生产者 Producer

Producer 概述

生产者向消息队列里写人消息,不同的业务场景需要生产者采用不同的写人策略。比如同步发送、异步发送、延迟发送、发送事务消息等

发送状态 SendStatus

  1. 发送消息时,将获得发送结果 SendResult,其中包含发送状态 SendStatus。我们假设消息的isWaitStoreMsgOK=true(默认为true)。如果没有抛出异常,我们将始终得到 SEND_OK
  2. 不同状态在不同的刷盘策略和同步策略的配置下含义是不同的。
  • SendStatus.SEND_OK:表示发送成功(不代表没有问题),发送成功的具体含义,比如消息是否已经被存储到磁盘?消息是否被同步到了Slave 上?消息在Slave 上是否被写人磁盘?需要结合所配置的刷盘策略、主从策略来定。这个状态还可以简单理解为,没有发生上面列出的三个问题状态就是 SEND_OK
  • SendStatus.FLUSH_DISK_TIMEOUT:表示没有在规定时间内完成刷盘(需要Broker 的刷盘策略创立设置成 SYNC_FLUSH 才会报这个错误)。FlushDiskType 默认是ASYNC_FLUSH,并且syncFlushTimeout 默认是5s
  • SendStatus.FLUSH_SLAVE_TIMEOUT:表示在主备方式下,并且Broker 被设置成 SYNC_MASTER 方式(默认是 ASYNC_MASTER),没有在设定时间内完成主从同步,默认 syncFlushTimeout 是 5s。
  • SendStatus.SLAVE_NOT_AVAILABLE:表示在主备方式下,并且Broker 被设置成SYNC_MASTER ,但是没有找到被配置成Slave 的Broker 。

消息重复和消息丢失 Duplication or Missing

  1. FLUSH_DISK_TIMEOUTFLUSH_SLAVE_TIMEOUT 的发送状态并且 Broker 挂了,导致消息丢失的风险,这时有两种处理方式:
  • 什么都不做,导致消息丢失
  • 重发,有可能导致消息重复。推荐的方式,并且在消费的时候处理重复的情况。
  1. 注意:但是当发送状态 SLAVE_NOT_AVAILABLE时,重新发送是无用的。如果发生这种情况,应该保持场景并警告集群管理器。

发送超时 Timeout

  1. 客户机将请求发送给Broker,并等待响应,但是如果超过最大等待时间,且没有返回响应,则客户机将抛出 RemotingTimeoutException。默认等待时间为3秒。可以使用 send(msg, timeout) 传递超时参数。
  2. 不建议等待时间过短,因为 Broker 需要一些时间来刷新磁盘或。
  3. 如果该值超过 syncFlushTimeout 很多,那么它可能没有什么影响,因为代理可能在超时之前返回带有FLUSH_SLAVE_TIMEOUT或FLUSH_SLAVE_TIMEOUT的响应

消息大小 Message Size

官方建议不大于512K

异步发送 Async Sending

默认 send(msg) 将阻塞,直到返回响应。如果关心性能,建议使用send(msg, callback),它将以异步方式工作

生产者组 Producer Group

  1. 正常情况下,producer group没有什么实际的作用。但是如果是有事务的,需要关注一下。
  2. 默认情况下,一个的JVM中的同一个 producer group 建议使用一个 producer 。因为生成器在发送消息方面足够强大,每个生成器组建议只允许一个实例,以避免不必要的生成器实例初始化。

线程安全 Thread Safety

producer 是线程安全的

发送性能 Performance

如果想在一个JVM中使用多个producer进行大数据处理,建议:

  • 使用多个producer异步发送,建议3~5个
  • 为每个producer,设置 setInstanceName

DefaultMQProducer 简单使用

发送消息5步骤

  1. 设置Producer 的 GroupName
  2. 设置lnstanceName ,当一个Jvm 需要启动多个Producer 的时候,通过设置不同的InstanceName 来区分,不设置的话系统使用默认名称DEFAULT
  3. 设置NameServer 地址
  4. 设置发送失败重试次数,当网络出现异常的时候,这个次数影响消息的重复投递次数。想保证不丢消息,可以设置多重试几次
  5. 组装消息并发送。消息的发送有同步和异步两种方式。

例子

写一个高质量的生产者程序,重点在于对发送结果的处理,要充分考虑各种异常,写清对应的处理逻辑

  1. 同步发送
public class SimpleSyncProducer {
    public static void main(String[] args) throws MQClientException, UnsupportedEncodingException, RemotingException, InterruptedException, MQBrokerException {

        // 设置 producerGroup
        DefaultMQProducer producer = new DefaultMQProducer("base_producer_group");
        // 设置 nameServer
        producer.setNamesrvAddr("10.0.64.106:9876;10.0.64.107:9876");
        // 设置实例名称 InstanceName
        producer.setInstanceName("SimpleSyncProducer");
        // 设置重试次数
        producer.setRetryTimesWhenSendFailed(3);
        producer.start();

        for (int i = 0; i < 100; i++) {
            // 组装消息
            Message message = new Message();
            message.setTopic("base_topic");
            message.setTags("base_api");
            message.setBody(("Hello RocketMQ > " + i).getBytes(RemotingHelper.DEFAULT_CHARSET));
            // 同步发送
            SendResult result = producer.send(message);
            System.out.printf("%s%n", result);
        }
        producer.shutdown();
    }
}

  1. 异步发送
public class SimpleAsyncProducer {
    public static void main(String[] args) throws MQClientException, UnsupportedEncodingException, RemotingException, InterruptedException, MQBrokerException {

        // 1- 设置 GroupName
        DefaultMQProducer producer = new DefaultMQProducer("base_producer_group");
        // 设置NameServer 地址
        producer.setNamesrvAddr("10.0.64.106:9876;10.0.64.107:9876");
        // 设置 lnstanceName ,当一个Jvm 需要启动多个Producer 的时候,通过 设置不同的InstanceName 来区分,不设置的话系统使用默认名称“DEFAULT” 。
        producer.setInstanceName("base_producer");
        // 设置发送失败重试次数
        producer.setRetryTimesWhenSendFailed(3);
        producer.start();

        // 组装消息并发送
        // 消息的发送有同步和异步两种方式
        for (int i = 0; i < 100; i++) {
            Message message = new Message("base_topic", "base_api", ("Hello RocketMQ > " + i).getBytes(RemotingHelper.DEFAULT_CHARSET));

            // 异步发送
            producer.send(message, new SendCallback() {
                @Override
                public void onSuccess(SendResult sendResult) {
                    System.out.printf("%s%n", sendResult);
                    sendResult.getSendStatus();
                }

                @Override
                public void onException(Throwable e) {
                    e.printStackTrace();
                }
            });
        }

        producer.shutdown();

    }
}

  1. 异步 AsyncProducer 集合 CountDownLatch 实现
public class AsyncProducer {

    public static void main(String[] args) throws MQClientException, InterruptedException {

        DefaultMQProducer producer = new DefaultMQProducer("Jodie_Daily_test");
        producer.start();
        // 异步重试
        producer.setRetryTimesWhenSendAsyncFailed(0);

        // 定义 CountDownLatch
        int messageCount = 100;
        final CountDownLatch countDownLatch = new CountDownLatch(messageCount);
        for (int i = 0; i < messageCount; i++) {
            try {
                final int index = i;
                // 组装消息
                Message msg = new Message("Jodie_topic_1023", "TagA", "OrderID188", "Hello world".getBytes(RemotingHelper.DEFAULT_CHARSET));
                // 异步发送
                producer.send(msg, new SendCallback() {
                    @Override
                    public void onSuccess(SendResult sendResult) {
                        countDownLatch.countDown();
                        System.out.printf("%-10d OK %s %n", index, sendResult.getMsgId());
                    }

                    @Override
                    public void onException(Throwable e) {
                        countDownLatch.countDown();
                        System.out.printf("%-10d Exception %s %n", index, e);
                        e.printStackTrace();
                    }
                });
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
        countDownLatch.await(5, TimeUnit.SECONDS);
        producer.shutdown();
    }
}

延迟消息:setDelayTimeLevel

  1. RocketMQ 支持发送延迟消息,Broker 收到这类消息后,延迟一段时间再处理, 使消息在规定的一段时间后生效。
  2. 延迟消息的使用方法是在创建Message 对象时,调用setDelayTimeLevel(int level) 方法设置延迟时间,然后再把这个消息发送出去。目前延迟的时间不支持任意设置,仅支持预设值的时间长度(1s/5s/1Os/30s/1m/2m/3m/4m/5m/6m/7m/8m/9m/1Om/20m/30m/1h/2h) 。比如 setDelayTimeLevel(3)表示延迟10s 。
// 发送延迟消息 延迟10s
message.setDelayTimeLevel(3);

自定义消息发送规则:MessageQueueSelector

  1. 消息发送与消费的黑盒:
  • 一个 Topic 会有多个Message Queue ,如果使用Producer 的默认配置,这个Producer 会轮流向各个MessageQueue 发送消息。
  • Consumer 在消费消息的时候,会根据负载均衡策略,消费被分配到的Message Queue ,如果不经过特定的设置,某条消息被发往哪个MessageQueue ,被哪个Consumer 消费是未知的。
  1. 如果业务需要把消息发送到指定的MessageQueue 里,比如把同一类型的消息都发往相同的MessageQueue,可以用 MessageQueueSelector 实现
  2. 发送消息的时候,使用public SendResult send ( Message msg, MessageQueueSelector selector, Object arg )函数发送消息即可。在MessageQueueSelector 的实现中,根据传人的Object 参数,或者根据Message 消息内容确定把消息发往那个Message Queue ,返回被选中的Message Queue 。
public class CusProducer {

    public static void main(String[] args) throws Exception {

        // 设置 producerGroup
        DefaultMQProducer producer = new DefaultMQProducer("base_producer_group");
        // 设置 nameServer
        producer.setNamesrvAddr("10.0.64.106:9876;10.0.64.107:9876");
        producer.start();

        for (int i = 0; i < 100; i++) {
            Message message = new Message();
            message.setTopic("base_topic");
            message.setTags("base_api");
            message.setBody(("Hello RocketMQ > " + i).getBytes(RemotingHelper.DEFAULT_CHARSET));
            // 设置自定义规则
            SendResult result = producer.send(message, new OrderMessageQueueSelector(), 13);
            System.out.printf("%s%n", result);
        }

        producer.shutdown();
    }

    /**
     * 自定义规则: 返回被选中的Message Queue 
     * 在MessageQueueSelector 的实现中,根据传人的Object 参数,或者根据Message 消息内容确定把消息发往那个Message Queue
     */
    public static class OrderMessageQueueSelector implements MessageQueueSelector {
        @Override
        public MessageQueue select(List<MessageQueue> mqs, Message msg, Object orderKey) {
            int id = Integer.parseInt(orderKey.toString());
            int idMainIndex = id / 100;
            int size = mqs.size();
            int index = idMainIndex % size;
            return mqs.get(index);
        }
    }
}

事务消息

事务消息与实现思路

  1. RocketMQ 的事务消息,是指发送消息事件和其他事件需要同时成功或同时失败(发送消息与本地业务一致性)。比如银行转账, A 银行的某账户要转一万元到B 银行的某账户。A 银行发送B 银行账户增加一万元 这个消息,要和从A 银行账户扣除一万元这个操作同时成功或者同时失败

  2. RocketMQ 采用两阶段提交的方式实现事务消息,使用 TransactionMQProducer 类实现,先发消息,然后业务操作,根据业务操作结果确认之前消息是提交还是回滚(做commit 还是rollback)。具体步骤如下:

  • 1)发送方向RocketMQ 发送待确认消息
  • 2)RocketMQ 将收到的待确认消息持久化成功后,向发送方回复消息已经发送成功,此时第一阶段消息发送完成。
  • 3)发送方开始执行本地事件逻辑,即业务操作
  • 4)发送方根据本地事件执行结果向 RocketMQ 发送二次确认( Commit 或是Rollback ) 消息
    • RocketMQ 收到Commit 状态则将第一阶段消息标记为可投递,订阅方将能够收到该消息;
    • 收到Rollback 状态则删除第一阶段的消息,订阅方接收不到该消息。
  • 5)如果出现异常情况,步骤4)提交的二次确认最终未到达 RocketMQ,服务器在经过固定时间段后将对待确认消息、发起回查请求。
  • 6)发送方收到消息回查请求后(如果发送一阶段消息的Producer 不能工作,回查请求将被发送到和Producer 在同一个Group 里的其他Producer ),通过检查对应消息的本地事件执行结果返回Co mmit 或Roolback 状态
  • 7)RocketMQ 收到回查请求后,按照步骤4 ) 的逻辑处理
  1. 以上是 RocketMQ 之前的版本实现事务消息的逻辑。但是因为RocketMQ 依赖将数据顺序写到磁盘这个特征来提高性能,步骤4 )却需要更改第一阶段消息的状态,这样会造成磁盘Catch 的脏页过多,降低系统的性能。所以RocketMQ 在4.x 的版本中将这部分功能去除。系统中的一些上层Class 都还在,用户可以根据实际需求实现自己的事务功能。

  2. 客户端有三个类来支持用户实现事务消息:

  • LocalTransactionExecuter 用来实例化步骤3 )的逻辑,根据情况返回 LocalTransactionState.ROLLBACK_MESSAGE 或者LocalTransactionState.COMMIT_MESSAGE 状态。
  • TransactionMQProducer 其用法和 DefaultMQProducer 类似,要通过它启动一个Producer 并发消息,但是比DefaultMQProducer 多设置本地事务处理函数和回查状态函数。
  • TransactionCheckListener 实现步骤5 )中MQ 服务器的回查请求,返回 LocalTransactionState.ROLLBACK_MESSAGE 或者LocalTransactionState.COMMIT_MESSAGE

官方例子代码

  1. 消息发送
public class TransactionProducer {
    public static void main(String[] args) throws MQClientException, InterruptedException {
        TransactionListener transactionListener = new TransactionListenerImpl();

        // 使用 TransactionMQProducer
        TransactionMQProducer producer = new TransactionMQProducer("please_rename_unique_group_name");
        // 自定义 线程池并发
        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;
            }
        });

        // 设置 线程池与事务监听
        producer.setExecutorService(executorService);
        producer.setTransactionListener(transactionListener);
        producer.start();

        String[] tags = new String[]{"TagA", "TagB", "TagC", "TagD", "TagE"};
        for (int i = 0; i < 10; i++) {
            try {
                Message msg = new Message("TopicTest1234", tags[i % tags.length], "KEY" + i, ("Hello RocketMQ " + i).getBytes(RemotingHelper.DEFAULT_CHARSET));
                SendResult sendResult = producer.sendMessageInTransaction(msg, null);
                System.out.printf("%s%n", sendResult);

                Thread.sleep(10);
            } catch (MQClientException | UnsupportedEncodingException e) {
                e.printStackTrace();
            }
        }

        for (int i = 0; i < 100000; i++) {
            Thread.sleep(1000);
        }
        producer.shutdown();
    }
}

  1. 本地事务监听
  • executeLocalTransaction 当发送 prepare 消息成功后, 回调这个方法,处理本地事务业务,返回事务状态。
  • checkLocalTransaction 用于检查本地事务状态并响应MQ检查请求
public class TransactionListenerImpl implements TransactionListener {
    private AtomicInteger transactionIndex = new AtomicInteger(0);

    private ConcurrentHashMap<String, Integer> localTrans = new ConcurrentHashMap<>();

    @Override
    public LocalTransactionState executeLocalTransaction(Message msg, Object arg) {
        int value = transactionIndex.getAndIncrement();
        int status = value % 3;
        localTrans.put(msg.getTransactionId(), status);
        return LocalTransactionState.UNKNOW;
    }

    @Override
    public LocalTransactionState checkLocalTransaction(MessageExt msg) {
        Integer status = localTrans.get(msg.getTransactionId());
        if (null != status) {
            switch (status) {
                case 0:
                    return LocalTransactionState.UNKNOW;
                case 1:
                    return LocalTransactionState.COMMIT_MESSAGE;
                case 2:
                    return LocalTransactionState.ROLLBACK_MESSAGE;
                default:
                    return LocalTransactionState.COMMIT_MESSAGE;
            }
        }
        return LocalTransactionState.COMMIT_MESSAGE;
    }
}

参考

  1. Apache RocketMQ 官网
  2. Learning_RocketMQ 源码
  3. 官方 producer 最佳实践
  4. 官方例子
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值