MetaQ 实例之二

五、Message Producer

        一、创建生产者:通过工厂来创建消息生产者:

final MessageProducer producer = sessionFactory.createProducer();

        二、发布Topic:

                服务端注册它提供的topic到zookeeper,但是客户端链接到zookeeper后还不知道应该连接哪个服务器。它必须告诉zookeeper想去连接哪个topic的服务器,拿到服务器地址,然后才能建立socket连接,最后才可以发送消息。

因此创建生产者之后还需要一个发布Topic的过程:

        
          topic  ;
        producerpublish(topic);

            调用publish(topic)这个方法就可以告诉zookeeper我们想要查找提供topic为meta-test的服务器,从zookeeper获取服务器地址后,会话工厂会连接服务器,生产者就可以使用这些连接来发送消息了。

        publish(topic)方法可以针对同一个topic调用多次,这跟调用一次的效果是一样的; 而且topic都需要被发布一次。

        三、发送消息(同步)

final SendResult sendResult = producer.sendMessage(new Message(topic, "hello,MetaQ!".getBytes()));

        发送的结果保存在返回的SendResult实例。同时。你可能需要处理一些异常,如中断异常和客户端异常。通过SendResult.isSuccess()返回的布尔值可以判断发送成功还是失败。

如果发送失败,通过SendResult.getErrorMessage()方法可以获取发送失败的具体错误信息,方便调试。如果发送成功,则可以通过getPartition()getOffset()方法获取消息被发送到哪个分区,以及消息在该分区数据文件中的绝对偏移量。

send(message)是一个同步调用,默认使用3秒的超时时间,如果超时还没有返回则会抛出MetaOpeartionTimeoutException异常,这个异常是MetaClientException异常的子类。如果要捕捉超时异常,可以单独catch这个异常信息。

send同时有一个同步的重载的方法用于设定发送超时时间,比如5秒发送超时:

     producersend(message, , );

        四、异步发送

        同步发送会阻塞当前线程,直到服务端返回发送结果或者超时才能解除阻塞。现在应用都讲究异步化,因此MessageProducer还提供了异步发送的方法,采用经典的回调方式:

producersendMessage( (topic, getBytes()),  () {                    
                      (  ) {                         (resultisSuccess()) {                            outprintln(  resultgetPartition());

                        }                         {                            errprintln(  resultgetErrorMessage());
                        }
                    }                    
                      (  ) {
                        eprintStackTrace();
                    }
                });

        send(msg, callback)除了接收消息外,还接收一个实现了接口SendMessageCallback的回调对象。这个接口有两个必须实现的方法:

  • onMessageSent(sendResult),当消息发送后,返回发送的结果,检查sendResult来判断成功还是失败。

  • onException(throwable),当客户端异常发生的时候,此时消息已经发送失败,你可以处理这个异常或者简单地打印日志记录。

异步的send方法就没有返回结果了,发送结果通过onMessageSent返回给回调对象。同样,异步的send方法也有指定超时的重载版本send(message, callback, timeout, timeUnit)

        四 . 一、异步发送的流控

                异步发送消息,在发送结果从服务端返回之前,你传入的callback对象会一直在内存里,由于异步的send方法不会阻塞,因此理论上你不断地异步发送消息,注册无穷多个callback到生产者那里等待通知。但是由于内存是有限的,很可能过多的callback对象会导致内存爆满并频繁发生Full GC,导致服务彻底不可用。因此异步发送应该做一定的流量控制限制,比如这里提供一个简单的例子,使用信号量做流控:

      permits  ();     
         () {            
             (permitstryAcquire()) {                
                 {
                    producersend(message,  () {                         
                          ( ) {                              {                                
                            }                                              {                                
                                permitsrelease();
                            }

                        }                                 
                          ( ) {
                            eprintStackTrace();                            
                            permitsrelease();
                        }
                    });
                }                 ( e) {
                    eprintStackTrace();
                }                
                ;
            }             {                
                yield();
            }
        }

        我们设置信号量的最大许可为1万,然后每次发送消息前都尝试申请许可,申请成功才可以发送消息,然后在callback里释放许可(无论是发送成功失败还是产生异常都需要释放许可);如果申请失败,则调用yield让出执行权,等待while循环的下次重试。

