在当今互联网和电商盛行的情况下,网站的性能受到了极大地挑战。大数据,高并发成为大型网站的标志。无论淘宝的双11优惠,还是小米抢购,它们都有一个共同的特点,那就是在短时间内,突然涌入超出平时数倍的用户。
如果每个用户从请求,到订单处理,再到响应返回均在一个请求中同步处理的话,用户的响应时间将会随着并发量的提高越来越久,直到最后服务器崩溃。在这种情况下,可以使用JMS消息队列,异步处理订单。用户发出请求,服务器接收请求以后,向消息队列中发送一个消息,就立刻返回“订单正处理”的消息给用户。而订单处理服务器可以不停的从消息队列中取出消息,按照自己的节奏进行处理。这就像生产者-消费者模式一样。通过这种异步的处理方式,用户响应时间得到缩减,服务器的压力也可以被时间分担,从而避过洪峰期。
在之前的文章JavaMail发送google email中,我使用的是同步方式发送email,可以一个用户的响应有多慢。今天我将使用JMS的方式,改进邮件发送系统。
JMS
首先还是要简单的介绍下JMS(Java Messaging Service)。太基础的就不再唠叨了,这里只列出JMS的两种消息模式:
-
PTP模式
PTP模式中,消息是Queue的形式从一端到另一端。无论client2是否连接,运行中,JMS的可靠性使得Msg都不会丢失。当client2恢复运行时,Queue会继续传输。Queue的两端可以有多个clients,但是每一个消息,只能被一个consumer client消费。所以,对于消费客户端们而言,也属于争抢式消费。

-
Pub/Sub模式
在发布订阅的模式中,则是以topic,subscription,client的方式。所有的订阅者均可受到消息,一个消息会被重复的发送给不同的消费者。

一个topic下面可以挂很多的subscription,但是这些subscription只有4种类型,这4中类型如下图:

Subscription:只能有一个client,当client断开连接,subscription则自动销毁。
Durable Subscription:只能有一个client,当client断开连接后,Message会被存在subscription中,一旦client重新连接,则继续发送消息。
Shared Subscription:可以有多个client同时挂在一个subscription上,这样可以有多个client并行的处理此Subscription下的消息。记住,消息只能被其中一个client所消费,不能被多个client同时消费。
Shared Durable Subscription:区别在durable上,即client断线,subscription会继续存在。
PTP适合单一消息类型,单一消费者类型。而Pub/Sub适合多种类消息,和多种消费者类型。
此外,消息可以是可持久化的,也可以是非持久化的。持久化的消息将被写入硬盘,当MQ server重启后,消息不会丢失。
消息的消费方式也存在两种模式:
-
JMS client API调用。此种方式的缺点在于,需要自己维护多线程。
-
MDB(Message Driven Bean)。使用EJB的方式,可以由EJB容器帮忙管理多线程。其中MDB是多实例的,每个消息过来都是一个新的线程。
JMS服务器
JMS是针对java来讲的,一般,称为MQ server。MQ Server不只针对Java,还针对C,C++,PHP,C#.... 非常多的平台。当前最火的是Apache ActiveMQ,也有使用JBoss Messaging的。但本文将使用Oracle的Glassfish Open MQ。
Open MQ是Oracle Glassfish下的一款MQ server,它是第一个实现了JMS2.0标准的MQ server。它可以单独使用,搭建集群,也可以内嵌到Glassfish中使用。在Glassfish4中,OpenMQ已经集成在Glassfish4中。Glassfish4可以使用内嵌的OpenMQ来满足小型网站的异步消息处理,也可以使用外部的OpemMQ集群来满足大型网站的异步消息处理。
有人会很纠结,ActiveMQ,OpenMQ到底哪一个好?其实我觉得每一款软件都有各自的特点,它们均能支撑起一个大型网站的架构,问题在于你如何使用它们。Glassfish是一个缩小型的WebLogic,轻巧易用。OpenMQ支持JMS2.0,易集成于Glassfish,又为Oracle支持的开源项目,其能力一样强悍。唯一的缺点是,集成于Glassfish中的OpenMQ不支持C客户端。
接下来,我将使用JMS消息队列来优化邮件发送系统。
小型网站的邮件发送系统
必备软件
Glassfish4,版本4是必须的,因为4中集成了OpenMQ,我们可以直接在Glassfish4中使用本地JMS服务。针对大型网站的远程JMS服务,我们将在下一篇文章中实现。
改造方案
本文将尝试两种改造方案:
-
JMS client API + threadPool,自己在后台启动线程监听,并管理多线程。
-
MDB, EJB容器进行Message监听,并帮助管理多线程。
**应该使用多少线程?**线程的数量与CPU的数量和IO阻塞时间有关系。如果线程没有任何IO阻塞,那么,线程数量应该和CPU数量相同。因为多余的线程需要等待CPU。如果存在IO阻塞,则需要多余CPU数量的线程,一个线程阻塞在IO上的时候,CPU不至于空闲,可以去执行其它线程。仔细推导,可以能通过IO阻塞时间跟运行时间的比例,可以计算出所需线程的数量。
显然,方案二是最明智的做法。家下来,开始实现方案二。
配置JMS Resources
打开Glassfish管理页面http://localhost:4848
-
配置JMS ConnectionFactory。可以使用glassfish默认的。

