SpringBoot整合RabbitMQ之 典型应用场景实战一

实战前言

RabbitMQ 做为目前应用至关普遍的消息中间件,在企业级应用、微服务应用中充当着重要的角色。特别是在一些典型的应用场景以及业务模块中具备重要的做用,好比业务服务模块解耦、异步通讯、高并发限流、超时业务、数据延迟处理等。html

其中课程的学习连接地址:https://edu.csdn.net/course/detail/9314 spring

RabbitMQ 官网拜读

首先,让咱们先拜读 RabbitMQ 官网的技术开发手册以及相关的 Features,感兴趣的朋友能够耐心的阅读其中的相关介绍,相信会有必定的收获,地址可见:数据库

http://www.rabbitmq.com/getstarted.html后端

阅读该手册过程当中,咱们能够得知 RabbitMQ 其实核心就是围绕 “消息模型” 来展开的,其中就包括了组成消息模型的相关组件:生产者,消费者,队列,交换机,路由,消息等!而咱们在实战应用中,实际上也是牢牢围绕着 “消息模型” 来展开撸码的!缓存

下面,我就介绍一下这一消息模型的演变历程,固然,这一历程在 RabbitMQ 官网也是能够窥览获得的!springboot

enter image description here

enter image description here

enter image description here

上面几个图就已经概述了几个要点,并且,这几个要点的含义能够说是字如其名!服务器

  1. 生产者:发送消息的程序
  2. 消费者:监听接收消费消息的程序
  3. 消息:一串二进制数据流
  4. 队列:消息的暂存区/存储区
  5. 交换机:消息的中转站,用于接收分发消息。其中有 fanout、direct、topic、headers 四种
  6. 路由:至关于密钥/第三者,与交换机绑定便可路由消息到指定的队列!

正如上图所展现的消息模型的演变,接下来咱们将以代码的形式实战各类典型的业务场景!微信

SpringBoot 整合 RabbitMQ 实战

工欲善其事,必先利其器。咱们首先须要借助 IDEA 的 Spring Initializr 用 Maven 构建一个 SpringBoot 的项目,并引入 RabbitMQ、Mybatis、Log4j 等第三方框架的依赖。搭建完成以后,能够简单的写个 RabbitMQController 测试一下项目是否搭建是否成功(能够暂时用单模块方式构建)多线程

紧接着,咱们进入实战的核心阶段,在项目或者服务中使用 RabbitMQ,其实无非是有几个核心要点要紧紧把握住,这几个核心要点在撸码过程当中须要“时刻的游荡在本身的脑海里”,其中包括:并发

  1. 我要发送的消息是什么
  2. 我应该须要建立什么样的消息模型:DirectExchange+RoutingKey?TopicExchange+RoutingKey?等
  3. 我要处理的消息是实时的仍是须要延时/延迟的?
  4. 消息的生产者须要在哪里写,消息的监听消费者须要在哪里写,各自的处理逻辑是啥

基于这样的几个要点,咱们先小试牛刀一番,采用 RabbitMQ 实战异步写日志与异步发邮件。固然啦,在进行实战前,咱们须要安装好 RabbitMQ 及其后端控制台应用,并在项目中配置一下 RabbitMQ 的相关参数以及相关 Bean 组件。

RabbitMQ 安装完成后,打开后端控制台应用:http://localhost:15672  输入guest guest 登陆,看到下图即表示安装成功

enter image description here

而后是项目配置文件层面的配置 application.properties

spring.rabbitmq.host=127.0.0.1
spring.rabbitmq.port=5672
spring.rabbitmq.username=guest
spring.rabbitmq.password=guest
spring.rabbitmq.listener.concurrency=10
spring.rabbitmq.listener.max-concurrency=20
spring.rabbitmq.listener.prefetch=5

其中,后面三个参数主要是用于“并发量的配置”,表示:并发消费者的初始化值,并发消费者的最大值,每一个消费者每次监听时可拉取处理的消息数量。

接下来,咱们须要以 Configuration 的方式配置 RabbitMQ 并以 Bean 的方式显示注入 RabbitMQ 在发送接收处理消息时相关 Bean 组件配置其中典型的配置是 RabbitTemplate 以及 SimpleRabbitListenerContainerFactory,前者是充当消息的发送组件,后者是用于管理  RabbitMQ监听器listener 的容器工厂,其代码以下:

