消息队列

消息队列

什么是消息队列?

消息队列是分布式架构中的重要组件,但并不是必须的,一般我们会简称它为MQ(Message Queue),嗯,就是很直白的简写。它是进程和进程之间通信的方法。 

队列是一种先进先出的数据结构。

[外链图片转存失败(img-nNQqsjh0-1566268961998)(C:\Users\Administrator\Desktop\新建文件夹\3f33dd7829dd47d79725aba489128a3c.png)]

消息队列可以简单理解为:把要传输的数据放在队列中

为什么要使用消息队列

为什么要使用消息队列?消息队列并不是必须要使用的,在特定的场景下才能体现它的价值。

消息队列的优点及其应用的场景:

1.异步处理

列如实现一个商城注册的功能:

传统的方式 串行:写入数据库→发送邮件→发送短信→成功向用户返回信息(耗时最长)

​ 并行:写入数据库→发送邮件,发送短信→成功向用户返回信息(耗时稍短)

使用消息队列:写入数据库→发送邮件,发送短信写入队列,向用户返回信息。在写入数据库完成后立即向消息队列写入消息,这个过程非常短暂几乎可以忽略不计,写入队列之后立即向用户返回信息,然后发送邮件发送短息由消费者订阅消息队列处理。实现异步处理,同时也实现了写入数据库和邮件短信发送的解耦。(耗时缩短了一半左右)

2.应用解耦

首先我们看下耦合较高的情况,谁愿意负责A系统?难道被累死么?

img

负责A系统的大兄弟自作主张引入MQ消息队列后,我管你老王、老张还是老李要什么数据,我放在MQ中,你们要就从MQ中拿,别来烦我

img

3.流量削峰

广泛应用于秒杀或抢购活动中,避免流量过大导致应用系统挂掉的情况;

没有用MQ就是这样:

img

使用MQ:

img

消息队列的使用(springamqp)实现发送邮件功能

工具:idea,spring,

首先要在系统上安装RabbitMQ,然后引入依赖

<!-- spring-rabbit依赖 -->
<spring-rabbit.version>1.7.2.RELEASE</spring-rabbit.version>
<!-- spring-amqp依赖 -->
<spring-amqp.version>1.7.2.RELEASE</spring-amqp.version>
-------------------------------------------------------------------------------------------
		   <dependency>
                <groupId>org.springframework.amqp</groupId>
                <artifactId>spring-rabbit</artifactId>
                <version>${spring-rabbit.version}</version>
            </dependency>
            <dependency>
                <groupId>org.springframework.amqp</groupId>
                <artifactId>spring-rabbit</artifactId>
                <version>${spring-amqp.version}</version>
            </dependency>

配置文件