-
配置Destination Resources. 展开Resources->JMS Resources->Destination Resources. 创建一个Queue resource。

配置Java Mail Session
请在上一篇文章中http://my.oschina.net/xpbug/blog/263974#OSC_h2_3 找到Gmail的配置方法。
创建一个Web项目
创建一个名为sample的web项目。
创建MDB
创建接收JMS消息的MDB,我们使用简单的text message。email地址在message体中。MDB会向地址中发送一个简单邮件。
1package com.mycompany; 2 3import java.util.logging.Level; 4import java.util.logging.Logger; 5import javax.ejb.ActivationConfigProperty; 6import javax.ejb.MessageDriven; 7import javax.jms.JMSException; 8import javax.jms.Message; 9import javax.jms.MessageListener; 10import javax.mail.Address; 11import javax.mail.MessagingException; 12import javax.mail.Session; 13import javax.mail.Transport; 14import javax.mail.internet.InternetAddress; 15import javax.mail.internet.MimeMessage; 16import javax.naming.Context; 17import javax.naming.InitialContext; 18import javax.naming.NamingException; 19 20/** 21 * 22 * @author none2 23 */ 24@MessageDriven(activationConfig = { 25 @ActivationConfigProperty(propertyName = "destinationLookup", propertyValue = "jms/myQueue"), 26 @ActivationConfigProperty(propertyName = "destinationType", propertyValue = "javax.jms.Queue") 27}) 28public class EmailMessageBean implements MessageListener { 29 private final Session mySession; 30 31 public EmailMessageBean() throws NamingException { 32 Context initCtx = new InitialContext(); 33 mySession = (Session) initCtx.lookup("mail/mySession"); 34 } 35 36 @Override 37 public void onMessage(Message message) { 38 try { 39 String address = message.getBody(String.class); 40 javax.mail.Message mail = new MimeMessage(mySession); 41 mail.setFrom(new InternetAddress("joey.zhangpeng@gmail.com")); 42 Address toAddress = new InternetAddress(address); 43 mail.addRecipient(javax.mail.Message.RecipientType.TO, toAddress); 44 mail.setSubject("Hello"); 45 mail.setText("A notification."); 46 Transport.send(mail); 47 } catch (MessagingException | JMSException ex) { 48 Logger.getLogger(EmailMessageBean.class.getName()).log(Level.SEVERE, null, ex); 49 } 50 } 51}
index.html
创建网站的默认页面
1<html> 2 <head> 3 <title>TODO supply a title</title> 4 <meta charset="UTF-8"> 5 <meta name="viewport" content="width=device-width, initial-scale=1.0"> 6 </head> 7 <body> 8 <form method="post" action="/sample/NotifyServlet"> 9 Email:<input name="email" value=""/> 10 <input type="submit" value="Buy" name="submit"/> 11 </form> 12 </body> 13</html>
创建NotifyServlet
在servlet中,使用jms client,发送jms消息到queue中。
1package com.mycompany; 2 3import java.io.IOException; 4import java.io.PrintWriter; 5import javax.annotation.Resource; 6import javax.jms.ConnectionFactory; 7import javax.jms.JMSContext; 8import javax.jms.Queue; 9import javax.servlet.ServletException; 10import javax.servlet.annotation.WebServlet; 11import javax.servlet.http.HttpServlet; 12import javax.servlet.http.HttpServletRequest; 13import javax.servlet.http.HttpServletResponse; 14 15/** 16 * 17 * @author none2 18 */ 19@WebServlet(name = "NotifyServlet", urlPatterns = {"/NotifyServlet"}) 20public class NotifyServlet extends HttpServlet { 21 @Resource(lookup = "java:comp/DefaultJMSConnectionFactory") 22 private ConnectionFactory connectionFactory; 23 24 @Resource(lookup = "jms/myQueue") 25 private Queue queue; 26 27 @Override 28 protected void doPost(HttpServletRequest request, HttpServletResponse response) 29 throws ServletException, IOException { 30 String email = request.getParameter("email"); 31 try (JMSContext context = connectionFactory.createContext();) { 32 context.createProducer().send(queue, email); 33 } 34 response.setContentType("text/html;charset=UTF-8"); 35 try (PrintWriter out = response.getWriter()) { 36 /* TODO output your page here. You may use following sample code. */ 37 out.println("<!DOCTYPE html>"); 38 out.println("<html>"); 39 out.println("<head>"); 40 out.println("<title>Servlet NotifyServlet</title>"); 41 out.println("</head>"); 42 out.println("<body>"); 43 out.println("<h1>You have send a notification to " + email + "</h1>"); 44 out.println("</body>"); 45 out.println("</html>"); 46 } 47 } 48}
最后,进行项目打包并部署。访问http://localhost:8080/sample/ 来测试一下。