JMS消息传送机制

本文深入探讨了 Java 消息服务 (JMS) 的关键特性,包括保证消息传递的机制、事务处理以及消息编组的具体实现。通过详细的解释和示例代码,帮助读者理解如何确保消息的可靠传输,并介绍了不同确认模式的特点。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

 JMS不仅需要高效的存储消息,还需要确保消息能够无误的被传输.这就需要JMS提供一种"保证传送"机制和事务.如果抛开JMS的规范,那么它的技术实现本身就是网络IO + 文件存储;其中网络IO的困境就在"数据传输安全保证""网络失效"等方面,"文件存储"即要求数据需要被高效的存取.其中"文件存储"层面已经有较多的第三方存储工具和解决方案,比如文件类型数据库/内存数据库等.如果消息系统需要分布式且整体架构良好,那么上述问题,就更加棘手.

 

一.保证传送

    保证传送,用来描述JMS Client和JMS Provider之间的消息交互是"可靠的",这种"可靠"并非绝对意义上的百分百数据不丢失或者数据不重复,所谓"保证传送",就是在良好的API设计和相对良好的运行环境中,消息最终会被接收和消费;但是如果API设计不良或者运行环境极其糟糕,比如事务使用不正确,或者确认机制操作不当,或者网络环境糟糕以至于无法连续的通讯等,都将会导致消息的发送和接收,出现"意料之外"的事情:队列消息被多次发送,消息被重复接收等.

    


 一条消息的"来龙去脉"可以简单的通过上图表示

    1:  消息生产者中send会阻塞操作(网络IO阻塞),如果网络异常,将导致消息发送失败,此时生产者需要在编码设计上做好异常检测或者重发机制;其中网络异常主要体现在"网络中断"或者JMS Provider失效.

    2:  JMS Provider接收到消息之后,将会保存在内存中(或者DB中),此后并立即向此Producer发送"ACK"信号,如果JMS Provider在存储上遇到无法解决的异常(比如磁盘故障,嵌入式DB异常),那么"ACK"响应将会携带异常信息(message);如果"ACK"发送中网络IO异常导致无法发送,将会导致"此消息"被移除,此时send方法也将从阻塞中返回并抛出异常.因为网络IO异常,通常TCP链接的双端都能及时的感知到.

        ACK发送正常,指的是TCP连接上"ACK"数据已经通过网络发送给了Client,这个过程中没有发生异常,此后也意味着当前JMS消息被持久存储,producer的send方法也正确返回.此后如果Client端在处理ACK信息过程中出现问题(几乎不可能),那么JMS Provider也将认为此消息已经被正确发送.需要提醒的是,在Client端:消息的send和ACK在IO流上不是一个连续的过程,在不同的确认时机中,ACK信息是可以是同步的(send方法阻塞,知道收到ACK,比如持久化类型的消息),有些时异步获取的(比如事务中的消息,或者非持久化消息),不过JMS中的ACK MODE都producer并没有任何意义..

    3: 消息消费者receive消息,这个操作在Queue中是consumer主动"拉取"(监听),在Topic中是Provider推送;当消费者接收到消息之后,可以立即确认(AUTO),然后再去执行与消息有关的业务;也可以"延迟"确认等.

    4: 当消息被消费者接收到之后,JMS Provider将消息添加到此消费者的"待确认消息"列表,如果JMS Provider在此消费者的IO通道中阻塞返回,但却没有收到ACK,那么将导致此消息重发.如果ACK正常接收到,那么JMS Provider将会把此消息从持久存储设备删除.

 

    如下为JMS中ACK MODE:

 

    1. AUTO_ACKNOWLEDGE:自动确认,当Consumer客户端(通常是指Consumer所属的Session)在收到消息之后,即准备确认消息,通常在receive()方法返回之前,或者在messageListener.onMessage()正常执行之后,向Provider发送ACK指令..所谓AUTO_ACK,就是确认的时机有Session"择机"实施;开发者无法干扰.

 

    2. DUPS_OK_ACKNOWLEDGE: 允许延迟批量确认,重点在"批量",AUTO_ACK通常是确认一条消息(事实上在不同的JMS实现中,它仍然可以像DUPS一样确认多条消息)..当消费者接收到消息之后,并不是立即确认,而是"滞留"一段时间之后才会确认(批量确认);这种方式直观上是提升了client端消费数据的速度(优先接触消息),但这种模式下,需要消费者能够接受"消息重发"的情况,比如当Consumer客户端失效重启,那些尚未确认但已经被"touch"(消费)过的消息有可能会重复接受到;对于JMS Provider而言,如果它"等待"一定时间后,仍未收到"确认",将会重发消息.,这种模式,对性能的提升是不明确的,因为较晚的确认消息,意味着JMS Provider需要更长时间的保留"待确认"消息列表..究竟多少条消息作为"批量"的边界,有具体的JMS实现者决定.

 

    3. CLIENT_ACKNOWLEDGE: 客户端确认,需要要求消息消费者明确的调用message.acknowledge()方法来确认此消息的接收成功,无论何种原因(未捕获异常),如果此方法未被调用,那么此消息将会被重发.这种模式下,允许消费者自己选择时机确认消息,通常使用在消息编组(group)的情况下:将一系列消息作为一个group接收,当group中最后一个消息确认成功后,那么JMS Provider就认为此组消息全部接收成功(只需确认组的最后一条消息即可,那么JMS Provider会认为此前的其他消息也接收正常).

 