<?xml version="1.0" encoding="UTF-8"?>
<beans:beans xmlns="http://www.springframework.org/schema/rabbit"
             xmlns:beans="http://www.springframework.org/schema/beans"
             xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
             xsi:schemaLocation="http://www.springframework.org/schema/rabbit
                http://www.springframework.org/schema/rabbit/spring-rabbit.xsd
              http://www.springframework.org/schema/beans
              http://www.springframework.org/schema/beans/spring-beans.xsd">

    <!--1、配置连接工厂, 如果不配置host, port, username, passowrd, 则按默认值localhost:5672, guest/guest-->
    <!--<connection-factory id="connectionFactory" />-->
    <connection-factory id="connectionFactory"
                        host="localhost"
                        port="5672"
                        username="ego"
                        password="ego" />

    <!--2、配置队列queue, 交换机Exchange, 以及将他们结合在一起的binding-->
    <!--在queue以及exchange中, 有一个重要的属性durable, 默认为true, 可以防止宕机后数据丢失。-->
    <!--在listener-container中, 有acknowledge属性, 默认为auto, 即消费者成功处理消息后必须有个应答, 如果消费者程序发生异常或者宕机, 消息会被重新放回队列-->
    <admin connection-factory="connectionFactory" />
    <queue id="userAlertEmailQueue" name="user.alerts.email" durable="true" />
    <queue id="userAlertCellphoneQueue" name="user.alerts.cellphone" />     <!--durable默认为true-->

    <!--标准的AMQP Exchange有4种: Direct, Topic, Headers, Fanout, 根据实际需要选择。-->
    <!--Direct: 如果消息的routing key与绑定的routing key直接匹配的话, 消息将会路由到该队列上。-->
    <!--Topic: 如果消息的routing key与绑定的routing key符合通配符匹配的话, 消息将会路由到该队列上。-->
    <!--Headers: 如果消息参数表中的头信息和值都与binding参数表中相匹配, 消息将会路由到该队列上。-->
    <!--Fanout: 不管消息的routing key和参数表的头信息/值是什么, 消息将会路由到该队列上。-->
    <direct-exchange name="user.alert.email.exchange" durable="true"> <-- 防止宕机 -->
        <bindings>
            <binding queue="user.alerts.email" />     <!--默认的routing key与队列的名称相同-->
        </bindings>
    </direct-exchange>
    <direct-exchange name="user.alert.cellphone.exchange">
        <bindings>
            <binding queue="user.alerts.cellphone" />
        </bindings>
    </direct-exchange>

    <!--3、配置RabbitTemplate发送消息-->
    <template id="rabbitTemplate"
              connection-factory="connectionFactory" />

    <!--4、配置监听器容器和监听器来接收消息-->
    <beans:bean id="userListener" class="com.ego.controller.UserAlertHandler" />
    <listener-container connection-factory="connectionFactory" acknowledge="auto">
        <listener ref="userListener"
                  method="handleUserAlertToEmail"
                  queues="userAlertEmailQueue" />
        <!--<listener ref="userListener"
                  method="handleUserAlertToCellphone"
                  queues="userAlertCellphoneQueue" />-->
    </listener-container>

</beans:beans>

生产者:

/**
 * 用户注册
 * @param admin
 * @return
 */
@RequestMapping("/register")
@ResponseBody
public BaseResult register(AdminWithBLOBs admin) {    //AdminWithBLOBs用户对象
    BaseResult result = userService.adminSave(admin); //注册操作的响应
    String str=admin.getEmail();                      //注册需发送的邮箱

    // 注册信息添加成功发送消息至消息队列发送邮件
    if (200 == result.getCode())					//如果注册成功
        userAlertService.sendUserAlertToEmail(str);	  //携带邮箱调用发送邮件方法
        //result = sendMailService.sendMail(str);
    return result;
}
package com.ego.service.impl;

import com.ego.service.UserAlertServiceI;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

@Service("userAlertService")
public class UserAlertServiceImpl implements UserAlertServiceI {
	//核心对象
    private RabbitTemplate rabbit;

    @Autowired
    public UserAlertServiceImpl(RabbitTemplate rabbit) {
        this.rabbit = rabbit;
    }

    // 生产者发消息到exchange交换机
    public void sendUserAlertToEmail(String emailStr) {
    	
        // convertAndSend(String exchange, String routingKey, Object object)
        // 将对象object封装成Message对象后, 发送给exchange**************************
        rabbit.convertAndSend("user.alert.email.exchange", "user.alerts.email", emailStr);
    }
}

/*至此生产者已将消息发送者队列 交换机名:user.alert.email.exchange
						  路由key:user.alerts.email
						  参数Object:emailStr
						  发送消息完成之后立刻向前端响应,发送邮件异步处理
*/

消费者:注意xml配置中的第4条配置监听器,监听容器为com.ego.controller.UserAlertHandler这个类

​ 队列userAlertEmailQueue与xml中发送邮件的queue相同,UserAlertHandler监听到相匹配的消息后进 行消费,获取参数发送邮件。

package com.ego.controller;

import com.ego.service.rpcInter.SendMailServiceI;
import org.springframework.beans.factory.annotation.Autowired;

public class UserAlertHandler {

    @Autowired
    private SendMailServiceI sendMailService;

    //接收消息,异步发送邮件
    public void handleUserAlertToEmail(String str) {
        //System.out.println(str);
        sendMailService.sendMail(str);//此方法是笔者写的邮件服务
    }
}

