入门
中间件
什么是中间件
中间件是一种独立的系统软件服务程序,分布式应用软件借助这种软件在不同的技术之间共享资源(百度百科)
中间件是独立于项目的第三方辅助程序,用来提高项目的性能,降低耦合性(eleven).
中间件的特点
支持跨平台,跨语言使用
能够支持大量应用的使用
支持分布式部署
支持标准协议
支持标准的接口
消息中间件/消息队列
什么是消息队列
消息队列主要是用来充当消息缓冲的组件,一个项目最常见的架构是串行的,各个服务之间调用关系是串行的
加入消息队列中间件后可以将项目的串行关系转换成并行关系
此时订单系统在生成订单后会将消息发送至消息队列中,支付系统和仓库系统可以同时消费消息队列中的消息。
消息队列应用场景
消息队列的应用场景主要有三个:解耦,异步,削峰
解耦
还是以上面的商城系统流程图举例,在使用了消息队列后,系统由原来的各个子系统相互依赖的关系变成了不相互依赖的关系,这种转变就叫做解耦。因为我们的订单系统、支付系统和仓库系统等子系统都只和消息队列进行交互。通常我们讲的一个好的系统要是高内聚低耦合的,所以应用了消息队列中间件后可以起到解耦的作用。
异步
还是以上面的商城系统流程图举例,使用消息队列中间件之前,各个子系统的执行是顺序执行的,订单系统执行完毕后接着调用支付系统,支付系统执行完毕后调用仓库系统减少仓储,是顺序执行的,即同步执行。但是使用消息队列后,支付系统和仓库系统可以同时执行,即异步执行。在订单系统处理完毕后将消息发送至消息队列中,支付系统和仓库系统通过消息队列收到消息后会同步执行,进行逻辑处理。
削峰
还是以上面的商城系统流程图举例,使用消息队列中间件之前,各个子系统的执行是顺序执行的,假如支付系统最多只能支持100的并发量,而前面订单系统处理了1000次请求后,1000个请求都到了支付系统这里,那么支付系统此时就会造成网络拥塞。使用消息队列后,订单系统处理完1000个请求后直接将消息抛至消息队列中即可,然后订单系统返回给用户执行成功的反馈。支付系统没有那么大的并发量,根据自身的情况去消息队列中取消息处理即可,起到了削峰的作用。
消息队列的优缺点
使用消息队列的优点也是这三个:解耦,异步,削峰
缺点:
系统的复杂性增大
维护成本增大
人力成本增大
需要考虑额外的问题:一致性问题,消息持久化问题。
常见的消息中间件
特性 | ActiveMQ | RabbitMQ | RocketMQ | Kafka |
---|---|---|---|---|
单机吞吐量 | 万级,比 RocketMQ、Kafka 低一个数量级 | 同 ActiveMQ | 10 万级,支撑高吞吐 | 10 万级,高吞吐,一般配合大数据类的系统来进行实时数据计算、日志采集等场景 |
topic 数量对吞吐量的影响 | topic 可以达到几百/几千的级别,吞吐量会有较小幅度的下降,这是 RocketMQ 的一大优势,在同等机器下,可以支撑大量的 topic | topic 从几十到几百个时候,吞吐量会大幅度下降,在同等机器下,Kafka 尽量保证 topic 数量不要过多,如果要支撑大规模的 topic,需要增加更多的机器资源 | ||
时效性 | ms 级 | 微秒级,这是 RabbitMQ 的一大特点,延迟最低 | ms 级 | 延迟在 ms 级以内 |
可用性 | 高,基于主从架构实现高可用 | 同 ActiveMQ | 非常高,分布式架构 | 非常高,分布式,一个数据多个副本,少数机器宕机,不会丢失数据,不会导致不可用 |
消息可靠性 | 有较低的概率丢失数据 | 基本不丢 | 经过参数优化配置,可以做到 0 丢失 | 同 RocketMQ |
功能支持 | MQ 领域的功能极其完备 | 基于 erlang 开发,并发能力很强,性能极好,延时很低 | MQ 功能较为完善,还是分布式的,扩展性好 | 功能较为简单,主要支持简单的 MQ 功能,在大数据领域的实时计算以及日志采集被大规模使用 |
初级
下载和安装
自行百度,注意RabbitMQ是erlang开发的,所以安装RabbitMQ需要安装erlang的开发环境。
建议:学习阶段可以安装windows版本的,减少不必要的学习成本。
RabbitMQ协议是AMQP协议
AMQP(Advanced Message Queuing Protocol,高级消息队列协议)是一个进程间传递异步消息的网络协议
所谓协议就是定义的一种规范,RabbitMQ按照这种规范进行消息的传递。
其中AMQP传输模型中的几个名词解释:
Broker:指的是一个RabbitMQ服务节点。
Publisher:生产者
Consumer:消费者
Exchange:交换机,生产者将消息发送至交换机,交换机将消息转发至对应的队列
queue:队列,存储消息
bingings:交换机和队列需要绑定一个routingkey
routingkey:路由key,生产者发送消息的时候需要指定路由key,交换机拿到消息后根据routingkey和绑定的routingkey进行匹配,将消息发送至对应的队列
Virtual host:虚拟机,类似mysql中的一个database
中级
RabbitMQ高级特性
消息可靠性
在mq中我们分为消息生产者和消息消费者两个角色,那么保证生产者成功发送消息和消费者成功消费消息就叫做消息的可靠性。-----这是我自己提出的一个概念
那么保证消息的可靠性就需要保证消息的投递可靠性和消息的消费可靠性。
接下来我们分别讲解消息的投递可靠性和消费的消费可靠性。
生产者可靠性投递
生产者可靠性投递指的是确保生产者发送的消息能成功进入到消息队列,如果在发送过程中发生失败,那么需要如何处理?
消息投递失败可能会出现两种情况:
- 消息发送Exchange失败
- 消息发送Exchange成功,但是Exchange转发至Queue失败。
那么针对以上两种情况,RabbitMQ提出了两种处理机制
-
confirm确认机制
消息在成功发送到Exchange后,交换机会给给生产者立即反馈结果,告诉生产者是否成功收到消息,通过一个回调函数生产者能确认消息是否到达交换机成功
-
return机制
当Exchange将消息转发至Queue失败时,会反馈给生产者一个结果,通过一个回调函数生产者可以处理发送失败的消息。
confirm确认机制代码实例
//生产者
public class Producter {
public static void main(String[] args) throws IOException, TimeoutException {
//1.获取通道实例
Channel channel = RabbitConfig.getChannel();
//2.声明交换机
channel.exchangeDeclare("confirm_exchange", BuiltinExchangeType.DIRECT,true,false,null);
//3.生命并配置队列
channel.queueDeclare("queue_confirm",true,false,false,null);
//4.绑定队列到交换机
channel.queueBind("queue_confirm","confirm_exchange","test_confirm");
//5.开启confirm确认机制
channel.confirmSelect();
//6.添加confirm监听器
channel.addConfirmListener(new ConfirmListener() {
//消息成功后执行
@Override
public void handleAck(long deliveryTag, boolean multiple) throws IOException {
System.out.println("消息发送交换机成功!!!");
}
//消息失败后执行
@Override
public void handleNack(long deliveryTag, boolean multiple) throws IOException {
System.out.println("消息发送交换机失败!!!");
System.out.println("处理失败消息入库或者重发逻辑");
}
});
//7.发送消息
channel.basicPublish("confirm_exchange","test_confirm",null,"hhahahaa".getBytes());
}
}
//消费者
public class Consumer {
public static void main(String[] args) throws IOException, TimeoutException {
//获取通道实例
Channel channel = RabbitConfig.getChannel();
//消费消息
channel.basicConsume("queue_confirm",true,new DefaultConsumer(channel){
@Override
public void handleDelivery(String consumerTag,
Envelope envelope,
AMQP.BasicProperties properties,
byte[] body)
throws IOException
{
System.out.println(new String(body));
}
});
}
}
注意:可能你在代码实操的过程中发现confirm内的回调并没有被执行,问题可能是你前面一次执行失败,导致队列和交换机已经被创建了,可以将已创建的交换机和队列删除重新执行。
return机制代码实例
//生产者
public class Producter {
public static void main(String[] args) throws IOException, TimeoutException {
//1.获取通道实例
Channel channel = RabbitConfig.getChannel();
//2.声明交换机
channel.exchangeDeclare("confirm_exchange", BuiltinExchangeType.DIRECT,true,false,null);
//3.生命并配置队列
channel.queueDeclare("queue_confirm",true,false,false,null);
//4.绑定队列到交换机
channel.queueBind("queue_confirm","confirm_exchange","test_confirm");
//5.开启confirm确认机制
channel.confirmSelect();
//6.添加confirm监听器
channel.addConfirmListener(new ConfirmListener() {
//消息成功后执行
@Override
public void handleAck(long deliveryTag, boolean multiple) throws IOException {
System.out.println("消息发送交换机成功!!!");
}
//消息失败后执行
@Override
public void handleNack(long deliveryTag, boolean multiple) throws IOException {
System.out.println("消息发送交换机失败!!!");
System.out.println("处理失败消息入库或者重发逻辑");
}
});
//7.添加return机制监听器
channel.addReturnListener(new ReturnListener() {
/**
* @param replyCode 回复代码
* @param replyText 回复解释
* @param exchange 交换机
* @param routingKey 路由key
* @param properties 参数
* @param body 消息
* @throws IOException
*/
@Override
public void handleReturn(int replyCode, String replyText, String exchange, String routingKey, AMQP.BasicProperties properties, byte[] body) throws IOException {
System.out.println(replyCode);
System.out.println(replyText);
System.out.println(new String(body));
}
});
//7.发送消息,需要开启return机制,mandatory参数为true开启return机制
//该参数默认为false,那么入队列失败的消息将被mq丢弃。
channel.basicPublish("confirm_exchange","test_confirm1",true,null,"hhahahaa".getBytes());
}
}
//解释:这里我为了能观察到入队列失败的效果,写了一个不存在的队列名称
//消费者
public class Consumer {
public static void main(String[] args) throws IOException, TimeoutException {
Channel channel = RabbitConfig.getChannel();
channel.basicConsume("queue_confirm",true,new DefaultConsumer(channel){
@Override
public void handleDelivery(String consumerTag,
Envelope envelope,
AMQP.BasicProperties properties,
byte[] body)
throws IOException
{
System.out.println(new String(body));
}
});
}
}
消费者消费可靠性
为什么需要消息的消费可靠性?
当消费者拿到消息后开始进行逻辑处理,但是在逻辑处理的过程中可能会发生异常或者error,那么这条消息并没有被处理成功,所以就需要重新获取消息进行处理。
ack确认机制
消费者消息的消费可靠性可以通过ack确认机制来保证。
在rabbitMQ中的两种确认机制:
- 自动确认:在接收消息的时候指定autoAck参数为true。
- 手动确认:在接收消息的时候指定autoAck参数为false,然后处理完逻辑后手动的调用channel.basicNack()或者channel.basicAck()确认消息。
代码实例
//生产者,我们使用生产者可靠性投递的代码
public class Producter {
public static void main(String[] args) throws IOException, TimeoutException {
//1.获取通道实例
Channel channel = RabbitConfig.getChannel();
//2.声明交换机
channel.exchangeDeclare("ack_exchange", BuiltinExchangeType.DIRECT,true,false,null);
//3.生命并配置队列
channel.queueDeclare("queue_ack",true,false,false,null);
//4.绑定队列到交换机
channel.queueBind("queue_ack","ack_exchange","test_ack");
//5.开启confirm确认机制
channel.confirmSelect();
//6.添加confirm监听器
channel.addConfirmListener(new ConfirmListener() {
//消息成功后执行
@Override
public void handleAck(long deliveryTag, boolean multiple) throws IOException {
System.out.println("消息发送交换机成功!!!");
}
//消息失败后执行
@Override
public void handleNack(long deliveryTag, boolean multiple) throws IOException {
System.out.println("消息发送交换机失败!!!");
System.out.println("处理失败消息入库或者重发逻辑");
}
});
//7.添加return机制监听器
channel.addReturnListener(new ReturnListener() {
/**
* @param replyCode 回复代码
* @param replyText 回复解释
* @param exchange 交换机
* @param routingKey 路由key
* @param properties 参数
* @param body 消息
* @throws IOException
*/
@Override
public void handleReturn(int replyCode, String replyText, String exchange, String routingKey, AMQP.BasicProperties properties, byte[] body) throws IOException {
System.out.println(replyCode);
System.out.println(replyText);
System.out.println(new String(body));
}
});
//7.发送消息,需要开启return机制,mandatory参数为true开启return机制
channel.basicPublish("ack_exchange","test_ack",true,null,"hhahahaa".getBytes());
}
}
//消费者
public class Consumer {
public static void main(String[] args) throws IOException, TimeoutException {
Channel channel = RabbitConfig.getChannel();
//1.设置autoAck为false,true为自动确认,false为手动确认
channel.basicConsume("queue_ack",false,new DefaultConsumer(channel){
@Override
public void handleDelivery(String consumerTag,
Envelope envelope,
AMQP.BasicProperties properties,
byte[] body)
throws IOException
{
//2.获取消息
System.out.println(new String(body));
//3.处理逻辑,手动确认消息
try {
int a = 3/0;
//此处手动的抛出异常,
//确认成功
channel.basicAck(envelope.getDeliveryTag(), true);
} catch (Exception e) {
//确认失败,告诉mq消息处理失败重新发送消息
//参数二multiple:是否批量确认
//参数三requeue:是否重新入队列 ,false:会丢弃消息,true:此消费者会一直接收消息进行处理
channel.basicNack(envelope.getDeliveryTag(),true,true);
}
}
});
}
}
//注意:如果不确认消息,那么消息的状态就是 Unacked,可以在mq的web控制界面看到
消费端限流
在项目开发中我们可能会遇到一种情况,比如mq中有了大量的消息,但是消费端同一时刻只能处理10条消息,所以此时就需要应用rabbitMQ的限流策略。
代码实例
//生产者
public class Producter {
public static void main(String[] args) throws IOException, TimeoutException {
//1.获取通道实例
Channel channel = RabbitConfig.getChannel();
//2.声明交换机
channel.exchangeDeclare("xianliu_exchange", BuiltinExchangeType.DIRECT,true,false,null);
//3.生命并配置队列
channel.queueDeclare("queue_xianliu",true,false,false,null);
//4.绑定队列到交换机
channel.queueBind("queue_xianliu","xianliu_exchange","test_xianliu");
//5.开启confirm确认机制
channel.confirmSelect();
//6.添加confirm监听器
channel.addConfirmListener(new ConfirmListener() {
//消息成功后执行
@Override
public void handleAck(long deliveryTag, boolean multiple) throws IOException {
System.out.println("消息发送交换机成功!!!");
}
//消息失败后执行
@Override
public void handleNack(long deliveryTag, boolean multiple) throws IOException {
System.out.println("消息发送交换机失败!!!");
System.out.println("处理失败消息入库或者重发逻辑");
}
});
//7.添加return机制监听器
channel.addReturnListener(new ReturnListener() {
/**
* @param replyCode 回复代码
* @param replyText 回复解释
* @param exchange 交换机
* @param routingKey 路由key
* @param properties 参数
* @param body 消息
* @throws IOException
*/
@Override
public void handleReturn(int replyCode, String replyText, String exchange, String routingKey, AMQP.BasicProperties properties, byte[] body) throws IOException {
System.out.println(replyCode);
System.out.println(replyText);
System.out.println(new String(body));
}
});
//7.发送消息,需要开启return机制,mandatory参数为true开启return机制
//一次性发送10条消息
for (int i = 0; i < 10; i++) {
channel.basicPublish("xianliu_exchange","test_xianliu",true,null,"hhahahaa".getBytes());
}
RabbitConfig.close(channel);
}
}
//消费者
public class Consumer {
public static void main(String[] args) throws IOException, TimeoutException, InterruptedException {
//1.获取通道实例
Channel channel = RabbitConfig.getChannel();
//2.设置限流策略
/**
* 参数一:prefetchSize:消息的大小,设置为0是不限制大小,通常设置为0
* 参数二:prefetchCount:告诉rabbitmq不要一次性给消费者推送大于N个消息
* 参数三:global:是否是全局的,是否针对的整个Connection,一个Connection有多个Channel
*/
channel.basicQos(0,5,false);
//3.确认模式需要设置为手动确认
channel.basicConsume("queue_xianliu",false,new DefaultConsumer(channel){
@Override
public void handleDelivery(String consumerTag,
Envelope envelope,
AMQP.BasicProperties properties,
byte[] body)
throws IOException
{
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
//2.获取消息
System.out.println(new String(body));
//3.处理逻辑,手动确认消息
try {
//确认成功
channel.basicAck(envelope.getDeliveryTag(), true);
} catch (Exception e) {
//告诉mq消息处理失败重新发送消息
//参数二multiple:是否批量确认
//参数三requeue:是否重新入队列 ,false:会丢弃消息
channel.basicNack(envelope.getDeliveryTag(),true,true);
}
}
});
}
}
//注意:设置限流策略需要将消费者确认模式设置为手动确认
消息过期-TTL
针对设置消息过期我们有两种方式:
- 设置队列的过期时间
- 设置单条消息的过期时间
注意点
如果即设置了队列的过期时间,也设置了单条消息的过期时间,那么以时间短的那条设置为主。
设置队列的过期时间:队列过期时间到达后,会将过期的消息全部移除
设置单条消息的过期时间:消息过期后不会立即移除,只有当消息到达队列顶端时,mq会判断消息是否已经过期,然后再将消息移除。
代码案例:
//TTL只涉及生产者,这里只贴出生产者的代码。
//生产者---队列过期
public class Producter {
public static void main(String[] args) throws IOException, TimeoutException {
//1.获取通道实例
Channel channel = RabbitConfig.getChannel();
//2.声明交换机
channel.exchangeDeclare("ttl_exchange", BuiltinExchangeType.DIRECT,true,false,null);
//3.生命并配置队列
//3.1 封装队列过期时间参数
Map<String, Object> arguments = new HashMap();
arguments.put("x-message-ttl",20000);
channel.queueDeclare("ttl_xianliu",true,false,false,arguments);
//4.绑定队列到交换机
channel.queueBind("ttl_xianliu","ttl_exchange","test_ttl");
//一次性发送10条消息
for (int i = 0; i < 10; i++) {
channel.basicPublish("ttl_exchange","test_ttl",null,"hhahahaa".getBytes());
}
RabbitConfig.close(channel);
}
}
//生产者----设置单条消息过期
public class Producter2 {
public static void main(String[] args) throws IOException, TimeoutException {
//1.获取通道实例
Channel channel = RabbitConfig.getChannel();
//2.声明交换机
channel.exchangeDeclare("ttl_exchange", BuiltinExchangeType.DIRECT,true,false,null);
//3.生命并配置队列
channel.queueDeclare("ttl_xianliu",true,false,false,null);
//4.绑定队列到交换机
channel.queueBind("ttl_xianliu","ttl_exchange","test_ttl");
//一次性发送10条消息
for (int i = 0; i < 10; i++) {
//设置单条消息过期
AMQP.BasicProperties.Builder builder = new AMQP.BasicProperties.Builder();
//DeliveryMode等于2就说明这个消息是持久化的。1是默认,不是持久的。
builder.deliveryMode(2);
// 设置过期时间TTL=30000ms
builder.expiration("30000");
AMQP.BasicProperties properties = builder.build() ;
channel.basicPublish("ttl_exchange","test_ttl",properties,"hhahahaa".getBytes());
}
RabbitConfig.close(channel);
}
}
死信队列-DLX
简介
死信队列全称为dead-letter-exchange,简称DLX
DLX我们可以理解成一个普通的队列,任何一个普通的队列我们都可以当作死信队列,当其他队列的消息变成死信时,这些死信消息就会被发布到死信队列中。
死信交换机:因为我们发送消息都是给交换机发送的,然后交换机转发至绑定的队列上。所以要想死信队列生效,还需要创建对应的死信交换机。
什么样的消息才会变成死信消息?
- 消息被拒绝(basic.reject / basic.nack),并且requeue = false
- 消息TTL过期
- 队列达到最大长度
操作步骤
-
创建死信交换机
2. 创建死信队列并绑定死信交换机
- 创建生产者
//生产者
public class Producter {
public static void main(String[] args) throws IOException, TimeoutException {
//1.获取通道实例
Channel channel = RabbitConfig.getChannel();
//2.声明交换机
channel.exchangeDeclare("test_dlx_exchange", BuiltinExchangeType.DIRECT,true,false,null);
//3.声明并配置队列
Map<String, Object> arguments = new HashMap();
//3.1 封装队列过期时间参数
arguments.put("x-message-ttl",5000);
//3.2 设置死信交换机为我们创建的dlx.exchange这个交换机
arguments.put("x-dead-letter-exchange","dlx.exchange");
channel.queueDeclare("test_dlx_queue",true,false,false,arguments);
//4.绑定队列到交换机
channel.queueBind("test_dlx_queue","test_dlx_exchange","test_dlx");
//一次性发送10条消息
for (int i = 0; i < 10; i++) {
channel.basicPublish("test_dlx_exchange","test_dlx",null,"hhahahaa".getBytes());
}
RabbitConfig.close(channel);
}
}
//解释:过期的消息就会被发送到死信交换机上,然后转发至死信队列
延迟队列
RabbitMQ本身是不支持延迟队列的,但是通过前面讲的TTL和DLX就可以实现延迟队列。
延迟队列的应用场景:比如商城系统判断用户下订单后如果30分钟未支付,那么就取消订单。
可以给支付系统的mq设置消息过期时间TTL为30分钟,超过30分钟后,消息就进入到死信队列,然后死信队列绑定的消费者就去处理取消订单的逻辑。