• 消息中间件(二)-----ActiveMQ高级特性和用法


    嵌入式ActiveMQ

    package cn.enjoyedu.embed;
    
    import org.apache.activemq.broker.BrokerService;
    import org.apache.activemq.broker.jmx.ManagementContext;
    
    public class EmbedMQ {
        public static void main(String[] args) throws Exception {
            BrokerService brokerService = new BrokerService();
            brokerService.setBrokerName("EmbedMQ");
            brokerService.addConnector("tcp://localhost:62000");
            brokerService.setManagementContext(new ManagementContext());
            brokerService.start();
        }
    }

    消息存储的持久化

    ActiveMQ的另一个问题就是只要是软件就有可能挂掉,挂掉不可怕,怕的是挂掉之后把信息给丢了,怎么办,可以进行消息的持久化,ActiveMQ提供了几种持久化方式:

    1. AMQ消息存储-基于文件的存储方式,它具有写入速度快和容易恢复的特点。消息存储在一个个文件中,文件的默认大小为32M,如果一条消息的大小超过了32M,那么这个值必须设置大一点。当一个存储文件中的消息已经全部被消费,那么这个文件将被标识为可删除,在下一个清除阶段,这个文件被删除。AMQ适用于ActiveMQ5.3之前的版本。
    2. KahaDB消息存储-提供了容量的提升和恢复能力,是现在的默认存储方式;KahaDB是基于文件的本地数据库储存形式,虽然没有AMQ的速度快,但是它具有强扩展性,恢复的时间比AMQ短,从5.4版本之后KahaDB做为默认的持久化方式。
    3. JDBC消息存储-消息基于JDBC存储的;
    4. Memory消息存储-基于内存的消息存储,由于内存不属于持久化范畴。所以内存存储不在讨论范围内。

    KahaDB

    由于KahaDB是默认的持久化存储方案。所以即使你不配置任何的KahaDB参数信息,ActiveMQ也会启动KahaDB。这种情况下,KahaDB文件所在位置是你的ActiveMQ安装路径下的/data/KahaDB子目录。

    <persistenceAdapter>
        <kahaDB directory="${activemq.data}/kahadb"/>
    </persistenceAdapter>

    关系型数据库存储方案

    从ActiveMQ 4+版本开始,ActiveMQ就支持使用关系型数据库进行持久化存储——通过JDBC实现的数据库连接。可以使用的关系型数据库囊括了目前市面的主流数据库。

    使用JDBC的方式持久化

    1、修改配置文件conf/activemq.xml:

    将其中的这段配置:

    <persistenceAdapter>
        <kahaDB directory="${activemq.data}/kahadb"/>
    </persistenceAdapter>

    修改为为:

    <persistenceAdapter>
           <jdbc PersistenceAdapter  dataSource="#mysql-ds "/>
    </persistenceAdapter>

    2、然后在</broker>标签后,增加数据源的配置:

        <bean id="mysql-ds" class="org.apache.commons.dbcp2.BasicDataSource" destroy-method="close">
    
            <property name="driverClassName" value="com.mysql.jdbc.Driver"/>
    
            <property name="url" value="jdbc:mysql://localhost:3306/activemq?relaxAutoCommit=true&useUnicode=true&characterEncoding=utf-8&serverTimezone=UTC"/>
    
            <property name="username" value="root"/>
    
            <property name="password" value="123456"/>
    
            <property name="poolPreparedStatements" value="true"/>
    
        </bean>

    其中?relaxAutoCommit=true必须有,其他的属性根据数据库的配置自行决定。

    3、将mysql-connector-java-5.1.34-bin.jar(版本可以自行选择)放到ActiveMQ的/ lib目录下。

    4、在Mysql数据库中增加在连接字符串中设置的数据库名activemq

    5、运行后,会发现在库中增加了3个表

    activemq_acks:用于存储订阅关系。如果是持久化Topic,订阅者和服务器的订阅关系在这个表保存,主要数据库字段如下:

      container:消息的destination

      sub_dest:如果是使用static集群,这个字段会有集群其他系统的信息

      client_id:每个订阅者都必须有一个唯一的客户端id用以区分

      sub_name:订阅者名称

      selector:选择器,可以选择只消费满足条件的消息。条件可以用自定义属性实现,可支持多属性and和or操作

      last_acked_id:记录消费过的消息的id

    activemq_lock:在集群环境中才有用,只有一个Broker可以获得消息,称为Master Broker,其他的只能作为备份等待Master Broker不可用,才可能成为下一个Master Broker。这个表用于记录哪个Broker是当前的Master Broker。

    activemq_msgs:用于存储消息,Queue和Topic都存储在这个表中。主要的数据库字段如下:

      id:自增的数据库主键

      container:消息的destination

      msgid_prod:消息发送者客户端的主键

      msg_seq:是发送消息的顺序,msgid_prod+msg_seq可以组成jms的messageid

      expiration:消息的过期时间,存储的是从1970-01-01到现在的毫秒数

      msg:消息本体的java序列化对象的二进制数据

      priority:优先级,从0-9,数值越大优先级越高

    消息的持久化订阅

    分别运行订阅模式和P2P模式,可以发现,P2P模式缺省把消息进行持久化,而topic模式是没有的。

    一般topic模式实验:

    1、 启动两个消费者,启动一个生产者,发送消息,两个消费者都可以收到。

    2、 关闭一个消费者,生产者发送消息,活跃的消费者可以收到消息,启动被关闭的消费者,无法收到消息。

    3、 关闭所有消费者,生产者发送消息,在ActiveMQ控制台可以看见消息已被接收,关闭再启动ActiveMQ,启动消费者收不到消息。

    如果topic模式下,需要消费者在离线又上线后,不管ActiveMQ是否重启过,都保证可以接受到消息,就需要进行持久化订阅。具体代码参见模块no-spirng包durabletopic。

    持久Topic消费者端

    需要设置客户端id:connection.setClientID("Mark");

    消息的destination变为 Topic

    消费者类型变为TopicSubscriber

    消费者创建时变为session.createDurableSubscriber(destination,"任意名字,代表订阅名 ");

    运行一次消费者,将消费者在ActiveMQ上进行一次注册。然后在ActiveMQ的管理控制台subscribers页面可以看见我们的消费者。

    代码:

    package cn.enjoyedu.durabletopic;
    
    import org.apache.activemq.ActiveMQConnection;
    import org.apache.activemq.ActiveMQConnectionFactory;
    
    import javax.jms.*;
    
    public class JmsDurableTopicConsumer {
        private static final String USERNAME = ActiveMQConnection.DEFAULT_USER;//默认连接用户名
        private static final String PASSWORD = ActiveMQConnection.DEFAULT_PASSWORD;//默认连接密码
        private static final String BROKEURL = ActiveMQConnection.DEFAULT_BROKER_URL;//默认连接地址
    
        public static void main(String[] args) {
            ConnectionFactory connectionFactory;//连接工厂
            Connection connection = null;//连接
            Session session;//会话 接受或者发送消息的线程
            TopicSubscriber  messageConsumer;//消息的消费者
            //实例化连接工厂
            connectionFactory = new ActiveMQConnectionFactory(JmsDurableTopicConsumer.USERNAME, JmsDurableTopicConsumer.PASSWORD, JmsDurableTopicConsumer.BROKEURL);
    
            try {
                //通过连接工厂获取连接
                connection = connectionFactory.createConnection();
                connection.setClientID("Mark");
                //启动连接
                connection.start();
                //创建session
                session = connection.createSession(false, Session.AUTO_ACKNOWLEDGE);
                //创建一个连接HelloWorld的消息队列
                Topic destination = session.createTopic("DurableTopic");
                //创建消息消费者
                messageConsumer = session.createDurableSubscriber(destination,"er");
                messageConsumer.setMessageListener(new MessageListener() {
                    public void onMessage(Message message) {
                        try {
                            System.out.println("Accept msg : " +((TextMessage)message).getText());
                        } catch (JMSException e) {
                            e.printStackTrace();
                        }
                    }
                });
            } catch (JMSException e) {
                e.printStackTrace();
            }
        }
    }

    效果:

    1、 运行生产者,发布消息,多个消费者可以正常收到。

    2、 关闭一个消费者,运行生产者,发布消息后再启动被关闭的消费者,可以收到离线后的消息;

    3、 关闭所有消费者,运行生产者,发布消息后,关闭ActiveMQ再启动,启动所有消费者,都可以收到消息。

    注意:生产者端无需另外单独配置

    持久Topic生产者端

    package cn.enjoyedu.durabletopic;
    
    import org.apache.activemq.ActiveMQConnection;
    import org.apache.activemq.ActiveMQConnectionFactory;
    
    import javax.jms.*;
    
    public class JmsDurableTopicProducer {
        //默认连接用户名
        private static final String USERNAME = ActiveMQConnection.DEFAULT_USER;
        //默认连接密码
        private static final String PASSWORD = ActiveMQConnection.DEFAULT_PASSWORD;
        //默认连接地址
        private static final String BROKEURL = ActiveMQConnection.DEFAULT_BROKER_URL;
        //发送的消息数量
        private static final int SENDNUM = 3;
    
        public static void main(String[] args) {
            ConnectionFactory connectionFactory;
            Connection connection = null;
            Session session;
            Destination destination;
            MessageProducer messageProducer;
            connectionFactory = new ActiveMQConnectionFactory(USERNAME,PASSWORD,BROKEURL);
            try {
                connection = connectionFactory.createConnection();
                connection.start();
                session = connection.createSession(true,Session.AUTO_ACKNOWLEDGE);
                destination = session.createTopic("DurableTopic");
                messageProducer = session.createProducer(destination);
                messageProducer.setDeliveryMode(DeliveryMode.PERSISTENT);
                for(int i=0;i<SENDNUM;i++){
                    String msg = "发送消息"+i+" "+System.currentTimeMillis();
                    TextMessage message = session.createTextMessage(msg);
                    System.out.println("发送消息:"+msg);
                    messageProducer.send(message);
                }
                session.commit();
            } catch (JMSException e) {
                e.printStackTrace();
            }finally {
                if(connection!=null){
                    try {
                        connection.close();
                    } catch (JMSException e) {
                        e.printStackTrace();
                    }
                }
            }
        }
    }

    消息非持久化

    修改messageProducer.setDeliveryMode(DeliveryMode.NON_PERSISTENT);来设置消息本身的持久化属性为非持久化。重复上述实验,可以发现,第1,2点保持不变,但是第三点,当关闭ActiveMQ再启动,消费者关闭后再启动,是收不到消息的。

    说明,即使进行了持久订阅,但是消息本身如果是不持久化的,ActiveMQ关闭再启动,这些非持久化的消息会丢失,进行持久订阅的消费者也是收不到自身离线期间的消息的。

    消息的可靠性

    消息发送成功后,接收端接收到了消息。然后进行处理,但是可能由于某种原因,高并发也好,IO阻塞也好,反正这条消息在接收端处理失败了。而点对点的特性是一条消息,只会被一个接收端给接收,只要接收端A接收成功了,接收端B,就不可能接收到这条消息,如果是一些普通的消息还好,但是如果是一些很重要的消息,比如说用户的支付订单,用户的退款,这些与金钱相关的,是必须保证成功的,那么这个时候要怎么处理呢?必须要保证消息的可靠性,除了消息的持久化,还包括两个方面,一是生产者发送的消息可以被ActiveMQ收到,二是消费者收到了ActiveMQ发送的消息。

    生产者

    send方法

    在生产者端,我们会使用send() 方法向ActiveMQ发送消息,默认情况下,持久化消息以同步方式发送,send() 方法会被阻塞,直到 broker 发送一个确认消息给生产者,这个确认消息表示broker已经成功接收到消息,并且持久化消息已经把消息保存到二级存储中。

    实验send()方法:send方法每执行一次,ActiveMQ管理控制台增加一条入队消息,数据库中增加一条消息。

                /* 通过连接工厂获取连接*/
                connection = connectionFactory.createConnection();
                /* 启动连接*/
                connection.start();
                /* 创建session
                 * 第一个参数表示是否使用事务,第二次参数表示是否自动确认*/
                session = connection.createSession(false, Session.AUTO_ACKNOWLEDGE);
                /* 创建一个消息队列*/
                //topic
                destination = session.createQueue("MsgReliability");
                /* 创建消息生产者*/
                messageProducer = session.createProducer(destination);
                /* 循环发送消息*/
                for (int i = 0; i < SENDNUM; i++) {
                    String msg = "发送消息" + i + " " + System.currentTimeMillis();
                    TextMessage textMessage = session.createTextMessage(msg);
                    System.out.println("标准用法:" + msg);
                    messageProducer.send(textMessage);
                }

    事务消息

    事务中消息(无论是否持久化),会进行异步发送,send() 方法不会被阻塞。但是commit 方法会被阻塞,直到收到确认消息,表示broker已经成功接收到消息,并且持久化消息已经把消息保存到二级存储中。

    实验事务消息:send方法每执行一次,ActiveMQ管理控制台和数据库中没有任何变化,只有执行完session.commit()后ActiveMQ管理控制台和数据库中才增加。

                /* 通过连接工厂获取连接*/
                connection = connectionFactory.createConnection();
                /* 启动连接*/
                connection.start();
                /* 创建session
                 * 第一个参数表示是否使用事务,第二次参数表示是否自动确认
                 * commit,rollbakc*/
                session = connection.createSession(true, Session.AUTO_ACKNOWLEDGE);
                /* 创建一个消息队列*/
                destination = session.createQueue("MsgReliability");
                /* 创建消息生产者*/
                messageProducer = session.createProducer(destination);
                /* 循环发送消息*/
                for (int i = 0; i < SENDNUM; i++) {
                    String msg = "发送消息" + i + " " + System.currentTimeMillis();
                    TextMessage textMessage = session.createTextMessage(msg);
                    System.out.println("标准用法:" + msg);
                    messageProducer.send(textMessage);
                }
                session.commit();

    总结

    非持久化又不在事务中的消息,可能会有消息的丢失。为保证消息可以被ActiveMQ收到,我们应该采用事务消息或持久化消息。

    消费者

    对消息的确认有4种机制

    • 1、 AUTO_ACKNOWLEDGE = 1    自动确认
    • 2、 CLIENT_ACKNOWLEDGE = 2    客户端手动确认   
    • 3、 DUPS_OK_ACKNOWLEDGE = 3    自动批量确认
    • 4、 SESSION_TRANSACTED = 0    事务提交并确认

    ACK_MODE描述了Consumer与broker确认消息的方式(时机),比如当消息被Consumer接收之后,Consumer将在何时确认消息。所以ack_mode描述的不是producer于broker之间的关系,而是customer于broker之间的关系。

    对于broker而言,只有接收到ACK指令,才会认为消息被正确的接收或者处理成功了,通过ACK,可以在consumer与Broker之间建立一种简单的“担保”机制.

    session = connection.createSession(false, Session.AUTO_ACKNOWLEDGE);

    第一个参数:是否支持事务,如果为true,则会忽略第二个参数,自动被jms服务器设置为SESSION_TRANSACTED

    AUTO_ACKNOWLEDGE  

    自动确认

        “同步”(receive)方法返回message给消息时会立即确认。

         在"异步"(messageListener)方式中,将会首先调用listener.onMessage(message),如果onMessage方法正常结束,消息将会正常确认。如果onMessage方法异常,将导致消费者要求ActiveMQ重发消息。此外需要注意,消息的重发次数是有限制的,每条消息中都会包含“redeliveryCounter”计数器,用来表示此消息已经被重发的次数,如果重发次数达到阀值,将导致broker端认为此消息无法消费,此消息将会被删除或者迁移到"dead letter"通道中。

         因此当我们使用messageListener方式消费消息时,可以在onMessage方法中使用try-catch,这样可以在处理消息出错时记录一些信息,而不是让consumer不断去重发消息;如果你没有使用try-catch,就有可能会因为异常而导致消息重复接收的问题,需要注意onMessage方法中逻辑是否能够兼容对重复消息的判断。

    实验方法:JmsMsgReliablityConsumerAsyn中onMessage方法中增加一条throw语句,出现消息重发的现象。

                /* 通过连接工厂获取连接*/
                connection = connectionFactory.createConnection();
                /* 启动连接*/
                connection.start();
                /* 创建session*/
                session = connection.createSession(true, Session.AUTO_ACKNOWLEDGE);
                /* 创建一个消息队列*/
                destination = session.createQueue("MsgReliability");
                /* 创建消息消费者*/
                messageConsumer = session.createConsumer(destination);
                /* 设置消费者监听器,监听消息*/
                messageConsumer.setMessageListener(new MessageListener() {
                    public void onMessage(Message message) {
                        try {
                            //do my work
                            System.out.println(((TextMessage) message).getText());
           
                        } catch (JMSException e) {
                            e.printStackTrace();
                        }
                        throw new RuntimeException("RuntimeException");
                    }
                });

    CLIENT_ACKNOWLEDGE :

    客户端手动确认,这就意味着AcitveMQ将不会“自作主张”的为你ACK任何消息,开发者需要自己择机确认。可以用方法: message.acknowledge(),或session.acknowledge();效果一样。

    如果忘记调用acknowledge方法,将会导致当consumer重启后,会接受到重复消息,因为对于broker而言,那些尚未真正ACK的消息被视为“未消费”。

    我们可以在当前消息处理成功之后,立即调用message.acknowledge()方法来"逐个"确认消息,这样可以尽可能的减少因网络故障而导致消息重发的个数;当然也可以处理多条消息之后,间歇性的调用acknowledge方法来一次确认多条消息,减少ack的次数来提升consumer的效率,不过需要自行权衡。

    实验方法:JmsMsgReliablityConsumerAsyn中将session模式改为Session.CLIENT_ACKNOWLEDGE启动两个消费者,发送消息后,可以看到JmsMsgReliablityConsumerAsyn接收了消息但不确认。当JmsMsgReliablityConsumerAsyn重新启动后,会再一次收到同样的消息。加入message.acknowledge()后该现象消失。

                /* 通过连接工厂获取连接*/
                connection = connectionFactory.createConnection();
                /* 启动连接*/
                connection.start();
                /* 创建session*/
                session = connection.createSession(true, Session.CLIENT_ACKNOWLEDGE);
                /* 创建一个消息队列*/
                destination = session.createQueue("MsgReliability");
                /* 创建消息消费者*/
                messageConsumer = session.createConsumer(destination);
                /* 设置消费者监听器,监听消息*/
                messageConsumer.setMessageListener(new MessageListener() {
                    public void onMessage(Message message) {
                        try {
                            //do my work
                            System.out.println(((TextMessage) message).getText());
                            message.acknowledge();
                        } catch (JMSException e) {
                            e.printStackTrace();
                        }
                    }
                });

    DUPS_OK_ACKNOWLEDGE

    类似于AUTO_ACK确认机制,为自动批量确认而生,而且具有“延迟”确认的特点,ActiveMQ会根据内部算法,在收到一定数量的消息自动进行确认。在此模式下,可能会出现重复消息,什么时候?当consumer故障重启后,那些尚未ACK的消息会重新发送过来。

    例如一次性推送6条信息,到了第4条报错了,那么前3天消费成功的就会被重复消费。

    SESSION_TRANSACTED

    当session使用事务时,就是使用此模式。当决定事务中的消息可以确认时,必须调用session.commit()方法,commit方法将会导致当前session的事务中所有消息立即被确认。在事务开始之后的任何时机调用rollback(),意味着当前事务的结束,事务中所有的消息都将被重发。当然在commit之前抛出异常,也会导致事务的rollback。

    通配符式订阅

    Wildcards 用来支持联合的名字分层体系(federated name hierarchies)。它不是JMS 规范的一部分,而是ActiveMQ 的扩展。ActiveMQ 支持以下三种

    wildcards:

    • · "." 用于作为路径上名字间的分隔符。
    • · "*" 用于匹配路径上的任何名字。
    • · ">" 用于递归地匹配任何以这个名字开始的destination。

    订阅者可以明确地指定destination 的名字来订阅消息,或者它也可以使用wildcards 来定义一个分层的模式来匹配它希望订阅的 destination。

    死信队列

    用来保存处理失败或者过期的消息。 

    出现下面情况时,消息会被重发: 

    1. 事务会话被回滚。
    2. 事务会话在提交之前关闭。
    3. 会话使用CLIENT_ACKNOWLEDGE模式,并且Session.recover()被调用。 
    4. 自动应答失败

    当一个消息被重发超过最大重发次数(缺省为6次,消费者端可以修改)时,会给broker发送一个"有毒标记“,这个消息被认为是有问题,这时broker将这个消息发送到死信队列,以便后续处理。 

    在配置文件(activemq.xml)来调整死信发送策略。

    可以单独使用死信消费者处理这些死信。

    注意,该代码中展示了如何配置重发策略。同时,重试策略属于ActiveMQ的部分,所以有部分connectionFactory,connection的声明等等不能使用接口,必须使用ActiveMQ的实现。

    代码:

    1、DlqProducer 

    package cn.enjoyedu.dlq;
    
    import org.apache.activemq.ActiveMQConnection;
    import org.apache.activemq.ActiveMQConnectionFactory;
    import org.apache.activemq.command.ActiveMQDestination;
    
    import javax.jms.JMSException;
    import javax.jms.MessageProducer;
    import javax.jms.Session;
    import javax.jms.TextMessage;
    
    public class DlqProducer {
    
        //默认连接用户名
        private static final String USERNAME = ActiveMQConnection.DEFAULT_USER;
        //默认连接密码
        private static final String PASSWORD = ActiveMQConnection.DEFAULT_PASSWORD;
        //默认连接地址
        private static final String BROKEURL = ActiveMQConnection.DEFAULT_BROKER_URL;
        //发送的消息数量
        private static final int SENDNUM = 1;
    
        public static void main(String[] args) {
            ActiveMQConnectionFactory connectionFactory;
            ActiveMQConnection connection = null;
            Session session;
            ActiveMQDestination destination;
            MessageProducer messageProducer;
            connectionFactory = new ActiveMQConnectionFactory(USERNAME,PASSWORD,BROKEURL);
    
            try {
                connection = (ActiveMQConnection) connectionFactory.createConnection();
                connection.start();
                session = connection.createSession(true,Session.AUTO_ACKNOWLEDGE);
                destination = (ActiveMQDestination) session.createQueue("TestDlq2");
                //destination = session.createTopic("HelloTopic8");
                messageProducer = session.createProducer(destination);
                for(int i=0;i<SENDNUM;i++){
                    String msg = "发送消息"+i+" "+System.currentTimeMillis();
                    TextMessage message = session.createTextMessage(msg);
                    System.out.println("发送消息:"+msg);
                    messageProducer.send(message);
                }
                session.commit();
            } catch (JMSException e) {
                e.printStackTrace();
            }finally {
                if(connection!=null){
                    try {
                        connection.close();
                    } catch (JMSException e) {
                        e.printStackTrace();
                    }
                }
            }
        }
    }

    2、DlqConsumer

    package cn.enjoyedu.dlq;
    
    import org.apache.activemq.ActiveMQConnection;
    import org.apache.activemq.ActiveMQConnectionFactory;
    import org.apache.activemq.RedeliveryPolicy;
    import org.apache.activemq.broker.region.policy.RedeliveryPolicyMap;
    import org.apache.activemq.command.ActiveMQDestination;
    
    import javax.jms.*;
    
    public class DlqConsumer {
    
        private static final String USERNAME = ActiveMQConnection.DEFAULT_USER;//默认连接用户名
        private static final String PASSWORD = ActiveMQConnection.DEFAULT_PASSWORD;//默认连接密码
        private static final String BROKEURL = ActiveMQConnection.DEFAULT_BROKER_URL;//默认连接地址
    
        public static void main(String[] args) {
            ActiveMQConnectionFactory connectionFactory;
            ActiveMQConnection connection = null;
            Connection jmsConnection;
            Session session;
            ActiveMQDestination destination;
            MessageConsumer messageConsumer;//消息的消费者
    
            //实例化连接工厂
            connectionFactory = new ActiveMQConnectionFactory(USERNAME,PASSWORD,BROKEURL);
            RedeliveryPolicy queuePolicy = new RedeliveryPolicy();
            queuePolicy.setMaximumRedeliveries(1);
    
            try {
                //通过连接工厂获取连接
                connection = (ActiveMQConnection) connectionFactory.createConnection();
                //启动连接
                connection.start();
                RedeliveryPolicyMap map = connection.getRedeliveryPolicyMap();
    
                //创建session
                session = connection.createSession(false, Session.AUTO_ACKNOWLEDGE);
                //创建一个连接HelloWorld的消息队列
                //destination = session.createTopic("TestDlq");
                destination = (ActiveMQDestination) session.createQueue("TestDlq2");
                map.put(destination, queuePolicy);
                //创建消息消费者
                messageConsumer = session.createConsumer(destination);
                messageConsumer.setMessageListener(new MessageListener() {
                    public void onMessage(Message message) {
                        try {
                            System.out.println("Accept msg : "+((TextMessage)message).getText());
                        } catch (JMSException e) {
                            e.printStackTrace();
                        }
                        throw new RuntimeException("测试DLQ抛出异常");
                    }
                });
            } catch (JMSException e) {
                e.printStackTrace();
            }
    
        }
    }

    3、ProcessDlqConsumer

    package cn.enjoyedu.dlq;
    
    import org.apache.activemq.ActiveMQConnection;
    import org.apache.activemq.ActiveMQConnectionFactory;
    
    import javax.jms.*;
    
    public class ProcessDlqConsumer {
    
        private static final String USERNAME = ActiveMQConnection.DEFAULT_USER;//默认连接用户名
        private static final String PASSWORD = ActiveMQConnection.DEFAULT_PASSWORD;//默认连接密码
        private static final String BROKEURL = ActiveMQConnection.DEFAULT_BROKER_URL;//默认连接地址
    
        public static void main(String[] args) {
            ConnectionFactory connectionFactory;//连接工厂
            Connection connection = null;//连接
    
            Session session;//会话 接受或者发送消息的线程
            Destination destination;//消息的目的地
    
            MessageConsumer messageConsumer;//消息的消费者
    
            //实例化连接工厂
            connectionFactory = new ActiveMQConnectionFactory(ProcessDlqConsumer.USERNAME, ProcessDlqConsumer.PASSWORD, ProcessDlqConsumer.BROKEURL);
    
            try {
                //通过连接工厂获取连接
                connection = connectionFactory.createConnection();
                //启动连接
                connection.start();
                //创建session
                session = connection.createSession(false, Session.AUTO_ACKNOWLEDGE);
                //创建一个连接HelloWorld的消息队列
                //destination = session.createTopic("TestDlq");
                destination = session.createQueue("DLQ.>");
    
                //创建消息消费者
                messageConsumer = session.createConsumer(destination);
                messageConsumer.setMessageListener(new MessageListener() {
                    public void onMessage(Message message) {
                        try {
                            System.out.println("Accept DEAD msg : " +((TextMessage)message).getText());
                        } catch (JMSException e) {
                            e.printStackTrace();
                        }
                    }
                });
    
    
            } catch (JMSException e) {
                e.printStackTrace();
            }
    
        }
    }

    消费者集群下需要考虑的问题

    我们现实中往往有这样的需求:

    1. 消息接收方和发送方都是集群。 

    2. 同一个消息的接收方可能有多个集群进行消息的处理。

    3. 不同集群对于同一条消息的处理不能相互干扰。

    希望可以达到如下的效果:集群A和集群B分别能完整收到所有的八条消息,并且集群里面的每个服务器不重复处理消息

    对于集群消息,采用单独采用queue或者topic都不满足要求。采用Queue模型导致:单独的queue,消息可能被其他集群消费

    采用Topic模型导致,采用topic消息可能被同一集群的相同应用重复消费。

    解决方案一,级联

    将Jms的Topic和Queue进行级联使用

    缺点,实现方式繁重,需要独立的中转的消息订阅者来完成,多了一次消息的投递和一次消息消费过程,对效率有影响,增加了消息中间件负载压力。

    解决方案二

    ActiveMQ提供了虚拟主题和组合Destinations都可以达到这个效果。

    虚拟Destination

    对于消息发布者来说,就是一个正常的Topic,名称以VirtualTopic.开头。例如VirtualTopic.vtgroup

    对于消息接收端来说,是个队列,不同应用里使用不同的前缀作为队列的名称,即可表明自己的身份即可实现消费端应用分组。

      例如Consumer.A.VirtualTopic. vtgroup,说明它是名称为A群组的消费端,同理Consumer.B.VirtualTopic. vtgroup说明是一个名称为B群组的客户端。

    默认虚拟主题的前缀是 :VirtualTopic.

     消费虚拟地址默认格式:Consumer.*.VirtualTopic.

    代码示例:

    1、生产者

    connection = connectionFactory.createConnection();
                connection.start();
    
                session = connection.createSession(true,Session.AUTO_ACKNOWLEDGE);
                //destination = session.createQueue("HelloWAM");
                destination = session.createTopic("VirtualTopic.vtgroup");
                messageProducer = session.createProducer(destination);
                for(int i=0;i<SENDNUM;i++){
                    String msg = "vtgroup "+i+" "+System.currentTimeMillis();
                    TextMessage message = session.createTextMessage(msg);
                    System.out.println("发送消息:"+msg);
                    messageProducer.send(message);
                }
                session.commit();

    2、消费者

    //通过连接工厂获取连接
                connection = connectionFactory.createConnection();
                //启动连接
                connection.start();
                //创建session
                session = connection.createSession(false, Session.AUTO_ACKNOWLEDGE);
                //创建一个连接HelloWorld的消息队列
                destination = session.createQueue("Consumer.A.VirtualTopic.vtgroup");
                //destination = session.createQueue("HelloWAM");
    
                //创建消息消费者
                messageConsumer = session.createConsumer(destination);
                messageConsumer.setMessageListener(new MessageListener() {
                    public void onMessage(Message message) {
                        try {
                            System.out.println("Accept msg : "
                                    +((TextMessage)message).getText());
                        } catch (JMSException e) {
                            e.printStackTrace();
                        }
                    }
                });

    组合Destination

    组合队列允许用一个虚拟的destination代表多个destinations。这样就可以通过composite destinations在一个操作中同时向多个destination发送消息。

    多个destination之间采用“,”分割。例如:

      Queue queue = new ActiveMQQueue("FOO.A,FOO.B,FOO.C");

      或

      Destination destination = session.createQueue("my-queue,my-queue2");

    如果希望使用不同类型的destination,那么需要加上前缀如queue:// 或topic://,例如:

        Queue queue = new ActiveMQQueue("cd.queue,topic://cd.mark");

    代码示例:

    1、生产者

               connection = connectionFactory.createConnection();
                connection.start();
    
                session = connection.createSession(true,Session.AUTO_ACKNOWLEDGE);
                destination = session.createQueue("cd.queue,topic://cd.mark");
                //destination = session.createTopic("VirtualTopic.vtgroup");
                messageProducer = session.createProducer(destination);
                for(int i=0;i<SENDNUM;i++){
                    String msg = "CdProducer "+i+" "+System.currentTimeMillis();
                    TextMessage message = session.createTextMessage(msg);
                    System.out.println("发送消息:"+msg);
                    messageProducer.send(message);
                }
                session.commit();

    2、消费者

                //通过连接工厂获取连接
                connection = connectionFactory.createConnection();
                //启动连接
                connection.start();
                //创建session
                session = connection.createSession(false, Session.AUTO_ACKNOWLEDGE);
                //创建一个连接HelloWorld的消息队列
                destination = session.createQueue("cd.queue");
                destination = session.createTopic("cd.mark");
    
                //创建消息消费者
                messageConsumer = session.createConsumer(destination);
                messageConsumer.setMessageListener(new MessageListener() {
                    public void onMessage(Message message) {
                        try {
                            System.out.println("Accept msg : "
                                    +((TextMessage)message).getText());
                        } catch (JMSException e) {
                            e.printStackTrace();
                        }
                    }
                });
  • 相关阅读:
    有关程序开发中有关验证中常用的正则表达式汇总
    python学习---logging模块
    有关递归函数,返回值的学习
    设计模式之建造者模式、模版方法
    XXL-JOB使用命令行的方式启动python时,日志过多导致阻塞的解决方式
    Spring Boot后端与Angular前端进行timestamp的交互
    设计模式之代理模式
    设计模式之工厂模式
    设计模式之单例模式
    设计模式之反射机制
  • 原文地址:https://www.cnblogs.com/alimayun/p/12791064.html
Copyright © 2020-2023  润新知