文档章节

JMS-使用消息队列优化网站性能

济远
 济远
发布于 2017/02/12 09:10
字数 1940
阅读 5
收藏 0
点赞 0
评论 0

在当今互联网和电商盛行的情况下,网站的性能受到了极大地挑战。大数据,高并发成为大型网站的标志。无论淘宝的双11优惠,还是小米抢购,它们都有一个共同的特点,那就是在短时间内,突然涌入超出平时数倍的用户。

如果每个用户从请求,到订单处理,再到响应返回均在一个请求中同步处理的话,用户的响应时间将会随着并发量的提高越来越久,直到最后服务器崩溃。在这种情况下,可以使用JMS消息队列,异步处理订单。用户发出请求,服务器接收请求以后,向消息队列中发送一个消息,就立刻返回“订单正处理”的消息给用户。而订单处理服务器可以不停的从消息队列中取出消息,按照自己的节奏进行处理。这就像生产者-消费者模式一样。通过这种异步的处理方式,用户响应时间得到缩减,服务器的压力也可以被时间分担,从而避过洪峰期。

在之前的文章 JavaMail发送google email 中,我使用的是同步方式发送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  

  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  找到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;

/**
 *
 */
@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;

/**
 *
 */
@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/  来测试一下。

后续,使用远程MQ server增加伸缩性

本文转载自:

共有 人打赏支持
济远
粉丝 0
博文 13
码字总数 13858
作品 0
南京
程序员

暂无文章

解决dokuwiki创建中文词条文件乱码问题

若直接创建中文词条,打开本地文件夹\dokuwiki\data\pages你会发现,中文字段显示的是URL乱码,需要改一下utf8格式,方法如下:(linux系统亲测有效) 打开 .dokuwiki\conf\local.php 添加一行...

Rhymo-Wu
5分钟前
0
0
设置圆角长条progressbar背景色

1、首先在Drawable下面新建一个xml文件,将这段代码复制进去 <?xml version="1.0" encoding="utf-8"?><layer-list xmlns:android="http://schemas.android.com/apk/res/android"> <!-......

王先森oO
5分钟前
0
0
Java语言学习(九):异常处理

异常是程序中的一些错误,但并不是所有的错误都是异常,并且错误有时候是可以避免的。常见的三种异常类型有: 检查性异常,如打开一个不存在的文件 运行时异常,如数组越界 错误,如栈溢出 ...

海岸线的曙光
8分钟前
0
0
深入分析golang多值返回以及闭包的实现

一、前言 golang有很多新颖的特性,不知道大家的使用的时候,有没想过,这些特性是如何实现的?当然你可能会说,不了解这些特性好像也不影响自己使用golang,你说的也有道理,但是,多了解底...

万建宁
9分钟前
0
0
img与background-image之间的区别

1.img <img src="图片来源" alt="图片无法显示时显示图片说明性文字" style="设置样式属性" /> img标签虽然不是块状元素,但是可以设置宽高,占位, img设置width后height会自适应匹配,如果...

爱喝水的小熊
11分钟前
0
0
Swift - 添加提示音

func createSound() { //建立的SystemSoundID对象 var soundID:SystemSoundID = 123 //获取声音地址 let path = Bundle.main.path(forResource: "3quan......

west_zll
13分钟前
0
0
为图片写水印的时候中文乱码

缘由:源代码在本地win7 操作系统添加水印正常,但在linux 7.4 上 添加水印乱码(空心方格) 问题的本质是在linux 操作系统中没有对Font 类支持的字体,才会出现乱码 问题截图: 1.系统linux...

qimh
13分钟前
0
0
微信小游戏子域和主域

1、主域只能够设置自身的敏感属性值 2、子域只能够读取自身、朋友、群友的敏感属性值

微信小程序-暗潮
13分钟前
0
0
Django时区详解

引言 相信使用Django的各位开发者在存储时间的时候经常会遇到这样子的错误: RuntimeWarning: DateTimeField received a naive datetime while time zone support is active. 这个错误到底...

bobway
19分钟前
0
0
改造工程步骤

背景: 对于存在有问题的项目(包括 代码不规范 数据库表命名不规范 )需要改造 步骤: 1 新建工程 : 将需要改造的项目拷贝一份 修改项目名称 2 将相应的表结构拷贝到新的数据库中 修改不直...

猿神出窍
25分钟前
0
0

没有更多内容

加载失败,请刷新页面

加载更多

下一页

返回顶部
顶部