二.事务

    事务用来描述"一系列消息要么全部确认成功,要么全不确认"的特征,它和数据库事务最终需要达成的效果是一样的.JMS Provider会缓存每个生产者当前事务下的所有消息,直到commit或者rollback.commit操作将会导致事务中所有的消息被持久存储;rollback意味着JMS Provider将会清除此事务下所有的消息记录...在事务未提交之前,消息是不会被持久存储的,也不会被消费者消费.

    每次事务提交之后,在client端会生成一个事务ID(一个session中不会出现重复的ID,clientID:sessionID:txID);事务的提交或者回滚都会携带ID.对于producer而言,在事务类型的session中,发送消息(一个或者多个)之后,需要执行session.commit(),否则消息将不会被存储.对于consumer而言,消息接收到之后,需要手动的使用commit,否则JMS Provider会认为消息没有被接收,导致重发,因此你可以认为commit就是一个消息确认操作.

Java代码   收藏代码
  1. session = connection.createSession(true, Session.AUTO_ACKNOWLEDGE);  
  2. try{  
  3.     Message message = session.createTextMessage(text);  
  4.     String cid = "ID:" + System.currentTimeMillis();  
  5.     message.setJMSCorrelationID(cid);  
  6.     producer.send(message);  
  7.     session.commit();  
  8. }catch(Exception e){  
  9. //  
  10. }  

 

Java代码   收藏代码
  1. public class QueueMessageListener implements MessageListener{  
  2.   
  3.     private Session session;  
  4.     public QueueMessageListener(Session session){  
  5.         this.session = session;  
  6.     }  
  7.     public void onMessage(Message message) {  
  8.         if(message == null){  
  9.             return;  
  10.         }  
  11.         try{  
  12.             //message handler  
  13.             //  
  14.             session.commit();  
  15.         }catch(Exception e){  
  16.             e.printStackTrace();  
  17.         }  
  18.     }  
  19. }  

 

三.消息编组

    一序列消息,如果不编组的话,可能会被分发给不同的消费者;但是很多时候,我们期望这"一序列"的消息能够有序的交付给一个消费者,无论是消息的发送还是消费,都希望它们是不可分割的"组合".那么此时我们需要"消息编组",我们需要使用到"JMSXGroupID"/"JMSXGroupSeq"两个属性.注意JMSProvider并不会根据JMSXGroupSeq进行排序,顺序还是需要自己来维护.

   1.消息生产者