使用消息队列有什么缺点及解决方案

总的来说消息队列的优势是异步提高系统抗峰值的能力还可以实现系统结构的解耦

但是它的缺点也很明显,引入消息队列会造成系统的可用性降低,假如引入了消息队列,如果其它系统好好地消息队列宕机了,那整个系统都GG。系统复杂性增加:要多考虑很多方面的问题,比如一致性问题、如何保证消息不被重复消费,如何保证保证消息可靠传输。因此,需要考虑的东西更多,系统复杂性增大。

1.消息队列选型

一般中小型选用吞吐量万级别的RabbitMQ,但是不推荐ActiveMQ,为什么呢?

因为ActiveMQ的更新频率很慢,据说是将重心放在了下一代,社区也不是很活跃,RabbitMQ的社区很活跃,有很多问题都可以找到解决方案,更新频率也还可以。

大型公司则在kafka和rocketMq 中二选一了,但一般都会选择kafka,因为如果阿里放弃维护rocketMQ则会给公司带来不必要的麻烦。

2.如何保证消息队列是高可用的?

以RabbitMQ为例:

RabbitMQ消息队列有三种形式,单一模式、普通集群、镜像集群。镜像集群就是我们的高可用模式

单一模式:单一模式没什么好讲的,吞吐量低,只是单独的运行一个RabbitMQ

普通集群:以两个RabbitMQ为例,(rab1、rab2),对于Queue来说,消息实体只存在于其中一个节点rab1(或者rab2),rab1和rab2两个节点仅有相同的元数据,即队列的结构。 当消费rab1中的一个元数据,但数据实体在rab2中,这时rab1和rab2之间会建立一个物理queue传输实体。对于同一个逻辑队列,要在多个节点建立物理Queue。否则无论consumer连rab1或rab2,出口总在rab1,会产生瓶颈。当rab1节点故障后,rab2节点无法取到rab1节点中还未消费的消息实体。如果做了消息持久化,那么得等rab1节点恢复,然后才可被消费;如果没有持久化的话,就会产生消息丢失的现象

镜像模式:把需要的队列做成镜像队列,存在与多个节点属于**RabbitMQ的HA方案。**该模式解决了普通模式中的问题,其实质和普通模式不同之处在于,消息实体会主动在镜像节点间同步,而不是在客户端取数据时临时拉取。该模式带来的副作用也很明显,除了降低系统性能外,如果镜像队列数量过多,加之大量的消息进入,集群内部的网络带宽将会被这种同步通讯大大消耗掉。所以在对可靠性要求较高的场合中适用 ,但是5G时代的来临可能会为镜像模式带来春天。

3.如何保证消息不被重复消费

-------消费时对消息进行持久化操作

其实无论是那种消息队列,造成重复消费原因其实都是类似的。正常情况下,消费者在消费消息时候,消费完毕后,会发送一个确认信息给消息队列,消息队列就知道该消息被消费了,就会将该消息从消息队列中删除。只是不同的消息队列发送的确认信息形式不同,例如RabbitMQ是发送一个ACK确认消息,RocketMQ是返回一个CONSUME_SUCCESS成功标志,kafka实际上有个offset的概念,简单说一下(如果还不懂,出门找一个kafka入门到精通教程),就是每一个消息都有一个offset,kafka消费过消息后,需要提交offset,让消息队列知道自己已经消费过了。那造成重复消费的原因?,就是因为网络传输等等故障,确认信息没有传送到消息队列,导致消息队列不知道自己已经消费过该消息了,再次将该消息分发给其他的消费者。

如何解决?这个问题针对业务场景来答分以下几点

(1)比如,你拿到这个消息做数据库的insert操作。那就容易了,给这个消息做一个唯一主键,那么就算出现重复消费的情况,就会导致主键冲突,避免数据库出现脏数据。
  (2)再比如,你拿到这个消息做redis的set的操作,那就容易了,不用解决,因为你无论set几次结果都是一样的,set操作本来就算幂等操作。
  (3)如果上面两种情况还不行,上大招。准备一个第三方介质,来做消费记录。以redis为例,给消息分配一个全局id,只要消费过该消息,将<id,message>以K-V形式写入redis。那消费者开始消费前,先去redis中查询有没消费记录即可。

