[JMS-使用消息隊列優化網站性能](http://www.tuicool.com/articles/QfEri2)
[TOC=1,3]
在當今互聯網和電商盛行的情況下,網站的性能受到了極大地挑戰。大數據,高并發成為大型網站的標志。無論淘寶的雙11優惠,還是小米搶購,它們都有一個共同的特點,那就是在短時間內,突然涌入超出平時數倍的用戶。
如果每個用戶從請求,到訂單處理,再到響應返回均在一個請求中同步處理的話,用戶的響應時間將會隨著并發量的提高越來越久,直到最后服務器崩潰。在這種情況下,可以使用JMS消息隊列,異步處理訂單。用戶發出請求,服務器接收請求以后,向消息隊列中發送一個消息,就立刻返回“訂單正處理”的消息給用戶。而訂單處理服務器可以不停的從消息隊列中取出消息,按照自己的節奏進行處理。這就像生產者-消費者模式一樣。通過這種異步的處理方式,用戶響應時間得到縮減,服務器的壓力也可以被時間分擔,從而避過洪峰期。
在之前的文章?[JavaMail發送google email](http://my.oschina.net/xpbug/blog/263974)?中,我使用的是同步方式發送email,可以一個用戶的響應有多慢。今天我將使用JMS的方式,改進郵件發送系統。
## JMS
首先還是要簡單的介紹下JMS(Java Messaging Service)。太基礎的就不再嘮叨了,這里只列出JMS的兩種消息模式:
1. PTP模式
PTP模式中,消息是Queue的形式從一端到另一端。無論client2是否連接,運行中,?JMS的可靠性使得?Msg都不會丟失。當client2恢復運行時,Queue會繼續傳輸。Queue的兩端可以有多個clients,但是每一個消息,只能被一個consumer client消費。所以,對于消費客戶端們而言,也屬于爭搶式消費。

2. 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重啟后,消息不會丟失。
消息的消費方式也存在兩種模式:
1. JMS client API調用。此種方式的缺點在于,需要自己維護多線程。
2. MDB(Message Driven Bean)。使用EJB的方式,可以由EJB容器幫忙管理多線程。其中MDB是多實例的,每個消息過來都是一個新的線程。
## JMS服務器
一般,稱為MQ server。當前最火的是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服務,我們將在下一篇文章中實現。
### 改造方案
本文將嘗試兩種改造方案:
1. JMS client API + threadPool,自己在后臺啟動線程監聽,并管理多線程。
2. MDB, EJB容器進行Message監聽,并幫助管理多線程。
**應該使用多少線程?**?線程的數量與CPU的數量和IO阻塞時間有關系。如果線程沒有任何IO阻塞,那么,線程數量應該和CPU數量相同。因為多余的線程需要等待CPU。如果存在IO阻塞,則需要多余CPU數量的線程,一個線程阻塞在IO上的時候,CPU不至于空閑,可以去執行其它線程。仔細推導,可以能通過IO阻塞時間跟運行時間的比例,可以計算出所需線程的數量?。
顯然,方案二是最明智的做法。家下來,開始實現方案二。
### 配置JMS Resources?
打開Glassfish管理頁面?[http://localhost:4848](http://localhost:4848/)??
1. 配置JMS ConnectionFactory。可以使用glassfish默認的。

2. 配置Destination Resources. 展開Resources->JMS Resources->Destination Resources. 創建一個Queue resource。

### 配置Java Mail Session
請在上一篇文章中?[http://my.oschina.net/xpbug/blog/263974#OSC_h2_3](http://my.oschina.net/xpbug/blog/263974#OSC_h2_3)??找到Gmail的配置方法。
### 創建一個Web項目
創建一個名為sample的web項目。?
### 創建MDB
創建接收JMS消息的MDB,我們使用簡單的text message。email地址在message體中。MDB會向地址中發送一個簡單郵件。
~~~
package com.mycompany;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.ejb.ActivationConfigProperty;
import javax.ejb.MessageDriven;
import javax.jms.JMSException;
import javax.jms.Message;
import javax.jms.MessageListener;
import javax.mail.Address;
import javax.mail.MessagingException;
import javax.mail.Session;
import javax.mail.Transport;
import javax.mail.internet.InternetAddress;
import javax.mail.internet.MimeMessage;
import javax.naming.Context;
import javax.naming.InitialContext;
import javax.naming.NamingException;
/**
*
* @author none2
*/
@MessageDriven(activationConfig = {
@ActivationConfigProperty(propertyName = "destinationLookup", propertyValue = "jms/myQueue"),
@ActivationConfigProperty(propertyName = "destinationType", propertyValue = "javax.jms.Queue")
})
public class EmailMessageBean implements MessageListener {
private final Session mySession;
public EmailMessageBean() throws NamingException {
Context initCtx = new InitialContext();
mySession = (Session) initCtx.lookup("mail/mySession");
}
@Override
public void onMessage(Message message) {
try {
String address = message.getBody(String.class);
javax.mail.Message mail = new MimeMessage(mySession);
mail.setFrom(new InternetAddress("joey.zhangpeng@gmail.com"));
Address toAddress = new InternetAddress(address);
mail.addRecipient(javax.mail.Message.RecipientType.TO, toAddress);
mail.setSubject("Hello");
mail.setText("A notification.");
Transport.send(mail);
} catch (MessagingException | JMSException ex) {
Logger.getLogger(EmailMessageBean.class.getName()).log(Level.SEVERE, null, ex);
}
}
}
~~~
### index.html
創建網站的默認頁面
~~~
<html>
<head>
<title>TODO supply a title</title>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
</head>
<body>
<form method="post" action="/sample/NotifyServlet">
Email:<input name="email" value=""/>
<input type="submit" value="Buy" name="submit"/>
</form>
</body>
</html>
~~~
### 創建NotifyServlet
在servlet中,使用jms client,發送jms消息到queue中。
~~~
package com.mycompany;
import java.io.IOException;
import java.io.PrintWriter;
import javax.annotation.Resource;
import javax.jms.ConnectionFactory;
import javax.jms.JMSContext;
import javax.jms.Queue;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
/**
*
* @author none2
*/
@WebServlet(name = "NotifyServlet", urlPatterns = {"/NotifyServlet"})
public class NotifyServlet extends HttpServlet {
@Resource(lookup = "java:comp/DefaultJMSConnectionFactory")
private ConnectionFactory connectionFactory;
@Resource(lookup = "jms/myQueue")
private Queue queue;
@Override
protected void doPost(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
String email = request.getParameter("email");
try (JMSContext context = connectionFactory.createContext();) {
context.createProducer().send(queue, email);
}
response.setContentType("text/html;charset=UTF-8");
try (PrintWriter out = response.getWriter()) {
/* TODO output your page here. You may use following sample code. */
out.println("<!DOCTYPE html>");
out.println("<html>");
out.println("<head>");
out.println("<title>Servlet NotifyServlet</title>");
out.println("</head>");
out.println("<body>");
out.println("<h1>You have send a notification to " + email + "</h1>");
out.println("</body>");
out.println("</html>");
}
}
}
~~~
最后,進行項目打包并部署。訪問?[http://localhost:8080/sample/](http://localhost:8080/sample/)??來測試一下。
## 后續,使用遠程MQ server增加伸縮性
- 誰能舉個通俗易懂的例子告訴我IAAS,SAAS,PAAS的區別?
- 服務器與容器
- 常見NIO框架
- Nginx/Apache 和Apache Tomcat 的區別
- tomcat結合nginx使用小結
- java nio框架netty 與tomcat的關系
- Nginx、Lighttpd與Apache的區別
- Apache vs Lighttpd vs Nginx對比
- 數據庫
- mybatis
- MyBatis傳入多個參數的問題
- MS
- JMS(Java消息服務)入門教程
- ActiveMQ
- JMS簡介與ActiveMQ實戰
- JMS-使用消息隊列優化網站性能
- 深入淺出JMS(一)--JMS基本概念
- 深入淺出JMS(二)--ActiveMQ簡單介紹以及安裝
- 深入淺出JMS(三)--ActiveMQ簡單的HelloWorld實例
- RabbitMq、ActiveMq、ZeroMq、kafka之間的比較,資料匯總
- kafka
- zookeeper
- 集群與負載
- 單機到分布式集群
- 日志
- 從Log4j遷移到LogBack的理由
- 角色權限
- shiro
- Shiro的認證和權限控制
- Spring 整合 Apache Shiro 實現各等級的權限管理
- 安全
- basic
- Servlet、Filter、Listener深入理解
- filter與servlet的比較
- Servlet Filter