淘宝购买商品后系统如何处理?认识消息服务之RabbitMQ基础,打造Direct、Fanout、Topic三种消息队列。(精选长文)

#当我们在淘宝购买商品后,系统会如何处理呢?

传统项目,我们一般会进行如下操作:

  • 同步发送订单到商家。

  • 同步在数据库中扣减库存。

  • 更新缓存使前后端数据一致。

而面对淘宝这样超级用量的系统,我们如果采用传统项目模式,会出现如下问题:

  • 一个订单需求会调用支付、库存、缓存、订单等多个系统或者模块,相互之间的调用多而复杂,维护起来非常繁琐。(解决办法:解耦,降低系统耦合度
  • 同步执行支付、库存、缓存、订单多个系统,总耗时为上述系统执行的总时长。一个系统卡壳会影响整个执行效率,稳定性弱,效率低。(解决办法:异步,提升效率
  • 面对整点购、秒杀抢购、双十一等瞬时并发量或晚上购物高峰大吞吐量等情况,如采用传统方式,大量请求涌入数据库,将会造成系统崩溃,宕机。(解决办法:削峰,降低并发请求)

这时候,就需要消息服务(消息队列Message Queue)来解决上述问题,实现解耦、异步、削峰目的。

消息队列是一种进程间异步通信方式,消息生产者在生产消息者后,会将消息保存在队列中,直到有消息消费者来取走它,即消息的发送者和接收者不需要同时与消息队列交互。目前开源的消息服务非常多,如ActiveMQ、RabbitMQ、RocketMQ等,这些产品也就是我们常说的消息中间件。

RabbitMQ是当前最主流的实现了AMQP(高级消息队列协议)的开源消息中间件之一,基于Erlang语言编写,可用于在分布式系统中存储转发消息,具有可靠性、支持多协议、高可用、灵活路由、集群部署、可视化管理等特点。

简单来说,当生产者(productor)产生大量的数据时,消费者(consumer)无法快速的消费信息,那么就需要一个类似于中间件的代理服务器,用来处理和保存这些数据,RabbitMQ就扮演了这个角色。

回到淘宝购买商品后系统如何处理这个问题,这时我们可以采用RabbitMQ中的Fanout广播模式来实现上述所有功能。

当我们购买商品后,RabbitMQ会将我们的购买信息广播到多个消息消费者中,消息消费者获得消息后即可以开始完成各自系统相应的功能,如:

  • 订单消息队列>>商家  (消息消费者1)>>拣配发货
  • 支付消息队列>>支付端、客户端  (消息消费者2)>>扣减花呗可用额度,提示消费还款
  • 数据库消息队列>>多个数据库  (消息消费者3)>>扣减库存、扣减客户优惠券、红包(如有)
  • 缓存消息队列>>多个缓存  (消息消费者4)>>显示最新库存,更新库存对应优惠信息
  • ......

RabbitMQ主要用于生产者和消费者之间的双向解耦。

#RabbitMQ安装部署教程:

①下载安装Erlang,因为RabbitMQ是基于Erlang语言编写,所以需要先下载安装Erlang环境。

根据自身系统选择安装包,点击安装包选择自己安装目录,然后下一步直到完成。Erlang Programming Language

②下载RabbitMQ,根据自身系统选择安装包,点击安装包选择自己安装目录,下一步直到完成。

开始菜单会多出以下快捷方式,分别为命令菜单,RabbitMQ启动,RabbitMQ停止。

RabbitMQ Command Prompt (sbin dir)

RabbitMQ Service - start

RabbitMQ Service - stop

Downloading and Installing RabbitMQ — RabbitMQ

③配置RabbitMQ,双击RabbitMQ Service - start启动RabbitMQ,双击RabbitMQ Command Prompt进入命令菜单。

默认登录账号和密码是guest/guest,这里我们建议新建一个admin管理员账号。

我们使用命令创建一个管理员账号及密码:rabbitmqctl add_user admin 12345678

将其设置为管理员角色:rabbitmqctl set_user_tags admin administrator

增加账号远程登录及配置、写、读权限:rabbitmqctl set_permissions -p / admin ".*" ".*" ".*"

开启WEB管理插件,实现可视化:rabbitmq-plugins enable rabbitmq_management

查询状态:rabbitmqctl status

④通过网址http://localhost:15672/ 进入登录界面,输入admin/12345678或者guest/guest登录WEB管理界面。安装配置启动成功。

这里需要注意的是网页登录默认端口为15672,后端登录默认端口为5672。

#了解Direct、Fanout、Topic三种消息队列

RabbitMQ所有消息生产者提交的消息都会交由Exchange交换机进行再分配,Exchange会根据不同的策略将消息分发到不同的Queue消息队列中。

Direct、Fanout、Topic是使用频率最高的消息队列,Header队列使用较少,本文不再介绍。

直连交换机(Direct exchange)将消息队列绑定到一个DirectExchange上,当一条消息到达时会被转发到与该条消息routing_key相同的Queue上,例如消息队列名为“green”,则routing_key为“green”的消息会被该消息队列接收。
扇形交换机(Fanout exchange)将所有到达FanoutExchange的消息转发给所有与它绑定的Queue上,routing_key失效。可广播消息到所有队列,没有任何处理,速度最快。
主题交换机(Topic exchange)在直连交换机基础上增加模式匹配,也就是对routing_key进行正则模式匹配,匹配成功则转发到一个或多个Queue上。*代表一个单词,#代表多个单词,该模式比较灵活。

#代码实现Direct、Fanout、Topic三种消息队列,基于Spring-boot-AMQP整合RabbitMQ3.95

首先添加Maven依赖:

        <!--rabbitmq依赖-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-amqp</artifactId>
        </dependency>

其次将RabbitMQ配置写入Spring-boot配置文件application.properties中:

①基础配置:连接信息,登录账号密码信息等。(必要)

##RabbitMQ基础配置------
spring.rabbitmq.host=127.0.0.1
spring.rabbitmq.port=5672
spring.rabbitmq.username=admin
spring.rabbitmq.password=12345678
spring.rabbitmq.connection-timeout=15000

②RabbitMQ监听配置: (非必要)本文是基础教程,所以默认关闭所有监听设置及回调函数引用。

confirm监听通过RabbitTemplate.ConfirmCallback回调函数记录消息消费情况;

return监听,通过RabbitTemplate.ReturnsCallback回调函数记录消息被路由情况;

Mandatory设置,return监听后消息没有找到合适队列会被return监听,而不会自动删除。

##RabbitMQ监听配置------
#开启confirm监听,通过RabbitTemplate.ConfirmCallback回调函数记录消息消费情况
#spring.rabbitmq.publisher-confirms=true
#开启return监听,通过RabbitTemplate.ReturnsCallback回调函数记录消息被路由情况
#spring.rabbitmq.publisher-returns=true
#开启return监听后消息没有被路由到合适队列的情况下会被return监听,而不会自动删除
#spring.rabbitmq.template.mandatory=true

③RabbitMQ消息消费端配置: (非必要)本文是基础教程,所以默认关闭所有监听设置及回调函数引用。

acknowledge-mode,设置消息消费ACK模式(用于监听消息消费端执行情况),NONE无ACK,MANUAL人工获取,AUTO自动获取;

concurrency,设置消息消费端的监听个数和最大个数,用于控制消费端的并发情况;

prefetch,开启限流,指定单个请求每次最多处理消息个数。

##RabbitMQ消息消费端配置------
#配置消息消费ACK模式,NONE:无ack默认成功,MANUAL需要人为获取channel之后调用方法向server发送ack,AUTO由spring-rabbit依据消息处理逻辑是否抛出异常自动发送ack
#spring.rabbitmq.listener.simple.acknowledge-mode=manual
#设置消息消费端的监听个数和最大个数,用于控制消费端的并发情况
#spring.rabbitmq.listener.simple.concurrency=1
#spring.rabbitmq.listener.simple.max-concurrency=10
#在单个请求中处理的消息个数,例如:开启限流,指定每次最多只能处理两条消息
#spring.rabbitmq.listener.simple.prefetch=2

#Direct直连消息队列精简代码,包括消息服务配置Config,消息消费端Service,实现端Controller

package com.example.demohelloworld.RabbitMq.Direct;
import org.springframework.amqp.core.Binding;
import org.springframework.amqp.core.BindingBuilder;
import org.springframework.amqp.core.DirectExchange;
import org.springframework.amqp.core.Queue;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
 * DirectExchange的路由策略是将消息队列绑定到一个DirectExchange上,当一条消息到达时
 * 会被转发到与该条信息routing key相同的Queue上,例如消息队列名为hello-queue,则
 * routing key为hello-queue的消息会被消息队列接收。
 */
@Configuration
public class RabbitDirectConfig {
    //设定Exchange交换机名字
    public final static String directname="my-direct";
    @Bean
    DirectExchange directExchange(){
        //三个参数分别为:名字、重启后是否依然有效、长期未用是否删除
        return new DirectExchange(directname,true,false);
    }
    //创建一个消息队列
    @Bean
    Queue queueDirect(){
        return new Queue("hello-queue");
    }
    //创建一个Binding对象将Exchange和Queue绑定在一起
    @Bean
    Binding bindingDirect(){
        return BindingBuilder.bind(queueDirect())
                .to(directExchange())
                .with("direct");
    }
}

Config代码思路,创建一个交换机directExchange,创建一个消息队列queueDirect,创建一个绑定对象bindingDirect将Exchange和Queue绑定在一起。

package com.example.demohelloworld.RabbitMq.Direct;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Component;
@Component
public class RabbitDirectService {
    //定义日志输出
    Logger logger= LoggerFactory.getLogger(this.getClass());
    //指定一个消息消费方法
    @RabbitListener(queues = "hello-queue")
    public void handler1(String msg){
        logger.info ("DirectReceiver:"+msg);
    }
}

Service代码思路:监听队列名为"hello-queue"的消息队列,指定一个消息消费方法logger日志输出,将所接收到的消息内容在后台输出(生产环境中则为具体执行的业务逻辑)。

package com.example.demohelloworld.RabbitMq.Direct;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class RabbitDirectController {
    @Autowired
    RabbitTemplate rabbitTemplate;
    @GetMapping("/direct")
    public void direct(){
        rabbitTemplate.convertAndSend("hello-queue","hello direct!");
    }
}

Controller代码思路:注入RabbitTemplate对象进行消息发送(模拟消息生产者发送消息),convertAndSend中参数为消息队列名称和消息具体内容。

最后启动项目开始测试,打开浏览器输入http://localhost:8080/direct,生产者发送的消息已经被消息消费者接收。

#Fanout广播消息队列精简代码 

package com.example.demohelloworld.RabbitMq.Fanout;
import org.springframework.amqp.core.Binding;
import org.springframework.amqp.core.BindingBuilder;
import org.springframework.amqp.core.FanoutExchange;
import org.springframework.amqp.core.Queue;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
 * FanoutExchange的数据交换策略时是把所有到达的消息转发给所有与它绑定的Queue,
 * 在这种策略中,routing key将不起任何作用。
 * 一条消息发出去以后,所有和该Exchange绑定的Queue都收到了消息。
 */
@Configuration
public class RabbitFanoutConfig {
    public final static String fanoutname="my-fanout";
    @Bean
    FanoutExchange fanoutExchange(){
        return new FanoutExchange(fanoutname,true,false);
    }
    @Bean
    Queue queueOne(){
        return new Queue("queue-one");
    }
    @Bean
    Queue queueTwo(){
        return new Queue("queue-two");
    }
    @Bean
    Binding bindingoOne(){
        return BindingBuilder.bind(queueOne())
                .to(fanoutExchange());
    }
    @Bean
    Binding bindingTwo(){
        return BindingBuilder.bind(queueTwo())
                .to(fanoutExchange());
    }
}

Config代码思路,创建一个交换机fanoutExchange,创建多个消息队列Queue并全部绑定到fanoutExchange上,实现广播效果。

package com.example.demohelloworld.RabbitMq.Fanout;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Component;
@Component
public class RabbitRanoutService {
    Logger logger= LoggerFactory.getLogger(this.getClass());
    @RabbitListener(queues = "queue-one")
    public void handlder1(String msg){
        logger.info("FanoutReceiverHandler1:"+msg);
    }
    @RabbitListener(queues = "queue-two")
    public void handlder2(String msg){
        logger.info("FanoutReceiverHandler2:"+msg);
    }
}

Service代码思路:两个(多个)消息消费者分别消费消息队列中的消息,将所接收到的消息内容在后台输出(生产环境中则为具体执行的业务逻辑)。

package com.example.demohelloworld.RabbitMq.Fanout;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class RabbitFanoutController {
    @Autowired
    RabbitTemplate rabbitTemplate;
    @GetMapping("/fanout")
    //发送消息时,不需要指定routing key,指定Exchange即可,routing key可以传一个null值
    public void fanout(){
        rabbitTemplate.convertAndSend(RabbitFanoutConfig.fanoutname,null,"hello fanout!");
    }
}

Controller代码思路:注入RabbitTemplate对象进行消息发送(模拟消息生产者发送消息),convertAndSend发送消息时不需要routingkey,指定Exchange交换机名字即可,routingkey可以直接传一个null。

最后启动项目开始测试,打开浏览器输入http://localhost:8080/fanout,生产者发送的消息已经被广播。

#Topic主题消息队列精简代码 

package com.example.demohelloworld.RabbitMq.Topic;
import org.springframework.amqp.core.Binding;
import org.springframework.amqp.core.BindingBuilder;
import org.springframework.amqp.core.Queue;
import org.springframework.amqp.core.TopicExchange;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
 * TopicExchange时比较复杂也比较灵活的路由策略。Queue通过routing key绑定到 TopicExchange上
 * 当消息到达后,TopicExchange根据消息的routing key将消息路由到一个或者多个Queue上
 */
@Configuration
public class RabbitTopicConfig {
    public final static String topicname="my-topic";
    @Bean
    TopicExchange topicExchange(){
        return new TopicExchange(topicname,true,false);
    }
    @Bean
    Queue sina(){
        return new Queue("sina");
    }
    @Bean
    Queue xinhua(){
        return new Queue("xinhua");
    }
    @Bean
    Queue baidu(){
        return new Queue("baidu");
    }
    //sina.#表示routing key包含"sina"的,都将被路由到sina的Queue上
    @Bean
    Binding sinaBinding(){
        return BindingBuilder.bind(sina())
                .to(topicExchange())
                .with("sina.#");
    }
    @Bean
    Binding xinhuaBinding(){
        return BindingBuilder.bind(xinhua())
                .to(topicExchange())
                .with("xinhua.#");
    }
    //#.baidu.#表示routing key包含"baidu"的,都将被路由到baidu的Queue上
    @Bean
    Binding baiduBinding(){
        return BindingBuilder.bind(baidu())
                .to(topicExchange())
                .with("#.baidu.#");
    }
}

Config代码思路,创建一个交换机topicExchange,创建三个消息队列Queue,分别用来存储sina、xinhua、baidu有关消息。三个Queue分别绑定到TopicExchange上,sina.#表示消息队列routingkey凡是以"sina"开头的,都将被路由到名称为"sina"的Queue上。#.baidu.#表示消息队列routingkey中凡是包含"baidu"的,都将被路由到名称为"baidu"的Queue上。

package com.example.demohelloworld.RabbitMq.Topic;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Component;
@Component
public class RabbitTopicService {
    Logger logger = LoggerFactory.getLogger(this.getClass());
    @RabbitListener(queues = "sina")
    public void handler1(String msg){
        logger.info("SinaReceiverHandler:"+msg);
    }
    @RabbitListener(queues = "xinhua")
    public void handler2(String msg){
        logger.info("XinhuaReceiverHandler:"+msg);
    }
    @RabbitListener(queues = "baidu")
    public void handler3(String msg){
        logger.info("BaiduReceiverHandler:"+msg);
    }

Service代码思路:创建三个消息消费者,将所接收到的消息内容在后台输出(生产环境中则为具体执行的业务逻辑)。 

package com.example.demohelloworld.RabbitMq.Topic;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class RabbitTopicController {
    @Autowired
    RabbitTemplate rabbitTemplate;
    @GetMapping("/topic")
    public void topic(){
        //sina.news被路由到sina的Queue上
        rabbitTemplate.convertAndSend(RabbitTopicConfig.topicname,"sina.news","这里是新浪新闻...");
        //xinhua.breaks被路由到xinhua的Queue上
        rabbitTemplate.convertAndSend(RabbitTopicConfig.topicname,"xinhua.breaks","这里是新华突发新闻...");
        //sina.baidu被同时路由到sina和baidu的Queue上
        rabbitTemplate.convertAndSend(RabbitTopicConfig.topicname,"sina.baidu","新浪百度联合报道...");
        //xinhua.baidu被同时路由到xinhua和baidu的Queue上
        rabbitTemplate.convertAndSend(RabbitTopicConfig.topicname,"xinhua.baidu","新华百度联合报道...");
        //news.baidu被路由到baidu的Queue上
        rabbitTemplate.convertAndSend(RabbitTopicConfig.topicname,"news.baidu","这里是百度新闻...");
    }
}

Controller代码思路:routingkey为

sina.news(以sina开头)被路由到sina的Queue上;

xinhua.breaks被路由到xinhua的Queue上;

sina.baidu被同时路由到sina(以sina开头)和baidu(包含baidu)的Queue上;

xinhua.baidu被同时路由到xinhua(以xinhua开头)和baidu(包含baidu)的Queue上;

news.baidu被路由到baidu(包含baidu)的Queue上。

这里就体现了Topic主题模式的灵活性了,可以根据routingkey名字来匹配消息队列。

最后启动项目开始测试,打开浏览器输入http://localhost:8080/topic,生产者发送的消息已经被routingkey相对应的消息消费者接收。

#总结

  • RabbitMQ作为AMQP开源消息中间件,广泛应用于各种生产环境。
  • RabbitMQ也是物联网QMTT最基础的插件,可用于搭建QMTT服务器。
  • 通过本文我们学习了RabbitMQ消息服务的基础知识,掌握三种最基本的消息队列模式。
  • 在生产环境中,我们还需要配置ConfirmCallback和ReturnsCallback两个回调函数用来监控消息消费情况和消息被路由情况,文末附上源码供参考。

ConfirmCallback和ReturnsCallback源码,需要在配置中打开监听配置。

首先配置回调函数服务

package com.example.demo.lazy;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
@Component
public class RabbitCallbackService {
    Logger logger= LoggerFactory.getLogger(this.getClass());
    @Autowired
    RabbitTemplate rabbitTemplate;
    //定义ConfirmCallback,用来监控消息消费情况,acknowledge-mode需要设置为manual人工模式,ack人工输出
    RabbitTemplate.ConfirmCallback confirmCallback = (correlationData, ack, cause) -> {
        logger.info("CorrelationData={}",correlationData);
        logger.info("ack={}",ack);
        if(!ack){
            logger.info("消息消费异常!");
        }else {
            logger.info("消息消费成功!");
        }
    };
    //定义ReturnsCallback,用来监控消息被路由情况
    RabbitTemplate.ReturnsCallback returnsCallback = returned -> logger.info("exchange={}",returned.getExchange(),"routingkey={}",returned.getRoutingKey(),"replycode={}",returned.getReplyCode(),"replytext={}",returned.getReplyText());
}

接下来在实现端引入回调函数服务

package com.example.demo.lazy;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class RabbitCallbankController {
    //注入回调函数服务
    @Autowired
    RabbitCallbackService service;
    @GetMapping("/sendmessage")
    public void sendmessage(){
        //开启return监听后消息没有被路由到合适队列的情况下会被return监听,而不会自动删除
        service.rabbitTemplate.setMandatory(true);//如果已经在properties中配置则不需要
        //注入ConfirmCallback用来监控消息消费情况
        service.rabbitTemplate.setConfirmCallback(service.confirmCallback);
        //ReturnsCallback,来监控消息被路由情况
        service.rabbitTemplate.setReturnsCallback(service.returnsCallback);
        //发送消息服务,自行添加代码即可
        service.rabbitTemplate.convertAndSend(........);
    }
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值