这里只是一个简单的流控示范,你也可以用有限的阻塞队列等方式来实现流控。

        五、发送事务消息

        MetaQ支持发送事务消息,包括本地事务和支持XA协议(二阶段提交协议)的分布式事务

        六、发送本地事务消息

                所谓本地事务消息,通俗来说就是批量发送一批消息,要么同时发送成功,要么同时发送失败,保证事务的ACID的特性。MessageProducer类有beginTransaction()commit()rollback()方法用来控制一个本地事务,一个具体的例子:

                
                producerbeginTransaction();                {                     result1  producersend(msg1);                     result2  producersend(msg2);                    (resultisSuccess()  result2isSuccess()){
                         producercommit();
                     }{                         
                         producerrollback();
                     }

                }( e){                    
                    producerrollback();
                }

通过beginTransaction开启一个本地事务,然后在事务内发送两条消息,只有在两条消息都发送成功的时候才能通过commit方法提交事务,否则都使用rollback回滚事务,同样,在发生异常的时候也应该回滚事务。

事务还可以设置超时时间

       
        producersetTransactionTimeout();

超过这个时间还没有提交或者回滚的事务,将被服务器自动回滚。事务的最大超时时间不能超过服务器设置的maxTxTimeoutInSeconds,超过会被设置为maxTxTimeoutInSeconds设定的最大值。

beginTransaction,rollbackcommit本质上是跟服务器做通讯,告诉服务器开始,回滚或者提交某个事务,通讯必然涉及到通讯超时的问题,也可以设置这些事务请求的超时时间,默认为5秒:

producersetTransactionRequestTimeout(,);

通常来说这个值不应该超过事务的时间时间。

注意:事务内可以发送多个不同的topic类型的消息,前提是可以在zookeeper上找到至少一台broker提供提供所有这些topic。下面的XA事务也是一样。

        七、发送XA事务消息

                所谓XA事务消息,是指在一个事务内除了MetaQ这个事务源之外,还有另外一个事务源参与了事务,最常见的比如数据库源。一个典型的场景是:往MetaQ发送消息,同时要向数据库插入一条数据,两个操作要么同时成功,要么同时失败。不允许出现发送消息成功,而插入数据库失败的情况,反之亦然。比如下订单这个操作,要往订单表插入一条记录,同时发送一条消息到MetaQ,执行一些异步任务如通知用户、物流,记录日志,统计分析等等,就需要分布式事务。JavaEE规范支持XA协议,也就是两阶段提交协议,更详细的关于这块的信息请参考JTA规范和J2EE规范,阅读两阶段协议的相关资料来获得。

在这里我们举一个简单的例子,假设有一张订单表是这样:

  ``. (      ()  AUTO_INCREMENT,      () ,      () ,      ()
);

现在我们就要模拟一个下订单的操作,同时插入一条记录到订单表,并且发送一条消息给MetaQ处理。

首先,创建XAMessageSessionFactory实例,跟创建MessageSessionFactory类似:

  xasf   (metaClientConfig);

接下来创建XAMessageProducer并发布topic:

   topic  ;  xaMessageProducer  xasfcreateXAProducer(); 
 xaMessageProducerpublish(topic);

XAMessageProducer就是一个参与分布式事务的事务源,接下来要创建一个XADataSource来代表数据库事务源,注意必须使用数据库的XA数据源:

          mysqlXADataSource   ();
        mysqlXADataSource
        .setUrl();
        mysqlXADataSourcesetUser();
        mysqlXADataSourcesetPassword();
        mysqlXADataSourcesetPreparedStatementCacheSize();

