ActiveMQ
消息队列中间件是分布式系统中重要的组件,主要解决应用耦合,异步消息,流量削锋等问题。实现高性能,高可用,可伸缩和最终一致性架构。是大型分布式系统不可缺少的中间件。消息形式支持点对点和订阅-发布
- ActiveMQ是消息队列技术,为解决高并发问题而生
- ActiveMQ生产者消费者模型(生产者和消费者可以跨平台、跨系统)
- ActiveMQ支持如下两种消息传输方式
什么是中间件(MOM)
由于业务、结构和技术是不断变化的,因此为其服务的软件系统就涉及到了整合。在整合、添加服务或扩展可用服务之后,就要考虑服务间消息传递的成本。所以需要增加一些组件去提供一个允许它们进行通信(不考虑它们之间的差异)的层。该层被称作中间件。
MOM的基本功能:
将信息以消息的形式,从一个应用程序传输到另一个或者多个应用程序。
mMOM主要特点:
消息异步接受:类似于手机短信的行为,消息发送者不需要等待消息接受者的响应,减少软件多系统集成的耦合度;
消息可靠接受:确保消息在消息中间件可靠保存,只要接受方接受到消息后才可删除,多个消息也可以组成原子事务
JMS
JMS即Java消息服务(Java Message
Service)应用程序接口,是一个Java平台中关于面向消息中间件(MOM)的API,用于在两个应用程序之间,或分布式系统中发送消息,进行异步通信。Java消息服务是一个与具体平台无关的API,绝大多数MOM提供商都对JMS提供支持。
消息传递模式
关键接口:
- ConnectionFactory:用于创建连接到消息中间件的连接工厂。
- Connection:代表了应用程序和服务之间的连接通路。
- Destination:指消息发布的地点,包括队列模式和主体模式。
- Session:表示一个单线程的上下文,用于发送和接受消息。
- MessageConsumer:由会话创建,用于接受发送到目的的消息。
- MessageProducer:由会话创建,用于发送消息。
- Message:是在消费者和生产者之间传递的对象,消息头,一组消息属性,和一个消息体。
MQ(消息队列)
MQ全称为Message Queue,消息队列(MQ)是正确而又完整的 JMS 实现,消息队列(MQ)是一种应用程序对应用程序的通信方法。应用程序通过写和检索出入列队的针对应用程序的数据(消息)来通信,而无需专用连接来链接它们。消息传递指的是程序之间通过在消息中发送数据进行通信,而不是通过直接调用彼此来通信,直接调用通常是用于诸如远程过程调用的技术。
- Producer:消息生产者,负责产生和发送消息到 Broker;
- Broker:消息处理中心。负责消息存储、确认、重试等,一般其中会包含多个 queue;
- Consumer:消息消费者,负责从 Broker 中获取消息,并进行相应处理;
应用场景:
小明去书店买三本书,然后去吃饭
同步处理的情况下:小明要一本一本的买
消息队列:小明告诉店员,他要哪三本书,然后等他吃完饭在回来拿就好
则店员就是消息队列,只需要告诉他你要做的事情,放进队列,然后处理就好
解耦性场景:
看到公司游戏广告的用户从点击广告页面推送给我方,除了相对应的福利外,首先判断是否有注册过或者玩过本公司游戏,
没有给予注册,但是点击的消息也要传到对应的计算服务里面。
同步:如果点击信息的服务挂了,影响判断是否注册,过度依赖其余系统
消息队列:写入消息队列,不影响主体业务
秒杀:
为了更加直观的展示MQ的应用场景,这里我们就用一个常见的电商系统中的几个业务,来具体说明下MQ在实际开发中应用场景。
我们的实际场景大概是一个基于微服务架构的电商系统,分为用户微服务、商品微服务、订单微服务、促销微服务等。基于微服务模式开发的系统,MQ的使用场景更多,下面我们逐一说明:
- 注册后我们可能需要做很多初始化的操作,如:调用邮件服务器发送邮件、调用促销服务赠送优惠劵、下发用户数据到客户关系系统等。那么这时候我们将这些操作去监听MQ,当用户注册成功过后,通过MQ通知其他业务进行操作。确保注册用户的性能。
- 后台发布商品的时候,商品数据需要从数据库中转换成搜索引擎数据(基于elasticsearch),那么我们应该将商品写入数据库后,再写入到MQ,然后通过监听MQ来生成elasticsearch对应的数据。
- 用户下单后,24小时未支付,需要取消订单。以前我们可能是定时任务循环查询,然后取消订单。实际上,我更推荐类似延迟MQ的方式,避免了很多无效的数据库查询,将一个MQ设置为24小时后才让消费者消费掉,这样很大程度上能减轻服务器压力。
- 支付完成后,需要及时的通知子系统(进销存系统发货,用户服务积分,发送短信)进行下一步操作,但是,支付回调我们都是需要保证高性能的,所以,我应该直接修改数据库状态,存入MQ,让MQ通知子系统做其他非实时的业务操作。这样能保证核心业务的高效及时。
MQ框架比较
ActiveMQ的消息传递模式
P2P (点对点)消息域使用 queue 作为 Destination,消息可以被同步或异步的发送和接收,每个消息只会给一个 Consumer 传送一次。
Pub/Sub(发布/订阅,Publish/Subscribe)消息域使用 topic 作为 Destination,发布者向 topic 发送消息,订阅者注册接收来自 topic 的消息。发送到 topic 的任何消息都将自动传递给所有订阅者。接收方式(同步和异步)与 P2P 域相同。
安装与下载(windows)
- 官方网站下载:http://activemq.apache.org/activemq-5154-release.html
- 解压后,根据操作系统的位数选择,用管理员身份打开activemq.bat(直接启动)或者installService.bat(使用服务启动)
- 登录http://localhost:8161/admin/,如果打开则为成功,账号密码都是admin
解释下上面图片中控制台这些按钮的基本信息:
- Home:查看 ActiveMQ 的常见信息
- Queues:查看 ActiveMQ 的队列信息
- Topics:查看 ActiveMQ 的主题信息
- Subscribers:查看主题的订阅者信息
- Connections:查看 ActiveMQ 客户端的连接信息
- Network:查看 ActiveMQ 的网络信息
- Scheduled:查看 ActiveMQ 的定时任务
- Send:用于通过表单方式向队列或者主题发送具体的消息
ActiveMQ的相关概念
1.Destination
目的地,JMS Provider(消息中间件)维护,用于对Message进行管理的对象。
MessageProducer需要指定Destination才能发送消息,MessageConsumer需要指定Destination才能接收消息。
2.Producer
消息生成者(客户端,生成消息),负责发送Message到目的地。应用接口为MessageProducer
3.Consumer【Receiver】
消息消费者(处理消息),负责从目的地中消费【处理|监听|订阅】Message。应用接口为MessageConsumer
4.Message
消息(Message),消息封装一次通信的内容。常见类型有:StreamMessage、BytesMessage、TextMessage、ObjectMessage、MapMessage。
5.ConnectionFactory
链接工厂, 用于创建链接的工厂类型。 注意,不能和 JDBC 中的 ConnectionFactory 混
淆。
6.Connection
链接. 用于建立访问 ActiveMQ 连接的类型, 由链接工厂创建. 注意,不能和 JDBC 中的
Connection 混淆。
7.Session
会话, 一次持久有效有状态的访问. 由链接创建. 是具体操作消息的基础支撑。
8.Queue&Topic
Queue是队列目的地,Topic是主题目的地。都是Destination的子接口。
Queue特点:队列中的消息,默认只能有唯一的一个消费者处理。
Topic特点:主题中的消息,会发送给所有的消费者同时处理。只有在消息可以重复处理的业务场景中可使用。
9.PTP
Point to Point。点对点消息模型,就是基于Queue实现的消息处理方式。
10.PUB&SUB
Publish&Subscribe。消息的发布/订阅模型。是基于Topic实现的消息处理方式。
ActiveMQ的异常处理
- 如何防止消息重复发送
解决方法:增加消息状态表,通俗来说就是一个账本,用来记录消息的处理状态,每次处理消息之前,都去状态表中查询一次,如果已经有相同的消息存在,那么不处理,可以防止重复发送 - 丢消息怎么办
解决方案:用持久化消息【可以使用对数据进行持久化JDBC,AMQ(日志文件),KahaDB和LevelDB】,或者非持久化消息及时处理不要堆积,或者启动事务,启动事务后,commit()方法会负责任的等待服务器的返回,也就不会关闭连接导致消息丢失了。 - 持久化消息非常慢
默认的情况下,非持久化的消息是异步发送的,持久化的消息是同步发送的,遇到慢一点的硬盘,发送消息的速度是无法忍受的。但是在开启事务的情况下,消息都是异步发送的,效率会有2个数量级的提升。所以在发送持久化消息时,请务必开启事务模式。其实发送非持久化消息时也建议开启事务,因为根本不会影响性能 - 服务挂掉
这得从ActiveMQ的储存机制说起。在通常的情况下,非持久化消息是存储在内存中的,持久化消息是存储在文件中的,它们的最大限制在配置文件的节点中配置。但是,在非持久化消息堆积到一定程度,内存告急的时候,ActiveMQ会将内存中的非持久化消息写入临时文件中,以腾出内存。虽然都保存到了文件里,但它和持久化消息的区别是,重启后持久化消息会从文件中恢复,非持久化的临时文件会直接删除
-------------------与springboot 2.0的整合------------------
pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.1.4.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.alipay</groupId>
<artifactId>demo</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>demo</name>
<description>Demo project for Spring Boot</description>
<properties>
<java.version>1.8</java.version>
</properties>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
<plugin>
<groupId>org.mybatis.generator</groupId>
<artifactId>mybatis-generator-maven-plugin</artifactId>
<version>1.3.2</version>
<configuration>
<configurationFile>src/main/resources/mybatis-generator-config.xml</configurationFile>
<verbose>true</verbose>
<overwrite>true</overwrite>
</configuration>
<executions>
<execution>
<id>Generate MyBatis Artifacts</id>
<goals>
<goal>generate</goal>
</goals>
</execution>
</executions>
<dependencies>
<dependency>
<groupId>org.mybatis.generator</groupId>
<artifactId>mybatis-generator-core</artifactId>
<version>1.3.2</version>
</dependency>
</dependencies>
</plugin>
</plugins>
</build>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- 消息队列-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-activemq</artifactId>
</dependency>
<!--消息队列连接池-->
<dependency>
<groupId>org.apache.activemq</groupId>
<artifactId>activemq-pool</artifactId>
<version>5.15.0</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-jms</artifactId>
</dependency>
<dependency>
<groupId>org.apache.activemq</groupId>
<artifactId>activemq-client</artifactId>
</dependency>
</dependencies>
</project>
yml配置文件
#日志
logging:
level:
com:
alipay:
demo:
mapper: debug
quartz: debug
# file: quartz-service.log
#端口
server:
port: 8090
tomcat:
uri-encoding: utf-8
spring:
activemq:
user: admin
password: admin
#true 表示使用内置的MQ,false则连接服务器
in-memory: true
# 给java用的tcp端口是61616
broker-url: tcp://127.0.0.1:61616
#需要加入配置文件,支持发布订阅模型,默认只支持点对点
jms:
pub-sub-domain: true
pool:
#true表示使用连接池;false时,每发送一条数据创建一个连接
enabled: true
#最大连接数
max-connections: 50
packages:
trust-all: true
生产者(producter)
BaseProducer.java
package com.hiqiblog.queue.producer;
import com.hiqiblog.queue.Producer;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jms.core.JmsMessagingTemplate;
public abstract class BaseProducer implements Producer {
@Autowired
private JmsMessagingTemplate jmsTemplate;
@Override
public void enqueue(String message) {
this.enqueue(this.getQueueName(), message);
}
protected void enqueue(String destinationName, String message) {
jmsTemplate.convertAndSend(destinationName, message);
}
protected abstract String getQueueName();
}
Producer.java
package com.hiqiblog.queue;
/**
* @Author helloc
* @Date 2019/7/26 9:12
* @Version 1.0
*/
public interface Producer {
void enqueue(String message);
}
AppQueueProducer.java
/**
* @Author helloc
* @Date 2019/7/26 9:12
* @Version 1.0
*/
@Component
public class AppQueueProducer extends BaseProducer {
public final static String APP_QUEUE = "helloq";
@Override
protected String getQueueName() {
return APP_QUEUE;
}
}
controller
package com.hiqiblog.controller;
import com.hiqiblog.ViewModel.ResponseMessage;
import com.hiqiblog.queue.producer.BaseProducer;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jms.core.JmsTemplate;
import org.springframework.jms.core.MessageCreator;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.jms.*;
/** 功能描述:模拟微信支付回调 @Author helloc @Date 2019/7/25 14:00 @Version 1.0 */
@RestController
@RequestMapping("/api/v1")
public class OrderController {
@Autowired private BaseProducer baseProducer;
/**
* 功能描述:微信支付回调接口
*
* @param msg 支付信息
* @return
*/
@GetMapping("order")
public Object order(String msg) {
baseProducer.enqueue(msg);
ResponseMessage responseMessage = new ResponseMessage();
responseMessage.setCode("1");
responseMessage.setMsg("Success");
return responseMessage;
}
@GetMapping("common")
public Object common(String msg) {
baseProducer.enqueue(msg);
ResponseMessage responseMessage = new ResponseMessage();
responseMessage.setCode("1");
responseMessage.setMsg("Success");
return responseMessage;
}
}
调用
结果:
消费者:
package com.hiqiblog.queue.consumer;
import com.hiqiblog.queue.Consumer;
import org.springframework.jms.annotation.JmsListener;
import org.springframework.stereotype.Component;
/**
* @Author helloc
* @Date 2019/7/26 11:40
* @Version 1.0
*/
@Component
public class AppQueueConsumer {
@Override
@JmsListener(destination = "helloq")
public void dequeue(String message) {
System.out.println("报文为:"+message);
}
}
两种通讯模式
- point-to-point:点对点(queue)
- publish/subscribe发布/订阅模式(topic)
queue 特点
1)一个消息只能被一个服务接收
2)消息一旦被消费,就会消失
3)如果没有被消费,就会一直等待,直到被消费
4)多个服务监听同一个消费空间,先到先得
topic 特点
1)一个消息可以被多个服务接收
2)订阅一个主题的消费者,只能消费自它订阅之后发布的消息。
3)消费端如果在生产端发送消息之后启动,是接收不到消息的,除非生产端对消息进行了持久化(例如广播,只有当时听到的人能听到信息)
queue 模式
点对点通信,每个消息只有一个消费者,消息保证送达,离线消费者可以在下次上线后收到之前积压的消息
topic 模式
普通订阅:
不区分消费者,当前有几个客户端在线,就发送几条广播给客户端。
持久订阅:
区分消费者,消费者在线则直接发送消息广播给消费者,消费者离线,只要该消费者有topic登记,就会为其保留消息直至其再次连接后一次性推送,消息可以积压。
扩展
如何保证消息队列的高可用性?
- RabbitMQ有三种模式:单机模式,普通集群模式(启动多个实例,每个实例都同步queue元数据),镜像集群模式(你创建的queue,无论元数据还是queue里的消息都会存在于多个实例上,然后每次你写消息到queue的时候,都会自动把消息到多个实例的queue里进行消息同步)
- kafka
Kafka天生就是一个分布式的消息队列,它可以由多个broker组成,每个broker是一个节点;你创建一个topic,这个topic可以划分为多个partition,每个partition可以存在于不同的broker上,每个partition就放一部分数据。
kafka 0.8以后,提供了HA机制,就是replica副本机制。kafka会均匀的将一个partition的所有replica分布在不同的机器上,来提高容错性。每个partition的数据都会同步到吉他机器上,形成自己的多个replica副本。然后所有replica会选举一个leader出来,那么生产和消费都去leader,其他replica就是follower,leader会同步数据给follower。当leader挂了会自动去找replica,然后会再选举一个leader出来,这样就具有高可用性了。
写数据的时候,生产者就写leader,然后leader将数据落地写本地磁盘,接着其他follower自己主动从leader来pull数据。一旦所有follower同步好数据了,就会发送ack给leader,leader收到所有follower的ack之后,就会返回写成功的消息给生产者。(当然,这只是其中一种模式,还可以适当调整这个行为)
消费的时候,只会从leader去读,但是只有一个消息已经被所有follower都同步成功返回ack的时候,这个消息才会被消费者读到。
如何保证消息消费时的幂等性?
RabbitMQ是ack,Kafka是offset
如何保证消息的可靠性传输?
- RabbitMQ
生产者弄丢了数据:
①事务机制:发送消息,如果消息没有成功被RabbitMQ接收到,那么生产者会收到异常报错,此时就可以回滚事务(channel.txRollback),然后重试发送消息
②confirm机制:开启confirm模式,在生产者那里设置开启confirm模式之后,你每次写的消息都会分配一个唯一的id,然后如果写入了RabbitMQ中,RabbitMQ会给你回传一个ack消息,告诉你说这个消息ok了。如果RabbitMQ没能处理这个消息,会回调你一个nack接口,告诉你这个消息接收失败,你可以重试
RabbitMQ弄丢了数据:
开启RabbitMQ的持久化,消息写入磁盘
第一个是创建queue的时候将其设置为持久化的,这样就可以保证RabbitMQ持久化queue的元数据,但是不会持久化queue里的数据;第二个是发送消息的时候将消息的deliveryMode设置为2,就是将消息设置为持久化的,此时RabbitMQ就会将消息持久化到磁盘上去。必须要同时设置这两个持久化才行
消费端弄丢了数据:
RabbitMQ提供的ack机制,简单来说,就是你关闭RabbitMQ自动ack,可以通过一个api来调用就行,然后每次你自己代码里确保处理完的时候,再程序里ack一把。这样的话,如果你还没处理完,不就没有ack?那RabbitMQ就认为你还没处理完,这个时候RabbitMQ会把这个消费分配给别的consumer去处理,消息是不会丢的 - kafka
生产者弄丢了数据:
如果按照上述的思路设置了ack=all,一定不会丢,要求是,你的leader接收到消息,所有的follower都同步到了消息之后,才认为本次写成功了。如果没满足这个条件,生产者会自动不断的重试,重试无限次
Kafka弄丢了数据:
给这个topic设置replication.factor参数:这个值必须大于1,要求每个partition必须有至少2个副本。
- 在kafka服务端设置min.insync.replicas参数:这个值必须大于1,这个是要求一个leader至少感知到有至少一个follower还跟自己保持联系,没掉队,这样才能确保leader挂了还有一个follower吧。
- 在producer端设置acks=all:这个是要求每条数据,必须是写入所有replica之后,才能认为是写成功了。
- 在producer端设置retries=MAX(很大很大很大的一个值,无限次重试的意思):这个是要求一旦写入失败,就无限重试,卡在这里了。
消费端弄丢了数据:
kafka会自动提交offset,那么只要关闭自动提交offset,在处理完之后自己手动提交offset,就可以保证数据不会丢