@Configuration
    public class RabbitmqConfig {
    private static final Logger log= LoggerFactory.getLogger(RabbitmqConfig.class);

    @Autowired
    private Environment env;

    @Autowired
    private CachingConnectionFactory connectionFactory;

    @Autowired
    private SimpleRabbitListenerContainerFactoryConfigurer factoryConfigurer;

    /**
     * 单一消费者
     * @return
     */
    @Bean(name = "singleListenerContainer")
    public SimpleRabbitListenerContainerFactory listenerContainer(){
        SimpleRabbitListenerContainerFactory factory = new SimpleRabbitListenerContainerFactory();
        factory.setConnectionFactory(connectionFactory);
        factory.setMessageConverter(new Jackson2JsonMessageConverter());
        factory.setConcurrentConsumers(1);
        factory.setMaxConcurrentConsumers(1);
        factory.setPrefetchCount(1);
        factory.setTxSize(1);
        factory.setAcknowledgeMode(AcknowledgeMode.AUTO);
        return factory;
    }

    /**
     * 多个消费者
     * @return
     */
    @Bean(name = "multiListenerContainer")
    public SimpleRabbitListenerContainerFactory multiListenerContainer(){
        SimpleRabbitListenerContainerFactory factory = new SimpleRabbitListenerContainerFactory();
        factoryConfigurer.configure(factory,connectionFactory);
        factory.setMessageConverter(new Jackson2JsonMessageConverter());
        factory.setAcknowledgeMode(AcknowledgeMode.NONE);
        factory.setConcurrentConsumers(env.getProperty("spring.rabbitmq.listener.concurrency",int.class));
        factory.setMaxConcurrentConsumers(env.getProperty("spring.rabbitmq.listener.max-concurrency",int.class));
        factory.setPrefetchCount(env.getProperty("spring.rabbitmq.listener.prefetch",int.class));
        return factory;
    }

    @Bean
    public RabbitTemplate rabbitTemplate(){
        connectionFactory.setPublisherConfirms(true);
        connectionFactory.setPublisherReturns(true);
        RabbitTemplate rabbitTemplate = new RabbitTemplate(connectionFactory);
        rabbitTemplate.setMandatory(true);
        rabbitTemplate.setConfirmCallback(new RabbitTemplate.ConfirmCallback() {
            @Override
            public void confirm(CorrelationData correlationData, boolean ack, String cause) {
                log.info("消息发送成功:correlationData({}),ack({}),cause({})",correlationData,ack,cause);
            }
        });
        rabbitTemplate.setReturnCallback(new RabbitTemplate.ReturnCallback() {
            @Override
            public void returnedMessage(Message message, int replyCode, String replyText, String exchange, String routingKey) {
                log.info("消息丢失:exchange({}),route({}),replyCode({}),replyText({}),message:{}",exchange,routingKey,replyCode,replyText,message);
            }
        });
        return rabbitTemplate;
    }}

RabbitMQ 实战:业务模块解耦以及异步通讯

在一些企业级系统中,咱们常常能够见到一个执行 function 一般是由许多子模块组成的,这个 function 在执行过程当中,须要 同步的将其代码从头开始执行到尾,即执行流程是 module_A -> module_B -> module_C -> module_D,典型的案例能够参见汇编或者 C 语言等面向过程语言开发的应用,如今的一些 JavaWeb 应用也存在着这样的写法。

而咱们知道,这个执行流程其实对于整个 function 来说是有必定的弊端的,主要有几点:

  1. 整个 function 的执行响应时间将好久;
  2. 若是某个 module 发生异常而没有处理得当,可能会影响其余 module 甚至整个 function 的执行流程与结果;
  3. 整个 function 中代码可能会很冗长,模块与模块之间可能须要进行强通讯以及数据的交互,出现问题时难以定位与维护,甚至会陷入 “改一处代码而动全身”的尴尬境地!

故而,咱们须要想办法进行优化,咱们须要将强关联的业务模块解耦以及某些模块之间实行异步通讯!下面就以两个场景来实战咱们的优化措施!

场景一:异步记录用户操做日志

对于企业级应用系统或者微服务应用中,咱们常常须要追溯跟踪记录用户的操做日志,而这部分的业务在某种程度上是不该该跟主业务模块耦合在一块儿的,故而咱们须要将其单独抽出并以异步的方式与主模块进行异步通讯交互数据。