接下来你还需要选择一个JTA实现,比如我们这里用Atomikos这个开源事务管理器实现,你也可以用容器内的JTA管理器,只要能获取TransactionManager即可,我们还需要一个XATransactionTemplate来包装XA事务的模板操作,我已经在例子里提供。利用这个模板来执行分布式事务:

                uid  ;                           address  ;                                 
                  template   (tm, xads, xaMessageProducer);
                templateexecuteCallback( () {                    
                      (  ,   ,  )                              {                          pstmt 
                                connprepareStatement();
                        pstmtsetInt(, uid);
                        pstmtsetString(, address);
                        pstmtclose();                         (pstmtexecuteUpdate()  ) {
                            statussetRollbackOnly();                             ;
                        }                         (producersendMessage( (topic, addressgetBytes()))isSuccess()) {
                            statussetRollbackOnly();
                        }                         ;
                    }
                });

注意在XACallback是如何处理发送失败或者插入失败的情况,都通过status.setRollbackOnly();来回滚整个分布式事务。也可以通过抛出运行时异常来回滚事务。

你可以尝试将address设置为null来故意违反数据库约束来观察事务的回滚情况。

XAMessageProducerMessageProducer类似,同样可设置事务超时和事务请求超时,恕不赘述。

        八、分布式事务的迁移

        XAMessageProducer有一个全局唯一的标识符,可以通过getUniqueQualifier()获取,这个标识符必须全局唯一,它将被保存在MetaQ和TransactionManager的事务日志里,用于追踪和标识。

当运行这个分布式事务的某台机器要做迁移,必须同时迁移TransactionManager的事务日志(为了recover事务),为了保证在不同机器之间做到平滑迁移,XAMessageProducer的标识符最好明确设置成一个不变的有意义的全局标识符,默认的标识符跟机器hostname绑定,你可以通过setUniqueQualifier方法明确修改这个标识符。

        九、挂起的分布式事务的处理

                TODO

        十、发送默认topic消息

                MetaQ的topic必须首先在服务端配置,生产者才可以发送这个Topic的消息。但是常常会有这样的需求,希望能发送任何类型topic的消息,而不是每次都要在MetaQ服务端配置topic。这可以通过设置MessageProducer的默认topic来实现。假设你发送topic1的消息,当生产者在zookeeper上找不到提供topic1的服务器,那就会选择提供默认topic的服务器来发送这条消息。

首先在服务端配置一个默认topic,如:

 [topic=app-*]

注意,必须保证app-*这个topic的numPartitions和系统[system]的配置保持一致。因为任意topic的消息因为没有明确在服务端配置,都会使用系统的默认配置,但是发送的时候却是使用app-*的配置,因此两者必须保持一致。

接下来设置MessageProducer的默认topic:

producersetDefaultTopic();

接下来你就可以发布任意以app-开头的topic的消息,例如:

producersend( (,getBytes()));
producersend( (,getBytes()));
producersend( (,getBytes()));

哪怕这些topic没有被publish过。这些topic都将在服务端动态创建,无需明确配置。

我们这里以app-*作为默认topic,如果你不想有任何前缀设置,那也可以使用*作为默认topic,这样就可以发送任意类型topic的消息。

        十一、定制分区选择器

            默认情况下,消息生产者会获取所有服务器的分区并组织成一张链表,按照broker id,partition号的顺序的排序,然后轮询发送消息到这些分区。这个轮询策略就是一个分区选择器RoundRobinPartitionSelector。你可以定制自己的分区选择器,只要实现PartitionSelector接口即可:

   {    
      ( ,  ,  )  ;
}

在定制实现自己的分区选择器之后,可以在创建消息生产者的时候传入:

     producer  sessionFactorycreateProducer( ());

除了RoundRobinPartitionSelector之外,MetaQ还提供RandomPartitionSelector用于随机选择分区发送。

        十二、发送顺序消息

        TODO

        十三、关闭:消息生产者作为重量级资源,最好也作为单例存在,同时也需要明确地调用shutdown()方法来释放资源。







你可能感兴趣的:(MetaQ 实例之二)