4.如何保证消费的可靠性传输?

分析:我们在使用消息队列的过程中,应该做到消息不能多消费,也不能少消费。如果无法做到可靠性传输,可能给公司带来千万级别的财产损失。同样的,如果可靠性传输在使用过程中,没有考虑到,这不是给公司挖坑么,你可以拍拍屁股走了,公司损失的钱,谁承担。还是那句话,认真对待每一个项目,不要给公司挖坑。

回答:其实这个可靠性传输,每种MQ都要从三个角度来分析:生产者弄丢数据、消息队列弄丢数据、消费者弄丢数据

RabbitMQ为列

(1)生产者丢数据
从生产者弄丢数据这个角度来看,RabbitMQ提供transaction和confirm模式来确保生产者不丢消息。
transaction机制就是说,发送消息前,开启事务(channel.txSelect()),然后发送消息,如果发送过程中出现什么异常,事物就会回滚(channel.txRollback()),如果发送成功则提交事物(channel.txCommit())。

然而缺点就是吞吐量下降了。因此生产上用confirm模式的居多。一旦channel进入confirm模式,所有在该信道上面发布的消息都将会被指派一个唯一的ID(从1开始),一旦消息被投递到所有匹配的队列之后,rabbitMQ就会发送一个Ack给生产者(包含消息的唯一ID),这就使得生产者知道消息已经正确到达目的队列了.如果rabiitMQ没能处理该消息,则会发送一个Nack消息给你,你可以进行重试操作,而且不会影响吞吐量,因为transaction事务操作会造成阻塞,而confirm不会。处理Ack和Nack的代码如下所示:

channel.addConfirmListener(new ConfirmListener() {  
           @Override  
           public void handleNack(long deliveryTag, boolean multiple) throws IOException {  
               System.out.println("nack: deliveryTag = "+deliveryTag+" multiple: "+multiple);  
           }  
           @Override  
           public void handleAck(long deliveryTag, boolean multiple) throws IOException {  
               System.out.println("ack: deliveryTag = "+deliveryTag+" multiple: "+multiple);  
           }  
  });

(2)消息队列丢数据
处理消息队列丢数据的情况,一般是开启持久化磁盘的配置。这个持久化配置可以和confirm机制配合使用,你可以在消息持久化磁盘后,再给生产者发送一个Ack信号。这样,如果消息持久化磁盘之前,rabbitMQ阵亡了,那么生产者收不到Ack信号,生产者会自动重发。

那么如何持久化呢,这里顺便说一下吧,其实也很容易,就下面两步

1、将queue的持久化标识durable设置为true,则代表是一个持久的队列
2、发送消息的时候将deliveryMode=2

这样设置以后,rabbitMQ就算挂了,重启后也能恢复数据

(3)消费者丢数据

消费者丢数据一般是因为采用了自动确认消息模式。这种模式下,消费者会自动确认收到信息。这时rahbitMQ会立即将消息删除,这种情况下如果消费者出现异常而没能处理该消息,就会丢失该消息。 至于解决方案,采用手动确认消息即可

itMQ阵亡了,那么生产者收不到Ack信号,生产者会自动重发。

那么如何持久化呢,这里顺便说一下吧,其实也很容易,就下面两步

1、将queue的持久化标识durable设置为true,则代表是一个持久的队列
2、发送消息的时候将deliveryMode=2

这样设置以后,rabbitMQ就算挂了,重启后也能恢复数据

(3)消费者丢数据

消费者丢数据一般是因为采用了自动确认消息模式。这种模式下,消费者会自动确认收到信息。这时rahbitMQ会立即将消息删除,这种情况下如果消费者出现异常而没能处理该消息,就会丢失该消息。 至于解决方案,采用手动确认消息即可

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值