下面咱们就用 RabbitMQ 的 DirectExchange+RoutingKey 消息模型也实现“用户登陆成功记录日志”的场景。如前面所言,咱们须要在脑海里回荡着几个要点:

  • 消息模型:DirectExchange+RoutingKey 消息模型
  • 消息:用户登陆的实体信息,包括用户名,登陆事件,来源的IP,所属日志模块等信息
  • 发送接收:在登陆的 Controller 中实现发送,在某个 listener 中实现接收并将监听消费到的消息入数据表;实时发送接收

首先咱们须要在上面的 RabbitmqConfig 类中建立消息模型:包括 Queue、Exchange、RoutingKey 等的创建,代码以下:

enter image description here

上图中 env 获取的信息,咱们须要在 application.properties 进行配置,其中 mq.env=local

enter image description here

此时,咱们将整个项目/服务跑起来,并打开 RabbitMQ 后端控制台应用,便可看到队列以及交换机及其绑定已经创建好了,以下所示:

enter image description here

enter image description here

接下来,咱们须要在 Controller 中执行用户登陆逻辑,记录用户登陆日志,查询获取用户角色视野资源信息等,因为篇幅关系,在这里咱们重点要实现的是用MQ实现 “异步记录用户登陆日志” 的逻辑,即在这里 Controller 将充当“生产者”的角色,核心代码以下:

@RestController
    public class UserController {

    private static final Logger log= LoggerFactory.getLogger(HelloWorldController.class);

    private static final String Prefix="user";

    @Autowired
    private ObjectMapper objectMapper;

    @Autowired
    private UserMapper userMapper;

    @Autowired
    private UserLogMapper userLogMapper;

    @Autowired
    private RabbitTemplate rabbitTemplate;

    @Autowired
    private Environment env;

    @RequestMapping(value = Prefix+"/login",method = RequestMethod.POST,consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
    public BaseResponse login(@RequestParam("userName") String userName,@RequestParam("password") String password){
        BaseResponse response=new BaseResponse(StatusCode.Success);
        try {
            //TODO:执行登陆逻辑
            User user=userMapper.selectByUserNamePassword(userName,password);
            if (user!=null){
                //TODO:异步写用户日志
                try {
                    UserLog userLog=new UserLog(userName,"Login","login",objectMapper.writeValueAsString(user));
                    userLog.setCreateTime(new Date());
                    rabbitTemplate.setMessageConverter(new Jackson2JsonMessageConverter());
                    rabbitTemplate.setExchange(env.getProperty("log.user.exchange.name"));
                    rabbitTemplate.setRoutingKey(env.getProperty("log.user.routing.key.name"));

                    Message message=MessageBuilder.withBody(objectMapper.writeValueAsBytes(userLog)).setDeliveryMode(MessageDeliveryMode.PERSISTENT).build();
                    message.getMessageProperties().setHeader(AbstractJavaTypeMapper.DEFAULT_CONTENT_CLASSID_FIELD_NAME, MessageProperties.CONTENT_TYPE_JSON); 
                    rabbitTemplate.convertAndSend(message);         
                }catch (Exception e){
                    e.printStackTrace();
                }

                //TODO:塞权限数据-资源数据-视野数据
            }else{
                response=new BaseResponse(StatusCode.Fail);
            }
        }catch (Exception e){
            e.printStackTrace();
        }
        return response;
    }}

在上面的“发送逻辑”代码中,其实也体现了咱们最开始介绍的演进中的几种消息模型,好比咱们是将消息发送到 Exchange 的而不是 Queue,消息是以二进制流的形式进行传输等等。当用 postman 请求到这个 controller 的方法时,咱们能够在 RabbitMQ 的后端控制台应用看到一条未确认的消息,经过 GetMessage 便可看到其中的详情,以下:

enter image description here

最后,咱们将开发消费端的业务代码,以下:

@Component
    public class CommonMqListener {

    private static final Logger log= LoggerFactory.getLogger(CommonMqListener.class);

    @Autowired
    private ObjectMapper objectMapper;

    @Autowired
    private UserLogMapper userLogMapper;

    @Autowired
    private MailService mailService;

    /**
     * 监听消费用户日志
     * @param message
     */
    @RabbitListener(queues = "${log.user.queue.name}",containerFactory = "singleListenerContainer")
    public void consumeUserLogQueue(@Payload byte[] message){
        try {
            UserLog userLog=objectMapper.readValue(message, UserLog.class);
            log.info("监听消费用户日志 监听到消息: {} ",userLog);
            //TODO:记录日志入数据表
            userLogMapper.insertSelective(userLog);
        }catch (Exception e){
            e.printStackTrace();
        }
    }

将服务跑起来以后,咱们便可监听消费到上面 Queue 中的消息,即当前用户登陆的信息,并且,咱们也能够看到“记录用户登陆日志”的逻辑是由一条异于主业务线程的异步线程去执行的:

enter image description here

“异步记录用户操做日志”的案例我想足以用于诠释上面所讲的相关理论知识点了,在后续篇章中,因为篇幅限制,我将重点介绍其核心的业务逻辑!

场景二:异步发送邮件

发送邮件的场景,其实也是比较常见的,好比用户注册须要邮箱验证,用户异地登陆发送邮件通知等等,在这里我以 RabbitMQ 实现异步发送邮件。实现的步骤跟场景一几乎一致!

1. 消息模型的建立

enter image description here

2. 配置信息的建立

enter image description here

3. 生产端

enter image description here

4. 消费端

enter image description here

RabbitMQ 实战:并发量配置与消息确认机制

实战背景

对于消息模型中的 listener 而言,默认状况下是“单消费实例”的配置,即“一个 listener 对应一个消费者”,这种配置对于上面所讲的“异步记录用户操做日志”、“异步发送邮件”等并发量不高的场景下是适用的。可是在对于秒杀系统、商城抢单等场景下可能会显得很吃力!

咱们都知道,秒杀系统跟商城抢单均有一个共同的明显的特征,即在某个时刻会有成百上千万的请求到达咱们的接口,即瞬间这股巨大的流量将涌入咱们的系统,咱们能够采用下面一图来大体体现这一现象:

enter image description here

当到了“开始秒杀”、“开始抢单”的时刻,此时系统可能会出现这样的几种现象:

  • 应用系统配置承载不了这股瞬间流量,致使系统直接挂掉,即传说中的“宕机”现象;
  • 接口逻辑没有考虑并发状况,数据库读写锁发生冲突,致使最终处理结果跟理论上的结果数据不一致(如商品存库量只有 100,可是高并发状况下,实际表记录的抢到的用户记录数据量却远远大于 100);
  • 应用占据服务器的资源直接飙高,如 CPU、内存、宽带等瞬间直接飙升,致使同库同表甚至可能同 host 的其余服务或者系统出现卡顿或者挂掉的现象;

因而乎,咱们须要寻找解决方案!对于目前来说,网上均有诸多比较不错的解决方案,在此我顺便提一下咱们的应用系统采用的经常使用解决方案,包括:

  • 咱们会将处理抢单的总体业务逻辑独立、服务化并作集群部署;
  • 咱们会将那股巨大的流量拒在系统的上层,即将其转移至 MQ 而不直接涌入咱们的接口,从而减小数据库读写锁冲突的发生以及因为接口逻辑的复杂出现线程堵塞而致使应用占据服务器资源飙升;
  • 咱们会将抢单业务所在系统的其余同数据源甚至同表的业务拆分独立出去服务化,并基于某种 RPC 协议走 HTTP 通讯进行数据交互、服务通讯等等;
  • 采用分布式锁解决同一时间同个手机号、同一时间同个 IP 刷单的现象;

下面,咱们用 RabbitMQ 来实战上述的第二点!即咱们会在“请求” -> "处理抢单业务的接口" 中间架一层消息中间件作“缓冲”、“缓压”处理,以下图所示:

enter image description here

并发量配置与消息确认机制

正如上面所讲的,对于抢单、秒杀等高并发系统而言,若是咱们须要用 RabbitMQ 在 “请求” - “接口” 之间充当限流缓压的角色,那便须要咱们对 RabbitMQ 提出更高的要求,即支持高并发的配置,在这里咱们须要明确一点,“并发消费者”的配置实际上是针对 listener 而言,当配置成功后,咱们能够在 MQ 的后端控制台应用看到 consumers 的数量,以下所示:

enter image description here

其中,这个 listener 在这里有 10 个 consumer 实例的配置,每一个 consumer 能够预监听消费拉取的消息数量为 5 个(若是同一时间处理不完,会将其缓存在 mq 的客户端等待处理!)

另外,对于某些消息而言,咱们有时候须要严格的知道消息是否已经被 consumer 监听消费处理了,即咱们有一种消息确认机制来保证咱们的消息是否已经真正的被消费处理。在 RabbitMQ 中,消息确认处理机制有三种:Auto - 自动、Manual - 手动、None - 无需确认,而确认机制须要 listener 实现 ChannelAwareMessageListener 接口,并重写其中的确认消费逻辑。在这里咱们将用 “手动确认” 的机制来实战用户商城抢单场景。

1.在 RabbitMQConfig 中配置确认消费机制以及并发量的配置

enter image description here

2.消息模型的配置信息

enter image description here

3.RabbitMQ 后端控制台应用查看此队列的并发量配置

enter image description here

4.listener 确认消费处理逻辑:在这里咱们须要开发抢单的业务逻辑,即“只有当该商品的库存 >0 时,抢单成功,扣减库存量,并将该抢单的用户信息记录入表,异步通知用户抢单成功!”

enter image description here

enter image description here

紧接着咱们采用 CountDownLatch 模拟产生高并发时的多线程请求(或者采用 jmeter 实施压测也能够!),每一个请求将携带产生的随机数:充当手机号 -> 充当消息,最终入抢单队列!在这里,我模拟了 50000 个请求,至关于 50000 手机号同一时间发生抢单的请求,而设置的产品库存量为 100,这在 product 数据库表便可设置

enter image description here

6.将抢单请求的手机号信息压入队列,等待排队处理

enter image description here

7.在最后咱们写个 Junit 或者写个 Controller,进行 initService.generateMultiThread(); 调用模拟产生高并发的抢单请求便可

@RestController
    public class ConcurrencyController {

    private static final Logger log= LoggerFactory.getLogger(HelloWorldController.class);

    private static final String Prefix="concurrency";

    @Autowired
    private InitService initService;

    @RequestMapping(value = Prefix+"/robbing/thread",method = RequestMethod.GET)
    public BaseResponse robbingThread(){
        BaseResponse response=new BaseResponse(StatusCode.Success);
        initService.generateMultiThread();
        return response;
    }}

8.最后,咱们固然是跑起来,在控制台咱们能够观察到系统不断的在产生新的请求(线程)– 至关于不断的有抢单的手机号涌入咱们的系统,而后入队列,listener 监听到请求以后消费处理抢单逻辑!最后咱们能够观察两张数据库表:商品库存表、商品成功抢单的用户记录表 - 只有当库存表中商品对应的库存量为 0、商品成功抢单的用户记录恰好 100 时 即表示咱们的实战目的以及效果已经达到了!!

enter image description here

总结:如此一来,咱们便将 request 转移到咱们的 mq,在必定程度缓解了咱们的应用以及接口的压力!固然,实际状况下,咱们的配置可能远远不仅代码层次上的配置,好比咱们的 mq 可能会作集群配置、负载均衡、商品库存的更新可能会考虑分库分表、库存更新可能会考虑独立为库存 Dubbo 服务并经过 Rest Api 异步通讯交互并独立部署等等。这些优化以及改进的目的其实无非是为了能限流、缓压、保证系统稳定、数据的一致等!而咱们的 MQ,在其中能够起到不可磨灭的做用,其字如其名:“消息队列”,而队列具备 “先进先出” 的特色,故而全部进入 MQ 的消息都将 “乖巧” 的在 MQ 上排好队,先来先排队,先来先被处理消费,由此一来至少能够避免 “瞬间时刻一窝蜂的 request 涌入咱们的接口” 的状况!

附注:在用 RabbitMQ 实战上述高并发抢单解决方案,其实我也在数据库层面进行了优化,即在读写存库时采用了“相似乐观锁”的写法,保证:抢单的请求到来时有库存,更新存库时保证有库存能够被更新!

彩蛋:本博文介绍了几个典型的RabbitMQ实战的业务场景,相关源码数据库能够来这里下载

(1)https://download.csdn.net/download/u013871100/10654482

(2)https://pan.baidu.com/s/1KUuz_eeFXOKF3XRMY2Jcew
学习过程有任何问题都可以与我交流,QQ:1948831260!下一篇博文将继续典型应用场景实战之死信队列的应用。感兴趣的童鞋能够关注一下个人微信公众号!