为了保证知识的完整性,先机械的上几个概念。
什么是中间件:
非底层操作系统软件,非业务应用软件,不是直接给最终用户使用的,不能直接给客户带来价值的软件统称为中间件。
什么是消息中间件:
关注于数据的发送和接收,利用高效可靠的异步消息传递机制集成分布式系统
什么是JMS?:
Java消息服务(Java Massage Service),是一个java平台中关于面向消息中间件的API,用于在两个应用程序之间,或分布式系统中发送消息,进行异步通信。
相信看这篇文章的小伙伴看到这几个概念之后都会一脸懵逼的说一句:请说人话。到底消息中间件是什么,怎么用,用在哪里,为什么要用这个,用上了有什么好处。那么恭喜你跟我当时知道消息中间件的时候是一样的,带着这几个疑问我们往下看!
为什么使用消息中间件,它究竟是什么东西?
消息中间件说说我自己的理解:
前提介绍:用一个经典的例子来说明,相信小伙伴肯定做过登录功能吧,前端用户输入用户名,密码,验证码等信息,请求后台服务,后台服务进行一系列业务逻辑,返回前端登录成功及必要业务数据。大家可以仔细想一下这个流程看似没什么问题,但是如果产品经理说:
- 咱们加个逻辑吧,用户登录成功之后为了安全需要给用户手机发送一条登录短信。好的那我们找到这部分的相关代码,在原有逻辑上修改增加了这部分业务逻辑,测试没问题上线。
- 没几天产品经理又拿着小本过来啦说:那个登录那块在加个逻辑,让用户登录之后自动增加积分,相当于签到的功能。这个时候我们心中可能会 *** 这样哈,但是手上是不能停的找到原有登录逻辑,在后面加上增加积分相关业务逻辑,代码也变得更多了,写好之后测试成功上线。
- 过几天产品经理又来啦哈:那个登录那加个逻辑,用户登录之后记录一下登录日志,方便后台管理。**** 我们又这样了哈,但是虽然嘴上一万个不爱动,但是手上还是找到登录的逻辑加上代码测试上线。
相信小伙伴看到这里心中会不会有点优化的建议呢,可能是把单独的逻辑拆分出来,通过方法调用的方式,代码简洁,易维护,确实是,没错,但是小伙伴有没有注意到,原来的登录功能可能需要50ms就好了,但是加了这两个逻辑后可能就是200ms了。还有万一发送邮件超时报错了 用户将登录不成功这是致命的。所以要不要有一个服务,让这个服务来做这个事,我们登录的时候只需要告诉这个服务一声说我登录了你可以执行登录之后的相关逻辑了,我相信发短信啊加积分这样的逻辑晚个1~2分钟执行没什么关系吧。让这个单独的服务异步的执行相关逻辑。消息中间件就可以做这个事情,当然它做的事还有很多:
采用消息中间件的方式:登录之后,将消息放在消息中间件里面,然后异步的发送通知调用指定服务,直接返回前端登录成功,这样登录的逻辑中仅仅加了一个向消息中间件发送消息的逻辑代码。发短信,加积分这样的逻辑就放在另一个服务中也起到了解耦的作用:
总结起来消息中间件可以:异步,削峰,解耦。
因为本文着重介绍activemq的入门知识,所以先介绍一下jms:
jms相关概念:
1.提供者:实现JMS规范的消息中间件服务器
2.生产者/发布者:创建并发送消息的客户端
3.消费者/订阅者:接收并处理消息的客户端
4.消息:应用程序之间传递的数据内容
5.消息模式:在客户端之间传递消息的方式,JMS中定义了主题,队列两种模式
- 队列模型(点对点):
客户端包括生产者和消费者
队列中的消息只能被一个消费者消费,
消费者可以随时消费队列中的消息 - 主题模型(一对多):
客户端包括发布者和订阅者
主题中的消息可以被所有的订阅者消费
消费者不能消费订阅之前就发送到主题中的消息
接下来步入正题,activemq安装:
版本情况:linux:centos7, java:8, activemq:5.15.8
前提是linux的服务器要有java环境 - 下载activemq的安装包并解压:
- 去bin目录下start
这里说明一下,如果启动之后什么错误也没有,但是通过ps-ef|grep activemq
命令发现没有启动成功,那么可以使用:./activemq console
查看启动错误。 - 关闭:
./activemq stop
编码,springboot项目环境:
pom
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-activemq</artifactId>
</dependency>
<!--pom.xml也要加入下面这个依赖包,否则启动报JmsMessagingTemplate注入失败 。-->
<dependency>
<groupId>org.apache.activemq</groupId>
<artifactId>activemq-pool</artifactId>
</dependency>
queue:
属于点对点的关系,消息发送到queue中,如果没人消费会存在queue中。多个消费者不能重复消费同一个消息,一个消息只能被一个消费者消费。
//生产者代码
//61616端口是给java用的tcp端口
public static final String URL="tcp://127.0.0.1:61616";
//定义一个队列名称(地址)
public static final String QUEUENAME="queue01";
public static void main(String[] args) throws JMSException {
//1.创建连接工厂,按照给定的URL地址,采用默认的用户名密码
ConnectionFactory connectionFactory = new ActiveMQConnectionFactory(URL);
//2.通过连接工厂获得连接
Connection connection = connectionFactory.createConnection();
connection.start();
//3.创建会话,第一个参数是 事务,第二个参数是签收(事务和签收的概念请看下面的介绍)
Session session = connection.createSession(false, Session.AUTO_ACKNOWLEDGE);
//4.创建目的地 具体是队列还是主题
Queue queue = session.createQueue(QUEUENAME);
//5.创建消息的生产者
MessageProducer messageProducer = session.createProducer(queue);
//模拟发送消息
for(int i=0;i<3;i++){
//6.创建消息
TextMessage textMessage = session.createTextMessage("哈哈" + i);
System.out.println("发送消息:哈哈"+i);
//7.消息生产者发送消息
messageProducer.send(textMessage);
}
//关闭资源
messageProducer.close();
session.close();
connection.close();
}
//消费者代码
public static final String URL="tcp://127.0.0.1:61616";
public static final String QUEUENAME="queue01";
public static void main(String[] args) throws JMSException {
//创建连接工厂和获取session等的步骤和上面的生产者一致:
ConnectionFactory connectionFactory = new ActiveMQConnectionFactory(URL);
Connection connection = connectionFactory.createConnection();
connection.start();
Session session = connection.createSession(false, Session.AUTO_ACKNOWLEDGE);
Destination destination = session.createQueue(QUEUENAME);
MessageConsumer messageConsumer = session.createConsumer(destination);
//和生产者不同的是,消费者是需要监听指定的队列来实时获取消息:
//1.同步阻塞方法(receive),
//订阅者或接收者调用message的receive方法来接受消息,receive方法在
//能够接收到消息或超时之前将一直阻塞。
while (true){
TextMessage message = (TextMessage)messageConsumer.receive();
if(message != null){
System.out.println("接受到消息"+message.getText());
}else{
break;
}
}
messageConsumer.close();
session.close();
connection.close();
}
topic:
每个消息可以有多个消费者,订阅某一个主题的消费者只能消费自他订阅之后产生的消息(这个可以配置,看下面。。)。
发布者发布时,topic不保存消息他是无状态的不落地,假如无人订阅就去生产,那么这条消息就是废消息,所以一般先启动订阅者 在启动发布者。
JMS规范允许客户创建持久化订阅,这在一定程度上放松了时间上的相关性要求,持久订阅允许消费者消费它在未处于激活状态时发送的消息。
//topic发布者,和queue生产者就差创建目的地的代码
public static final String URL="tcp://127.0.0.1:61616";
public static final String TOPIC_NAME="topic01";
public static void main(String[] args) throws JMSException {
//1.创建连接工厂,按照给定的URL地址,采用默认的用户名密码
ConnectionFactory connectionFactory = new ActiveMQConnectionFactory(URL);
//2.通过连接工厂获得连接
Connection connection = connectionFactory.createConnection();
connection.start();
//3.创建会话,第一个参数是 事务,第二个参数是签收
Session session = connection.createSession(false, Session.AUTO_ACKNOWLEDGE);
//4.创建目的地 具体是队列还是主题,这里选主题
Topic topic = session.createTopic(TOPIC_NAME);
//5.创建消息的生产者
MessageProducer messageProducer = session.createProducer(topic);
for(int i=0;i<3;i++){
//6.创建消息
TextMessage textMessage = session.createTextMessage("哈哈" + i);
System.out.println("发送消息:哈哈"+i);
//7.消息生产者发送消息
messageProducer.send(textMessage);
}
//关闭资源
messageProducer.close();
session.close();
connection.close();
}
//topic消费者
public static final String URL="tcp://127.0.0.1:61616";
public static final String TOPIC_NAME="topic01";
public static void main(String[] args) throws JMSException {
ConnectionFactory connectionFactory = new ActiveMQConnectionFactory(URL);
Connection connection = connectionFactory.createConnection();
connection.start();
Session session = connection.createSession(false, Session.AUTO_ACKNOWLEDGE);
Topic topic = session.createTopic(TOPIC_NAME);
MessageConsumer messageConsumer = session.createConsumer(topic);
//设置消息的监听器,也可以用上面queue消费者获取消息的方式。
messageConsumer.setMessageListener(new MessageListener() {
@Override
public void onMessage(Message message) {
TextMessage textMessage = (TextMessage) message;
try {
System.out.println(textMessage.getText());
} catch (JMSException e) {
e.printStackTrace();
}
}
});
//connection.close();
}
MS消息(activemq):
- 消息头:
1.JMSDestination:可以设置该消息的目的地
2.JMSDeliveryMode:持久或非持久模式,
3.JMSExpiration: 消息的过期时间,默认是永不过期
4.JMSpriority:消息的优先级,分0~9 10个优先级,0到4是普通消息,5到9是加急消息,JMS不要求mq严格按照这10个优先级发送消息,但必须保证加急消息要先于普通消息到达。默认是4级
5.JMSMessageId:消息id - 消息体:
1.封装具体的消息数据
2.五种消息格式:- TextMessage
- MapMessage
- BytesMessage
- StreamMessage
- ObjectMessage
3.消息属性:
如果需要除消息头以外的值,那么可以使用消息属性,他们是以属性名和属性值对的形式制定的。可以将属性视为消息头的扩展,属性指定一些消息头没有包含的附加信息。
对识别/标注 非常有用,
TextMessage textMessage = session.createTextMessage("哈哈" + i);
//设置String类型的属性
textMessage.setStringProperty("name","value");
//获取放在消息中的属性
textMessage.getStringProperty("name")
消息的可靠性:
queue:
//非持久化,当mq宕机 消息不存在
messageProducer.setDeliveryMode(DeliveryMode.NON_PERSISTENT);
//持久化,当mq宕机 消息存在
messageProducer.setDeliveryMode(DeliveryMode.PERSISTENT);
//默认是持久化,因为对于队列 可靠性是优先考虑的因素
topic:
主题的和队列的持久化不太一样,因为在之前的案例中,如果先启动生产者,没有订阅者的话发出的消息就是废消息,而且队列模式下所有的消息都会保存在这个队列里,没有消费者消费消息不会丢。
主题模式是消息发布到频道中就ok 所以topic的持久化意味着:
A订阅了主题,这个时候A离线生产者发出消息,A又在线还能收到之前离线时的消息。如果生产者发布了消息,A又离线导致A没有收到消息,这时候mq宕机,mq重启之后消息也不会丢,A在线还能收到消息。注意必须A先订阅了这个频道
需要修改生产者和消费者代码:
//生产者
//生产者的代码变化不大,将连接打开的代码放下边
//这里标识启动的是一个持久化的发布者
messageProducer.setDeliveryMode(DeliveryMode.PERSISTENT);
connection.start();
//消费者(订阅者)
public static final String URL="tcp://192.168.131.128:61616";
public static final String TOPIC_NAME="topic01";
public static void main(String[] args) throws JMSException {
ConnectionFactory connectionFactory = new ActiveMQConnectionFactory(URL);
Connection connection = connectionFactory.createConnection();
//这里设置客户端id
connection.setClientID("z3");
Session session = connection.createSession(false, Session.AUTO_ACKNOWLEDGE);
Topic topic = session.createTopic(TOPIC_NAME);
//这里是创建一个持久化的订阅者
TopicSubscriber subscriber = session.createDurableSubscriber(topic, "remark...");
//然后在开启连接,注册进持久化订阅者中。
connection.start();
while(true){
TextMessage message = (TextMessage) subscriber.receive();
if(message != null){
System.out.println(message.getText());
}else{
break;
}
}
//connection.close();
}
事务:
事务偏生产者,
Session session = connection.createSession(false, Session.AUTO_ACKNOWLEDGE);
事务是在上面这行代码中声明的,如果第一个参数是false就是没有事务,true就是有事务。
- false:
只要执行send方法,消息就发送到消息队列中 - true:
执行send方法之后,消息并没有立即进入到队列中,进入了缓冲区,执行commit方法之后,消息才会进到队列中。
//创建事务的会话
Session session = connection.createSession(true, Session.AUTO_ACKNOWLEDGE);
//会话提交,将消息送到队列中
session.commit();
在消费者的一方同理,开启事务,只有commit之后才会成功将消息消费。不commit消息会一直在。
Acknowledge: 签收
签收偏消费者:
意思是可以对每一个消息消费之后进行签收,消息才会从队列中消失。但是签收是和事务有关的:
- 非事务的情况下:
自动签收:消费就没了
手动签收:只有签收之后消息才会没有,不然消息会在队列中 - 事务情况下:
无论是自动签收还是手动签收,只要会话一提交,本次会话所有消息自动签收。会话回滚,消息会被再次传送
利用上面的签收和事务的机制可以保证消费消息的可靠
Activemq传输协议:
- TCP:
这个是默认的通讯协议,监听端口61616,。
- NIO:
如果想要切换到nio协议只需要: - 修改activemq的配置文件,添加nio协议配置
进到activemq的安装目录conf目录,编辑配置文件:
- 修改链接地址:
public static final String URL="nio://192.168.131.128:61618";
Activemq的消息存储和持久化:
为了避免意外宕机以后丢失消息,需要做到重启后可以恢复消息队列,消息系统一般都会采用持久化机制。
Activemq的消息持久化机制有 JDBC,AMQ,KahaDB和LevelDB,无论使用哪种持久化方式,消息的存储逻辑都是一致的。
就是在发送者将消息发送出去后,消息中心首先将消息存储在本地数据文件,内存数据库或者远程数据库等在试图将消息发送给接收者,成功则将消息从存储中删除,失败则继续尝试发送。
消息中心启动以后首先要检查指定的存储位置,如果有未发送成功的消息,则需要把消息发送出去。
KahaDB消息存储(默认):
基于日志文件,从activemq5.4开始默认的持久化插件,类似于redis中的aof
kahadb是目前默认的存储方式,可用于任何场景,提高了性能和恢复能力。消息存储使用一个事务日志和仅仅用一个索引文件来存储它所有的地址,数据被追加到data logs中。当不在需要log文件中的数据的时候,log文件会被丢弃。
kahadb在消息保存目录中只有4类文件和一个lock,
-
1.db-.log:
kahadb存储消息到预定义大小的数据记录文件中,当数据文件满时,一个新的文件会随之创建,number数据递增,当不再有引用到数据文件中的任何消息时,文件会被删除或者归档。 -
2.db.data:
该文件包涵了持久化的BTree索引,索引了消息数据记录中的消息,它是消息的索引文件,本质上是B-Tree,使用B-Tree作为索引指向db-.log里面存储的消息 -
3.db.free:
当前db.data文件里哪些页面是空闲的,文件内容是所有空闲页的id,建索引就从这个文件中拿名字 防止名字重复。 -
4.db.redo :
用来进行消息恢复,如果kahadb消息存储在强制退出后启动,用于恢复BTree索引。 -
5.lock 文件锁:
表示当前获得kahadb读写权限的broker
题后话,由于作者水平有限,文章中难免会有歧义的地方,欢迎大家批评指正,作者定会及时改正