Java代码   收藏代码
  1. package com.test.jms.simple;  
  2.   
  3. import javax.jms.Connection;  
  4. import javax.jms.ConnectionFactory;  
  5. import javax.jms.DeliveryMode;  
  6. import javax.jms.Destination;  
  7. import javax.jms.Message;  
  8. import javax.jms.MessageProducer;  
  9. import javax.jms.Queue;  
  10. import javax.jms.QueueConnectionFactory;  
  11. import javax.jms.Session;  
  12. import javax.naming.Context;  
  13. import javax.naming.InitialContext;  
  14.   
  15. public class XGroupProducer {  
  16.   
  17.     private MessageProducer producer;  
  18.     private Session session;  
  19.     private Connection connection;  
  20.     private boolean isOpen = true;  
  21.       
  22.     public XGroupProducer() throws Exception{  
  23.         Context context = new InitialContext();  
  24.         ConnectionFactory connectionFactory = (QueueConnectionFactory)context.lookup("QueueCF");  
  25.         connection = connectionFactory.createConnection();  
  26.         session = connection.createSession(true, Session.AUTO_ACKNOWLEDGE);  
  27.         Destination queue = (Queue)context.lookup("queue1");  
  28.         producer = session.createProducer(queue);  
  29.         producer.setDeliveryMode(DeliveryMode.PERSISTENT);  
  30.         connection.start();  
  31.           
  32.     }  
  33.       
  34.       
  35.     public boolean send(String[] texts) {  
  36.         if(!isOpen){  
  37.             throw new RuntimeException("session has been closed!");  
  38.         }  
  39.         try{  
  40.             synchronized (this) {  
  41.                 String groupId = connection.getClientID() + ":Group";  
  42.                 Message bm = session.createTextMessage();  
  43.                 bm.setStringProperty("GroupMarker""begin");  
  44.                 producer.send(bm);  
  45.                 for(int i= 0 ; i < texts.length; i++) {  
  46.                     Message message = session.createTextMessage(texts[i]);  
  47.                     message.setStringProperty("JMSXGroupID", groupId);  
  48.                     message.setIntProperty("JMSXGroupSeq", i +1);  
  49.                     producer.send(message);  
  50.                 }  
  51.                 //取消group粘性  
  52.                 Message em = session.createTextMessage();  
  53.                 em.setStringProperty("JMSXGroupID", groupId);  
  54.                 em.setIntProperty("JMSXGroupSeq"0);  
  55.                 em.setStringProperty("GroupMarker""end");  
  56.                 producer.send(em);  
  57.                 session.commit();  
  58.             }  
  59.             return true;  
  60.         }catch(Exception e){  
  61.             return false;  
  62.         }  
  63.     }  
  64.       
  65.       
  66.     public synchronized void close(){  
  67.         try{  
  68.             if(isOpen){  
  69.                 isOpen = false;  
  70.             }  
  71.             session.close();  
  72.             connection.close();  
  73.         }catch (Exception e) {  
  74.             //  
  75.         }  
  76.     }  
  77.       
  78. }  

    2.消息消费者 

Java代码   收藏代码
  1. package com.test.jms.simple;  
  2.   
  3. import javax.jms.Connection;  
  4. import javax.jms.ConnectionFactory;  
  5. import javax.jms.Destination;  
  6. import javax.jms.MessageConsumer;  
  7. import javax.jms.Queue;  
  8. import javax.jms.QueueConnectionFactory;  
  9. import javax.jms.Session;  
  10. import javax.naming.Context;  
  11. import javax.naming.InitialContext;  
  12.   
  13. import com.test.jms.object.XGroupQueueMessageListener;  
  14.   
  15. public class XGroupConsumer {  
  16.   
  17.     private Connection connection;  
  18.     private Session session;  
  19.     private MessageConsumer consumer;  
  20.       
  21.     private boolean isStarted;  
  22.       
  23.     public XGroupConsumer() throws Exception{  
  24.         Context context = new InitialContext();  
  25.         ConnectionFactory connectionFactory = (QueueConnectionFactory)context.lookup("QueueCF");  
  26.         connection = connectionFactory.createConnection();  
  27.         session = connection.createSession(false, Session.AUTO_ACKNOWLEDGE);  
  28.         Destination queue = (Queue)context.lookup("queue1");  
  29.         consumer = session.createConsumer(queue);  
  30.         consumer.setMessageListener(new XGroupQueueMessageListener());  
  31.           
  32.     }  
  33.       
  34.       
  35.     public synchronized boolean start(){  
  36.         if(isStarted){  
  37.             return true;  
  38.         }  
  39.         try{  
  40.             connection.start();  
  41.             isStarted = true;  
  42.             return true;  
  43.         }catch(Exception e){  
  44.             return false;  
  45.         }  
  46.     }  
  47.       
  48.     public synchronized void close(){  
  49.         isStarted = false;  
  50.         try{  
  51.             session.close();  
  52.             connection.close();  
  53.         }catch(Exception e){  
  54.             //  
  55.         }  
  56.     }  
  57.       
  58. }  

   3.测试类 

Java代码   收藏代码
  1. public class XGroupTestMain {  
  2.   
  3.     /** 
  4.      * @param args 
  5.      */  
  6.     public static void main(String[] args) throws Exception{  
  7.         XGroupConsumer consumer = new XGroupConsumer();  
  8.         consumer.start();  
  9.         XGroupProducer producer = new XGroupProducer();  
  10.         producer.send(new String[]{"1","2","3","4"});  
  11.   
  12.     }  
  13.   
  14. }  

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值