迁移:使用消息队列异步化系统
前言
前期为了快速开发,项目结构较为混乱,代码维护与功能扩展都比较困难,为了方便后续功能开发,最近对项目进行的重构,顺便在重构的过程中将之前的部分操作进行了异步处理,也第一次实际接触了JMS与消息队列。项目中采用的消息中间件为ActiveMQ。
什么是JMS
Java消息服务(Java Message Service,JMS)应用程序接口是一个Java平台中关于面向消息中间件(MOM)的API,用于在两个应用程序之间,或分布式系统中发送消息,进行异步通信。Java消息服务是一个与具体平台无关的API,绝大多数MOM提供商都对JMS提供支持。
Java消息服务的规范包括两种消息模式,点对点和发布者/订阅者。许多提供商支持这一通用框架因此,程序员可以在他们的分布式软件中实现面向消息的操作,这些操作将具有不同面向消息中间件产品的可移植性。
Java消息服务支持同步和异步的消息处理,在某些场景下,异步消息是必要的;在其他场景下,异步消息比同步消息操作更加便利。
Java消息服务支持面向事件的方法接收消息,事件驱动的程序设计现在被广泛认为是一种富有成效的程序设计范例,程序员们都相当熟悉。
在应用系统开发时,Java消息服务可以推迟选择面对消息中间件产品,也可以在不同的面对消息中间件切换。——Wiki
什么是消息队列
在计算机科学中,消息队列(英语:Message queue)是一种进程间通信或同一进程的不同线程间的通信方式,软件的贮列用来处理一系列的输入,通常是来自使用者。消息队列提供了异步的通信协议,每一个贮列中的纪录包含详细说明的资料,包含发生的时间,输入装置的种类,以及特定的输入参数,也就是说:消息的发送者和接收者不需要同时与消息队列互交。消息会保存在队列中,直到接收者取回它。
目前,有很多消息队列有很多开源的实现,包括JBoss Messaging、JORAM、Apache ActiveMQ、Sun Open Message Queue、Apache Qpid和HTTPSQS。
消息队列本身是异步的,它允许接收者在消息发送很长时间后再取回消息,这和大多数通信协议是不同的。例如WWW中使用的HTTP协议是同步的,因为客户端在发出请求后必须等待服务器回应。然而,很多情况下我们需要异步的通信协议。比如,一个进程通知另一个进程发生了一个事件,但不需要等待回应。但消息队列的异步特点,也造成了一个缺点,就是接收者必须轮询消息队列,才能收到最近的消息。
和信号相比,消息队列能够传递更多的信息。与管道相比,消息队列提供了有格式的数据,这可以减少开发人员的工作量。但消息队列仍然有大小限制。——Wiki
正文
基本类图结构如下:
说明:
AsyncWork:消息的处理类接口,定义各类型的消息的处理方式
AsyncWorkProducer:消息的生产者(JMS生产者),负责向消息队列里面放入消息
AsyncWorkConsumer:消息的消费者(JMS消费者),负责从消息队列中消费消息
AsyncWorkFactory:对外提供的服务的工厂类
EmailWork、PushNotificationWork、LoginLogWork...:实现AsyncWork接口,定义消息的具体处理方式
代码片段
1 public class AsyncWorkProducer { 2 3 //ConnectionFactory :连接工厂,JMS 用它创建连接 4 private ConnectionFactory connectionFactory; 5 6 private String queueName = "QueueName"; 7 8 public AsyncWorkProducer(String queueName){ 9 this.queueName = queueName; 10 init(); 11 } 12 13 private void init(){ 14 connectionFactory = new ActiveMQConnectionFactory( 15 ActiveMQConnection.DEFAULT_USER, 16 ActiveMQConnection.DEFAULT_PASSWORD, 17 SystemConfiguration.getString("asyc.location")); 18 } 19 20 public void sendMessage(Message message){ 21 Connection connection = null; 22 try { 23 // Connection :JMS 客户端到JMS Provider 的连接 | 构造ConnectionFactory实例对象,此处采用ActiveMq的实现jar 24 connection = connectionFactory.createConnection(); 25 //启动 26 connection.start(); 27 // Session: 一个发送或接收消息的线程 | 获取操作连接 28 Session session = connection.createSession(false,Session.AUTO_ACKNOWLEDGE); 29 // Destination :消息的目的地;消息发送给谁. 30 Destination destination = session.createQueue(queueName); 31 // MessageProducer:消息发送者 |得到消息生成者【发送者】 32 MessageProducer producer = session.createProducer(destination); 33 //设置不持久化,实际根据项目决定 34 producer.setDeliveryMode(DeliveryMode.NON_PERSISTENT); 35 // 发送消息到目的地方 36 producer.send(message); 37 } catch (Exception e) { 38 e.printStackTrace(); 39 }finally{ 40 try { 41 if (null != connection){ 42 connection.close(); 43 } 44 } catch (Throwable ignore) { 45 } 46 } 47 } 48 }
服务工厂类,貌似作用不大:
1 public class AsyncWorkFactory { 2 3 private static ConcurrentHashMap<String, AsyncWorkProducer> chm = new ConcurrentHashMap<String, AsyncWorkProducer>(); 4 5 private AsyncWorkFactory(){} 6 7 public static AsyncWorkProducer getProducer(String queueName){ 8 AsyncWorkProducer awp = chm.get(queueName); 9 if(awp==null){ 10 awp = new AsyncWorkProducer(queueName); 11 chm.put(queueName, awp); 12 } 13 14 return awp; 15 } 16 17 public static void sendMessage(Message message,String queueName){ 18 getProducer(queueName).sendMessage(message); 19 } 20 21 }
线程监听:
1 public class AsyncWorkConsumer implements Runnable{ 2 // ConnectionFactory :连接工厂,JMS 用它创建连接 3 private ConnectionFactory connectionFactory; 4 private AsycWork work; 5 6 private String queueName = "QueueName"; 7 8 public AsyncWorkConsumer(String queueName,AsycWork work){ 9 this.queueName = queueName; 10 this.work = work; 11 init(); 12 } 13 14 private void init(){ 15 connectionFactory = new ActiveMQConnectionFactory( 16 ActiveMQConnection.DEFAULT_USER, 17 ActiveMQConnection.DEFAULT_PASSWORD, 18 SystemConfiguration.getString("asyc.location")); 19 } 20 21 @Override 22 public void run() { 23 24 Connection connection = null; 25 try { 26 // Connection :JMS 客户端到JMS Provider 的连接 | 构造ConnectionFactory实例对象,此处采用ActiveMq的实现jar 27 connection = connectionFactory.createConnection(); 28 connection.start(); 29 // Session: 一个发送或接收消息的线程 | 获取操作连接 30 Session session = connection.createSession(false,Session.AUTO_ACKNOWLEDGE); 31 // Destination :消息的目的地;消息发送给谁. 32 Destination destination = session.createQueue(queueName); 33 // MessageProducer:消息发送者 |得到消息生成者【发送者】 34 MessageConsumer consumer = session.createConsumer(destination); 35 //设置不持久化,实际根据项目决定 36 while (true) { 37 //可设置接收者接收消息的时间 consumer.recevie(xxx) 38 Message message = consumer.receive(); 39 work.execute(message); 40 } 41 } catch (Exception e) { 42 e.printStackTrace(); 43 }finally{ 44 try { 45 if (null != connection){ 46 connection.close(); 47 } 48 } catch (Throwable ignore) { 49 } 50 } 51 } 52 }
回调处理:
1 public class EmailWorker implements AsycWork { 2 3 private static Logger log = LoggerFactory.getLogger(EmailWorker.class); 4 5 @Override 6 public void execute(Message message) { 7 8 ActiveMQMapMessage msg = (ActiveMQMapMessage) message; 9 try { 10 String address = msg.getString("address"); 11 String title = msg.getString("title"); 12 String content = msg.getString("content"); 13 14 Constants.sendMail(address, title, content); 15 } catch (JMSException e) { 16 log.error("异步邮件发送异常", e); 17 } 18 } 19 20 }
项目启动时执行如下代码启动线程:
1 Thread emailThread = new Thread(new AsyncWorkConsumer(AsycWork.EMAIL,emailWorker)); 2 emailThread.setDaemon(true); 3 emailThread.start(); 4 5 //启动线程绑定各种回调 6 Thread normalLogThread = new Thread(new AsyncWorkConsumer(AsycWork.NORMAL_LOG,normalLogWork)); 7 normalLogThread.setDaemon(true); 8 normalLogThread.start(); 9 10 Thread loginLogThread = new Thread(new AsyncWorkConsumer(AsycWork.LOGIN_LOG,loginLogWorker)); 11 loginLogThread.setDaemon(true); 12 loginLogThread.start();
调用异步的工具类:
1 public class AsyncUtils { 2 3 private static Logger log = LoggerFactory.getLogger(AsyncUtils.class); 4 5 public static void log(String type,String operate){ 6 7 if(!SystemConfigFromDB.getBoolean(SystemConfigFromDB.NEED_NORMAL_LOG)){ 8 return; 9 } 10 11 try{ 12 User user = (User) SecurityUtils.getSubject().getSession().getAttribute("loginUser"); 13 if(user==null){ 14 return; 15 } 16 17 OperateLog log = new OperateLog(user.getId(), user.getName(), operate,type, user.getLastLoginIp()); 18 ActiveMQObjectMessage message = new ActiveMQObjectMessage(); 19 message.setObject(log); 20 AsyncWorkFactory.sendMessage(message, AsycWork.NORMAL_LOG); 21 22 }catch (Exception e) { 23 log.error("日志记录出错!", e); 24 } 25 } 26 27 public static void sendMail(String address,String title,String content){ 28 if(!SystemConfigFromDB.getBoolean(SystemConfigFromDB.NEED_SEND_MAIL)){ 29 return; 30 } 31 32 try{ 33 ActiveMQMapMessage message = new ActiveMQMapMessage(); 34 message.setString("address", address); 35 message.setString("title", title); 36 message.setString("content", content); 37 38 AsyncWorkFactory.sendMessage(message, AsycWork.EMAIL); 39 40 }catch (Exception e) { 41 log.error("邮件发送出错!",e); 42 } 43 } 44 45 public static void loginLog(String uid,String ip,Date date){ 46 if(!SystemConfigFromDB.getBoolean(SystemConfigFromDB.NEED_LOG_CLIENTUSER_LOGINLOG)){ 47 return; 48 } 49 try{ 50 ActiveMQMapMessage message = new ActiveMQMapMessage(); 51 message.setString("uid", uid); 52 message.setString("ip", ip); 53 message.setString("date", DateUtils.formatDateTime(date, "yyyy-MM-dd HH:mm:ss")); 54 55 AsyncWorkFactory.sendMessage(message, AsycWork.LOGIN_LOG); 56 57 }catch (Exception e) { 58 log.error("邮件发送出错!",e); 59 } 60 } 61 }
在需要异步处理的地方执行类似如下代码:
AsyncUtils.sendMail("xxx@xxx.com", "邮件标题", "邮件内容");//异步发送邮件
这样就可以执行异步操作了。
适用于
异步系统适用于与主要业务逻辑无关的较耗时或不需要同步操作的,失败时不影响主业务逻辑的功能点:
比如:1、在用户注册的时候记录数据做后期统计、发送注册成功邮件等
2、系统操作的日志记录
3、iOS消息推送
4、发送短信
...
在使用异步系统之前,用户注册与注册日志记录是在同一个事务完成的,用户注册失败则不会记录日志,但同时,日志记录发生异常也会引起用户注册失败,日志记录本身是与用户注册这个逻辑不相关的工作,在日志发生异常的时候不应该使用户注册失败。
在使用异步系统之后,用户注册逻辑执行结束后,调用异步的注册日志记录与异步的注册邮件发送功能即可,不用等待日志记录与邮件发送的返回,即可直接返回用户注册成功。将日志与邮件异步处理,既提高了响应速度也使逻辑更加严谨。在发生异常的时候,消息队列会将消息继续保留,留待后续处理。
PS:本文的实现方式大部分为自己摸索的,之前没有接触过类似的模块,所以有些地方都是按照自己的理解处理的,通用的异步系统是不是这种结构本人不是太了解,欢迎交流。
后面会介绍一下最新的实现方式,修改为了基于Spring管理的异步系统,将ActiveMQ丢给了Spring,依靠Spring发送与监听消息,相比这个可能会更靠谱一点。