RabbitMQ入门到精通_余胜军版笔记
原笔记链接:https://files.cnblogs.com/files/henuliulei/Rabbitmq%E5%85%A5%E9%97%A8%E5%88%B0%E7%B2%BE%E9%80%9A.zip
原视频链接:2021年RabbitMQ入门到精通 余胜军(通俗易懂版本)_哔哩哔哩_bilibili
MQ架构设计原理
什么是消息中间件
消息中间件基于队列模型实现异步/同步传输数据
作用:可以实现支撑高并发、异步解耦、流量削峰、降低耦合度。
在了解中间件之前,我们先了解一下什么是同步?
首先我们想一下,两个公司之间如果有互相调用接口的业务需求,如果没有引入中间件技术,是怎么实现的呢?
用户发起请求给系统A,系统A接到请求直接调用系统B,系统B返回结果后,系统A才能返回结果给用户,这种模式就是同步调用。
所谓同步调用就是各个系统之间互相依赖,一个系统发送请求,其他系统也会跟着依次进行处理,只有所有系统处理完成后对于用户来讲才算完成了一次请求。只要其他系统出现故障,就会报错给用户。
那么引入中间件后,是如何做到异步调用的呢?
用户发起请求给系统A,此时系统A发送消息给MQ,然后就返回结果给用户,不去管系统B了。然后系统B根据自己的情况,去MQ中获取消息,获取到消息的时候可能已经过了1分钟甚至1小时,再根据消息的指示执行相应的操作。
那么想一想,系统A和系统B互相之间是否有通信?这种调用方式是同步调用吗?
系统A发送消息给中间件后,自己的工作已经完成了,不用再去管系统B什么时候完成操作。而系统B拉去消息后,执行自己的操作也不用告诉系统A执行结果,所以整个的通信过程是异步调用的。
说到这里,我们可以做个总结,消息中间件到底是什么呢?
其实消息中间件就是一个独立部署的系统。可以实现各个系统之间的异步调用。当然它的作用可不止这些,通过它可以解决大量的技术痛点,我们接下来会进行介绍。
消息中间件,总结起来作用有三个:异步化提升性能、降低耦合度、流量削峰。
异步化提升性能
先来说说异步化提升性能,上边我们介绍中间件的时候已经解释了引入中间件后,是如何实现异步化的,但没有解释具体性能是怎么提升的,我们来看一下下边的图。
没有引入中间件的时候,用户发起请求到系统A,系统A耗时20ms,接下来系统A调用系统B,系统B耗时200ms,带给用户的体验就是,一个操作全部结束一共耗时220ms。
如果引入中间件之后呢?看下边的图。
用户发起请求到系统A,系统A耗时20ms,发送消息到MQ耗时5ms,返回结果一共用了25ms,用户体验一个操作只用了25ms,而不用管系统B什么时候去获取消息执行对应操作,这样比较下来,性能自然大大提高
降低耦合度
再来聊聊解耦的场景,看下图。
如果没有引入中间件,那么系统A调用系统B的时候,系统B出现故障,导致调用失败,那么系统A就会接到异常信息,接到异常信息后肯定要再处理一下,返回给用户失败请稍后再试,这时候就得等待系统B的工程师解决问题,一切都解决好后再告知用户可以了,再重新操作一次吧。
这样的架构,两个系统耦合再一起,用户体验极差。
那么我们引入中间件后是什么样的场景呢,看下面的流程:
对于系统A,发送消息后直接返回结果,不再管系统B后边怎么操作。而系统B故障恢复后重新到MQ中拉取消息,重新执行未完成的操作,这样一个流程,系统之间没有影响,也就实现了解耦。
流量削峰
下面我们再聊聊最后一个场景,流量削峰
假如我们的系统A是一个集群,不连接数据库,这个集群本身可以抗下1万QPS
系统B操作的是数据库,这个数据库只能抗下6000QPS,这就导致无论系统B如何扩容集群,都只能抗下6000QPS,它的瓶颈在于数据库。
假如突然系统QPS达到1万,就会直接导致数据库崩溃,那么引入MQ后是怎么解决的呢,见下图:
引入MQ后,对于系统A没有什么影响,给MQ发送消息可以直接发送1万QPS。
此时对于系统B,可以自己控制获取消息的速度,保持在6000QPS一下,以一个数据库能够承受的速度执行操作。这样就可以保证数据库不会被压垮。
当然,这种情况MQ中可能会积压大量消息。但对于MQ来说,是允许消息积压的,等到系统A峰值过去,恢复成1000QPS时,系统B还是在以6000QPS的速度去拉取消息,自然MQ中的消息就慢慢被释放掉了。
这就是流量削峰的过程。在电商秒杀、抢票等等具有流量峰值的场景下可以使用这么一套架构。
传统的http请求存在那些缺点
1.Http请求基于请求与响应的模型,在高并发的情况下,客户端发送大量的请求达到
服务器端有可能会导致我们服务器端处理请求堆积。
2.Tomcat服务器处理每个请求都有自己独立的线程,如果超过最大线程数会将该请求缓存到队列中,如果请求堆积过多的情况下,有可能会导致tomcat服务器崩溃的问题。
所以一般都会在nginx入口实现限流,整合服务保护框架。
http请求处理业务逻辑如果比较耗时的情况下,容易造成客户端一直等待,阻塞等待 过程中会导致客户端超时发生重试策略,有可能会引发幂等性问题。
注意事项:接口是为http协议的情况下,最好不要处理比较耗时的业务逻辑,耗时的业务逻辑应该单独交给多线程或者是mq处理。
Mq应用场景有那些
- 异步发送短信
- 异步发送新人优惠券
- 处理一些比较耗时的操作
为什么需要使用mq
可以实现支撑高并发、异步解耦、流量削峰、降低耦合度。
同步发送http请求
客户端发送请求到达服务器端,服务器端实现会员注册业务逻辑,
1.insertMember() --插入会员数据 1s
2.sendSms()----发送登陆短信提醒 3s
3.sendCoupons()----发送新人优惠券 3s
总共响应需要6s时间,可能会导致客户端阻塞6s时间,对用户体验
不是很好。
多线程与MQ方式实现异步?
互联网项目:
客户端 安卓/IOS
服务器端:php/java
最好使用mq实现异步
多线程处理业务逻辑
用户向数据库中插入一条数据之后,在单独开启一个线程异步发送短信和优惠操作。
客户端只需要等待1s时间
优点:适合于小项目 实现异步
缺点:有可能会消耗服务器cpu资源资源
Mq处理业务逻辑
先向数据库中插入一条会员数据,让后再向MQ中投递一个消息,MQ服务器端在将消息推送给消费者异步解耦处理发送短信和优惠券。
Mq与多线程之间区别
MQ可以实现异步/解耦/流量削峰问题;
多线程也可以实现异步,但是消耗到cpu资源,没有实现解耦。
Mq消息中间件名词
Producer 生产者:投递消息到MQ服务器端;
Consumer 消费者:从MQ服务器端获取消息处理业务逻辑;
Broker MQ服务器端
Topic 主题:分类业务逻辑发送短信主题、发送优惠券主题
Queue 存放消息模型 队列 先进先出 后进后出原则 数组/链表
Message 生产者投递消息报文:json
主流mq区别对比
简单的比较
特性 |
ActiveMQ |
RabbitMQ |
RocketMQ |
kafka |
开发语言 |
java |
erlang |
java |
scala |
单机吞吐量 |
万级 |
万级 |
10万级 |
10万级 |
时效性 |
ms级 |
us级 |
ms级 |
ms级以内 |
可用性 |
高(主从架构) |
高(主从架构) |
非常高(分布式架构) |
非常高(分布式架构) |
功能特性 |
成熟的产品,在很多公司得到应用;有较多的文档;各种协议支持较好 |
基于erlang开发,所以并发能力很强,性能极其好,延时很低管理界面较丰富 |
MQ功能比较完备,扩展性佳 |
只支持主要的MQ功能,像一些消息查询,消息回溯等功能没有提供,毕竟是为大数据准备的,在大数据领域应用广。 |
https://copyfuture.com/blogs-details/20190816154718064u4lgjgg20z1o6id
Mq设计基础知识
多线程版本mq;
使用LinkedBlockingDeque模拟实现多线程的方式读写中间件
package com.mayikt.netty; import com.alibaba.fastjson.JSONObject; import java.util.concurrent.LinkedBlockingDeque; /** * @ClassName MayiktThreadMQ * @Author 蚂蚁课堂余胜军 QQ644064779 www.mayikt.com * @Version V1.0 **/ public class MayiktThreadMQ { private static LinkedBlockingDeque<JSONObject> msgs = new LinkedBlockingDeque<JSONObject>(); public static void main(String[] args) { // 生产线程 Thread producerThread = new Thread(new Runnable() { @Override public void run() { try { while (true) { Thread.sleep(1000); JSONObject data = new JSONObject(); data.put("userId", "1234"); // 存入消息 msgs.offer(data); } } catch (Exception e) { } } }, "生产者"); producerThread.start(); // 消费者线程 Thread consumerThread = new Thread(new Runnable() { @Override public void run() { try { while (true) { JSONObject data = msgs.poll(); if (data != null) { System.out.println(Thread.currentThread().getName() + "," + data); } } } catch (Exception e) { } } }, "消费者"); consumerThread.start(); } }
基于网络通讯版本mq netty实现
生产者
package com.mayikt.netty; import com.alibaba.fastjson.JSONObject; import io.netty.bootstrap.Bootstrap; import io.netty.buffer.ByteBuf; import io.netty.buffer.Unpooled; import io.netty.channel.*; import io.netty.channel.nio.NioEventLoopGroup; import io.netty.channel.socket.SocketChannel; import io.netty.channel.socket.nio.NioSocketChannel; /** * @ClassName MayiktNettyMQProducer * @Author 蚂蚁课堂余胜军 QQ644064779 www.mayikt.com * @Version V1.0 **/ public class MayiktNettyMQProducer { public void connect(int port, String host) throws Exception { //配置客户端NIO 线程组 EventLoopGroup group = new NioEventLoopGroup(); Bootstrap client = new Bootstrap(); try { client.group(group) // 设置为Netty客户端 .channel(NioSocketChannel.class) /** * ChannelOption.TCP_NODELAY参数对应于套接字选项中的TCP_NODELAY,该参数的使用与Nagle算法有关。 * Nagle算法是将小的数据包组装为更大的帧然后进行发送,而不是输入一次发送一次,因此在数据包不足的时候会等待其他数据的到来,组装成大的数据包进行发送,虽然该算法有效提高了网络的有效负载,但是却造成了延时。 * 而该参数的作用就是禁止使用Nagle算法,使用于小数据即时传输。和TCP_NODELAY相对应的是TCP_CORK,该选项是需要等到发送的数据量最大的时候,一次性发送数据,适用于文件传输。 */ .option(ChannelOption.TCP_NODELAY, true) .handler(new ChannelInitializer<SocketChannel>() { @Override protected void initChannel(SocketChannel ch) throws Exception { ch.pipeline().addLast(new MayiktNettyMQProducer.NettyClientHandler()); //// 1. 演示LineBasedFrameDecoder编码器 // ch.pipeline().addLast(new LineBasedFrameDecoder(1024)); // ch.pipeline().addLast(new StringDecoder()); } }); //绑定端口, 异步连接操作 ChannelFuture future = client.connect(host, port).sync(); //等待客户端连接端口关闭 future.channel().closeFuture().sync(); } finally { //优雅关闭 线程组 group.shutdownGracefully(); } } public static void main(String[] args) { int port = 9008; MayiktNettyMQProducer client = new MayiktNettyMQProducer(); try { client.connect(port, "127.0.0.1"); } catch (Exception e) { e.printStackTrace(); } } public class NettyClientHandler extends ChannelInboundHandlerAdapter { @Override public void channelActive(ChannelHandlerContext ctx) throws Exception { JSONObject data = new JSONObject(); data.put("type", "producer"); JSONObject msg = new JSONObject(); msg.put("userId", "123456"); msg.put("age", "23"); data.put("msg", msg); // 生产发送数据 byte[] req = data.toJSONString().getBytes(); ByteBuf firstMSG = Unpooled.buffer(req.length); firstMSG.writeBytes(req); ctx.writeAndFlush(firstMSG); } /** * 客户端读取到服务器端数据 * * @param ctx * @param msg * @throws Exception */ @Override public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception { ByteBuf buf = (ByteBuf) msg; byte[] req = new byte[buf.readableBytes()]; buf.readBytes(req); String body = new String(req, "UTF-8"); System.out.println("客户端接收到服务器端请求:" + body); } // tcp属于双向传输 @Override public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception { ctx.close(); } } }
MQ服务器
package com.mayikt.netty; import com.alibaba.fastjson.JSONObject; import io.netty.bootstrap.ServerBootstrap; import io.netty.buffer.ByteBuf; import io.netty.buffer.Unpooled; import io.netty.channel.*; import io.netty.channel.nio.NioEventLoopGroup; import io.netty.channel.socket.SocketChannel; import io.netty.channel.socket.nio.NioServerSocketChannel; import org.apache.commons.lang3.StringUtils; import java.io.UnsupportedEncodingException; import java.util.ArrayList; import java.util.concurrent.LinkedBlockingDeque; /** * @ClassName NettyMQServer2021 * @Author 蚂蚁课堂余胜军 QQ644064779 www.mayikt.com * @Version V1.0 **/ public class MayiktNettyMQServer { public void bind(int port) throws Exception { /** * Netty 抽象出两组线程池BossGroup和WorkerGroup * BossGroup专门负责接收客户端的连接, WorkerGroup专门负责网络的读写。 */ EventLoopGroup bossGroup = new NioEventLoopGroup(); EventLoopGroup workerGroup = new NioEventLoopGroup(); ServerBootstrap bootstrap = new ServerBootstrap(); try { bootstrap.group(bossGroup, workerGroup) // 设定NioServerSocketChannel 为服务器端 .channel(NioServerSocketChannel.class) //BACKLOG用于构造服务端套接字ServerSocket对象,标识当服务器请求处理线程全满时, //用于临时存放已完成三次握手的请求的队列的最大长度。如果未设置或所设置的值小于1,Java将使用默认值50。 .option(ChannelOption.SO_BACKLOG, 100) // 服务器端监听数据回调Handler .childHandler(new MayiktNettyMQServer.ChildChannelHandler()); //绑定端口, 同步等待成功; ChannelFuture future = bootstrap.bind(port).sync(); System.out.println("当前服务器端启动成功..."); //等待服务端监听端口关闭 future.channel().closeFuture().sync(); } catch (Exception e) { e.printStackTrace(); } finally { //优雅关闭 线程组 bossGroup.shutdownGracefully(); workerGroup.shutdownGracefully(); } } private class ChildChannelHandler extends ChannelInitializer<SocketChannel> { @Override protected void initChannel(SocketChannel ch) throws Exception { // 设置异步回调监听 ch.pipeline().addLast(new MayiktNettyMQServer.MayiktServerHandler()); } } public static void main(String[] args) throws Exception { int port = 9008; new MayiktNettyMQServer().bind(port); } private static final String type_consumer = "consumer"; private static final String type_producer = "producer"; private static LinkedBlockingDeque<String> msgs = new LinkedBlockingDeque<>(); private static ArrayList<ChannelHandlerContext> ctxs = new ArrayList<>(); // 生产者投递消息的:topicName public class MayiktServerHandler extends SimpleChannelInboundHandler<Object> { /** * 服务器接收客户端请求 * * @param ctx * @param data * @throws Exception */ @Override protected void channelRead0(ChannelHandlerContext ctx, Object data) throws Exception { JSONObject clientMsg = getData(data); String type = clientMsg.getString("type"); switch (type) { case type_producer: producer(clientMsg); break; case type_consumer: consumer(ctx); break; } } private void consumer(ChannelHandlerContext ctx) { // 保存消费者连接 ctxs.add(ctx); // 主动拉取mq服务器端缓存中没有被消费的消息 String data = msgs.poll(); if (StringUtils.isEmpty(data)) { return; } // 将该消息发送给消费者 byte[] req = data.getBytes(); ByteBuf firstMSG = Unpooled.buffer(req.length); firstMSG.writeBytes(req); ctx.writeAndFlush(firstMSG); } private void producer(JSONObject clientMsg) { // 缓存生产者投递 消息 String msg = clientMsg.getString("msg"); msgs.offer(msg); //需要将该消息推送消费者 ctxs.forEach((ctx) -> { // 将该消息发送给消费者 String data = msgs.poll(); if (data == null) { return; } byte[] req = data.getBytes(); ByteBuf firstMSG = Unpooled.buffer(req.length); firstMSG.writeBytes(req); ctx.writeAndFlush(firstMSG); }); } private JSONObject getData(Object data) throws UnsupportedEncodingException { ByteBuf buf = (ByteBuf) data; byte[] req = new byte[buf.readableBytes()]; buf.readBytes(req); String body = new String(req, "UTF-8"); return JSONObject.parseObject(body); } @Override public void channelReadComplete(ChannelHandlerContext ctx) throws Exception { ctx.flush(); } @Override public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception { ctx.close(); } } }
消费者
package com.mayikt.netty; import com.alibaba.fastjson.JSONObject; import io.netty.bootstrap.Bootstrap; import io.netty.buffer.ByteBuf; import io.netty.buffer.Unpooled; import io.netty.channel.*; import io.netty.channel.nio.NioEventLoopGroup; import io.netty.channel.socket.SocketChannel; import io.netty.channel.socket.nio.NioSocketChannel; /** * @ClassName MayiktNettyMQProducer * @Author 蚂蚁课堂余胜军 QQ644064779 www.mayikt.com * @Version V1.0 **/ public class MayiktNettyMQConsumer { public void connect(int port, String host) throws Exception { //配置客户端NIO 线程组 EventLoopGroup group = new NioEventLoopGroup(); Bootstrap client = new Bootstrap(); try { client.group(group) // 设置为Netty客户端 .channel(NioSocketChannel.class) /** * ChannelOption.TCP_NODELAY参数对应于套接字选项中的TCP_NODELAY,该参数的使用与Nagle算法有关。 * Nagle算法是将小的数据包组装为更大的帧然后进行发送,而不是输入一次发送一次,因此在数据包不足的时候会等待其他数据的到来,组装成大的数据包进行发送,虽然该算法有效提高了网络的有效负载,但是却造成了延时。 * 而该参数的作用就是禁止使用Nagle算法,使用于小数据即时传输。和TCP_NODELAY相对应的是TCP_CORK,该选项是需要等到发送的数据量最大的时候,一次性发送数据,适用于文件传输。 */ .option(ChannelOption.TCP_NODELAY, true) .handler(new ChannelInitializer<SocketChannel>() { @Override protected void initChannel(SocketChannel ch) throws Exception { ch.pipeline().addLast(new MayiktNettyMQConsumer.NettyClientHandler()); //// 1. 演示LineBasedFrameDecoder编码器 // ch.pipeline().addLast(new LineBasedFrameDecoder(1024)); // ch.pipeline().addLast(new StringDecoder()); } }); //绑定端口, 异步连接操作 ChannelFuture future = client.connect(host, port).sync(); //等待客户端连接端口关闭 future.channel().closeFuture().sync(); } finally { //优雅关闭 线程组 group.shutdownGracefully(); } } public static void main(String[] args) { int port = 9008; MayiktNettyMQConsumer client = new MayiktNettyMQConsumer(); try { client.connect(port, "127.0.0.1"); } catch (Exception e) { e.printStackTrace(); } } public class NettyClientHandler extends ChannelInboundHandlerAdapter { @Override public void channelActive(ChannelHandlerContext ctx) throws Exception { JSONObject data = new JSONObject(); data.put("type", "consumer"); // 生产发送数据 byte[] req = data.toJSONString().getBytes(); ByteBuf firstMSG = Unpooled.buffer(req.length); firstMSG.writeBytes(req); ctx.writeAndFlush(firstMSG); } /** * 客户端读取到服务器端数据 * * @param ctx * @param msg * @throws Exception */ @Override public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception { ByteBuf buf = (ByteBuf) msg; byte[] req = new byte[buf.readableBytes()]; buf.readBytes(req); String body = new String(req, "UTF-8"); System.out.println("客户端接收到服务器端请求:" + body); } // tcp属于双向传输 @Override public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception { ctx.close(); } } }
基于netty实现mq
消费者netty客户端与nettyServer端MQ服务器端保持长连接,MQ服务器端保存
消费者连接。
生产者netty客户端发送请求给nettyServer端MQ服务器端,MQ服务器端在将该
消息内容发送给消费者。
body:{"msg":{"userId":"123456","age":"23"},"type":"producer",”topic”:””}
生产者投递消息给MQ服务器端,MQ服务器端需要缓存该消息
如果mq服务器端宕机之后,消息如何保证不丢失:
持久化机制
如果mq接收到生产者投递消息,如果消费者不在的情况下,该消息是否会丢失?
不会丢失,消息确认机制 必须要消费者消费该消息成功之后,在通知给mq服务器端,删除该消息。而kafka即使读取消息后也不会立刻删除,而是设置该消息被消费了,一定时间后统一删除。
消费者已经和mq服务器保持长连接:Mq服务器端将该消息推送消费者。
消费者第一次刚启动的时候,消费者会主动拉取消息。
Mq如何实现抗高并发思想
Mq消费者根据自身能力情况 ,拉取mq服务器端消息消费。
默认的情况下是取出一条消息。
缺点:存在延迟的问题
需要考虑mq消费者提高速率的问题:
如何消费者提高速率:消费者实现集群、消费者批量获取消息即可。
RabbitMQ
RabbitMQ基本介绍
RabbitMQ是实现了高级消息队列协议(AMQP)的开源消息代理软件(亦称面向消息的中间件),RabbitMQ服务器是用Erlang语言编写的。
RabitMQ官方网站:
1.点对点(简单)的队列
2.工作(公平性)队列模式
3.发布订阅模式
4.路由模式Routing
5.通配符模式Topics
6.RPC
https://www.rabbitmq.com/getstarted.html
RabbitMQ环境的基本安装
1.下载并安装erlang,下载地址:http://www.erlang.org/download
2.配置erlang环境变量信息
新增环境变量ERLANG_HOME=erlang的安装地址
将%ERLANG_HOME%\bin加入到path中
3.下载并安装RabbitMQ,下载地址:http://www.rabbitmq.com/download.html
注意: RabbitMQ 它依赖于Erlang,需要先安装Erlang。
https://www.rabbitmq.com/install-windows.html
如何启动Rabbitmq
net start RabbitMQ
启动Rabbitmq常见问题
如果rabbitmq 启动成功无法访问 管理平台页面
进入到F:\path\rabbitmq\rabbitmq\rabbitmq_server-3.6.9\sbin>
执行
rabbitmq-plugins enable rabbitmq_management
rabbitmqctl start_app
Rabbitmq管理平台中心
RabbitMQ 管理平台地址 http://127.0.0.1:15672
默认账号:guest/guest 用户可以自己创建新的账号
Virtual Hosts:
像mysql有数据库的概念并且可以指定用户对库和表等操作的权限。那RabbitMQ呢?
RabbitMQ也有类似的权限管理。在RabbitMQ中可以虚拟消息服务器VirtualHost,每
个VirtualHost相当月一个相对独立的RabbitMQ服务器,每个VirtualHost之间是相互
隔离。exchange、queue、message不能互通。
默认的端口15672:rabbitmq管理平台端口号
默认的端口5672: rabbitmq消息中间内部通讯的端口
默认的端口号25672 rabbitmq集群的端口号
RabbitMQ常见名词
/Virtual Hosts---分类
/队列 存放我们消息
Exchange 分派我们消息在那个队列存放起来 类似于nginx
15672---rabbitmq控制台管理平台 http协议
25672rabbitmq 集群通信端口号
Amqp 5672 rabbitmq内部通信的一个端口号
RabbitMQ创建账户
RabbitMQ平台创建Virtual Hosts
RabbitMQ平台创建消息队列
快速入门RabbitMQ简单队列
首先需要再RabbitMQ平台创建Virtual Hosts 和队列。
/myVirtualHosts
----订单队列
----支付队列
- 在RabbitMQ平台创建一个队列;
- 在编写生产者代码
- 在编写消费者代码
下列模式用到的所有的依赖
<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>com.mayikt</groupId> <artifactId>mayikt-mq</artifactId> <version>1.0-SNAPSHOT</version> <build> <plugins> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-compiler-plugin</artifactId> <configuration> <source>8</source> <target>8</target> </configuration> </plugin> </plugins> </build> <dependencies> <!-- https://mvnrepository.com/artifact/com.alibaba/fastjson --> <dependency> <groupId>com.alibaba</groupId> <artifactId>fastjson</artifactId> <version>1.2.62</version> </dependency> <!-- https://mvnrepository.com/artifact/io.netty/netty-all --> <dependency> <groupId>io.netty</groupId> <artifactId>netty-all</artifactId> <version>4.1.54.Final</version> </dependency> <!-- https://mvnrepository.com/artifact/junit/junit --> <dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> <version>4.12</version> <scope>test</scope> </dependency> <dependency> <groupId>org.apache.commons</groupId> <artifactId>commons-lang3</artifactId> <version>3.4</version> </dependency> <!-- https://mvnrepository.com/artifact/com.rabbitmq/amqp-client --> <dependency> <groupId>com.rabbitmq</groupId> <artifactId>amqp-client</artifactId> <version>5.2.0</version> </dependency> <dependency> <groupId>org.slf4j</groupId> <artifactId>slf4j-simple</artifactId> <version>1.7.25</version> <scope>compile</scope> </dependency> </dependencies> </project>
下面的程序是在普通maven工程下创建,使用到的队列,交换机,虚拟主机都要我们手动在mq管理界面(port:15672)创建
公共类:
package com.mayikt.rabbitmq; import com.rabbitmq.client.AMQP; import com.rabbitmq.client.Connection; import com.rabbitmq.client.ConnectionFactory; import java.io.IOException; import java.util.concurrent.TimeoutException; /** * @ClassName RabbitMQConnection * @Author 蚂蚁课堂余胜军 QQ644064779 www.mayikt.com * @Version V1.0 **/ public class RabbitMQConnection { /** * 创建连接 * * @return * @throws IOException * @throws TimeoutException */ public static Connection getConnection() throws IOException, TimeoutException { //1.创建connectionFactory ConnectionFactory connectionFactory = new ConnectionFactory(); //2.配置Host connectionFactory.setHost("127.0.0.1"); //3.设置Port connectionFactory.setPort(5672); //4.设置账户和密码 connectionFactory.setUsername("guest"); connectionFactory.setPassword("guest"); //5.设置VirtualHost connectionFactory.setVirtualHost("/myVirtualHost"); return connectionFactory.newConnection(); } }
点对点模式
package com.mayikt.demo00; import com.mayikt.rabbitmq.RabbitMQConnection; import com.rabbitmq.client.Channel; import com.rabbitmq.client.Connection; import java.io.IOException; import java.util.concurrent.TimeoutException; /** * @ClassName Producer 演示消息确认机制 * @Author 蚂蚁课堂余胜军 QQ644064779 www.mayikt.com * @Version V1.0 **/ public class Producer { private static final String QUEUE_NAME = "test_quene"; public static void main(String[] args) { try { //1.创建一个新连接 Connection connection = RabbitMQConnection.getConnection(); //2.设置channel Channel channel = connection.createChannel(); //3.发送消息 String msg = "每特教育6666"; channel.basicPublish("", QUEUE_NAME, null, msg.getBytes()); boolean result = channel.waitForConfirms(); if (result) { System.out.println("消息投递成功"); } else { System.out.println("消息投递失败"); } channel.close(); connection.close(); } catch (Exception e) { } } }
package com.mayikt.demo00; import com.mayikt.rabbitmq.RabbitMQConnection; import com.rabbitmq.client.*; import java.io.IOException; import java.util.concurrent.TimeoutException; public class Consumer { private static final String QUEUE_NAME = "test_quene"; public static void main(String[] args) throws IOException, TimeoutException, IOException, TimeoutException { // 1.创建连接 Connection connection = Producer1.getConnection(); // 2.设置通道 final Channel channel = connection.createChannel(); DefaultConsumer defaultConsumer = new DefaultConsumer(channel) { @Override public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException { String msg = new String(body, "UTF-8"); System.out.println("消费者获取消息:" + msg); // // 消费者完成 消费者通知给mq服务器端删除该消息 channel.basicAck(envelope.getDeliveryTag(), false); } }; // 3.监听队列 channel.basicConsume(QUEUE_NAME, false, defaultConsumer); } }
Work模式
Producer
package com.mayikt.demo01; import com.mayikt.rabbitmq.RabbitMQConnection; import com.rabbitmq.client.Channel; import com.rabbitmq.client.Connection; import java.io.IOException; import java.util.concurrent.TimeoutException; /** * @ClassName Producer * @Author 蚂蚁课堂余胜军 QQ644064779 www.mayikt.com * @Version V1.0 **/ public class Producer { private static final String QUEUE_NAME = "mayikt-queue"; public static void main(String[] args) throws IOException, TimeoutException { //1.创建一个新连接 Connection connection = RabbitMQConnection.getConnection(); //2.设置channel Channel channel = connection.createChannel(); //3.发送消息 for (int i = 0; i < 100; i++) { String msg = "每特教育6666:i" + i; channel.basicPublish("", QUEUE_NAME, null, msg.getBytes()); } System.out.println("消息投递成功"); channel.close(); connection.close(); } }
Consumer01
package com.mayikt.demo01; import com.mayikt.rabbitmq.RabbitMQConnection; import com.rabbitmq.client.*; import java.io.IOException; import java.util.concurrent.TimeoutException; public class Consumer01 { private static final String QUEUE_NAME = "mayikt-queue"; public static void main(String[] args) throws IOException, TimeoutException, IOException, TimeoutException { // 1.创建连接 Connection connection = RabbitMQConnection.getConnection(); // 2.设置通道 Channel channel = connection.createChannel(); DefaultConsumer defaultConsumer = new DefaultConsumer(channel) { @Override public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException { try { Thread.sleep(1000); } catch (Exception e) { } String msg = new String(body, "UTF-8"); System.out.println("消费者获取消息:" + msg); } }; // 3.监听队列 channel.basicConsume(QUEUE_NAME, true, defaultConsumer); } }
Consumer02
package com.mayikt.demo01; import com.mayikt.rabbitmq.RabbitMQConnection; import com.rabbitmq.client.*; import java.io.IOException; import java.util.concurrent.TimeoutException; public class Consumer02 { private static final String QUEUE_NAME = "mayikt-queue"; public static void main(String[] args) throws IOException, TimeoutException, IOException, TimeoutException { // 1.创建连接 Connection connection = RabbitMQConnection.getConnection(); // 2.设置通道 Channel channel = connection.createChannel(); DefaultConsumer defaultConsumer = new DefaultConsumer(channel) { @Override public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException { String msg = new String(body, "UTF-8"); System.out.println("消费者获取消息:" + msg); } }; // 3.监听队列 channel.basicConsume(QUEUE_NAME, true, defaultConsumer); } }
测试结果
测试结果:
1、 消费者1和消费者2获取到的消息内容是不同的,同一个消息只能被一个消费者获取。
2、 消费者1和消费者2获取到的消息的数量是相同的,一个是奇数一个是偶数。
其实,这样是不合理的,应该是消费者1要比消费者2获取到的消息多才对。
Work模式的“能者多劳”
Consumer1
package com.mayikt.demo02; import com.mayikt.rabbitmq.RabbitMQConnection; import com.rabbitmq.client.*; import java.io.IOException; import java.util.concurrent.TimeoutException; public class Consumer1 { private static final String QUEUE_NAME = "mayikt-queue"; public static void main(String[] args) throws IOException, TimeoutException, IOException, TimeoutException { // 1.创建连接 Connection connection = RabbitMQConnection.getConnection(); // 2.设置通道 Channel channel = connection.createChannel(); //指定我们消费者每次批量获取消息 channel.basicQos(2); DefaultConsumer defaultConsumer = new DefaultConsumer(channel) { @Override public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException { String msg = new String(body, "UTF-8"); System.out.println("消费者获取消息:" + msg); try { // 消费者完成 删除该消息 channel.basicAck(envelope.getDeliveryTag(), false); }catch (Exception e){ } // } }; // 3.监听队列 channel.basicConsume(QUEUE_NAME, false, defaultConsumer); } }
Consumer2
package com.mayikt.demo02; import com.mayikt.rabbitmq.RabbitMQConnection; import com.rabbitmq.client.*; import java.io.IOException; import java.util.concurrent.TimeoutException; public class Consumer2 { private static final String QUEUE_NAME = "mayikt-queue"; public static void main(String[] args) throws IOException, TimeoutException, IOException, TimeoutException { // 1.创建连接 Connection connection = RabbitMQConnection.getConnection(); // 2.设置通道 Channel channel = connection.createChannel(); channel.basicQos(1); DefaultConsumer defaultConsumer = new DefaultConsumer(channel) { @Override public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException { try { Thread.sleep(1000); } catch (Exception e) { } String msg = new String(body, "UTF-8"); System.out.println("消费者获取消息:" + msg); // 消费者完成 删除该消息 channel.basicAck(envelope.getDeliveryTag(), false); } }; // 3.监听队列 channel.basicConsume(QUEUE_NAME, false, defaultConsumer); } }
package com.mayikt.demo02; import com.mayikt.rabbitmq.RabbitMQConnection; import com.rabbitmq.client.Channel; import com.rabbitmq.client.Connection; import java.io.IOException; import java.util.concurrent.TimeoutException; /** * @ClassName Producer * @Author 蚂蚁课堂余胜军 QQ644064779 www.mayikt.com * @Version V1.0 **/ public class Producer { private static final String QUEUE_NAME = "mayikt-queue"; public static void main(String[] args) throws IOException, TimeoutException { //1.创建一个新连接 Connection connection = RabbitMQConnection.getConnection(); //2.设置channel Channel channel = connection.createChannel(); //3.发送消息 for (int i = 0; i < 10; i++) { String msg = "每特教育6666:i" + i; channel.basicPublish("", QUEUE_NAME, null, msg.getBytes()); } System.out.println("消息投递成功"); channel.close(); connection.close(); } }
ProducerFanout
package com.mayikt.demo03; import com.mayikt.rabbitmq.RabbitMQConnection; import com.rabbitmq.client.Channel; import com.rabbitmq.client.Connection; import java.io.IOException; import java.util.concurrent.TimeoutException; public class ProducerFanout { /** * 定义交换机的名称 */ private static final String EXCHANGE_NAME = "fanout_exchange"; public static void main(String[] args) throws IOException, TimeoutException { // 创建Connection Connection connection = RabbitMQConnection.getConnection(); // 创建Channel Channel channel = connection.createChannel(); // 通道关联交换机 channel.exchangeDeclare(EXCHANGE_NAME, "fanout", true); //第二个参数type:交换机类型,常见的如fanout、direct、topic String msg = "每特教育6666"; channel.basicPublish(EXCHANGE_NAME, "", null, msg.getBytes()); channel.close(); connection.close(); } }
package com.mayikt.demo03; import com.mayikt.rabbitmq.RabbitMQConnection; import com.rabbitmq.client.*; import java.io.IOException; import java.util.concurrent.TimeoutException; public class MailConsumer { /** * 定义邮件队列 */ private static final String QUEUE_NAME = "fanout_email_queue"; /** * 定义交换机的名称 */ private static final String EXCHANGE_NAME = "fanout_exchange"; public static void main(String[] args) throws IOException, TimeoutException { System.out.println("邮件消费者..."); // 创建我们的连接 Connection connection = RabbitMQConnection.getConnection(); // 创建我们通道 final Channel channel = connection.createChannel(); // 关联队列消费者关联队列 channel.queueBind(QUEUE_NAME, EXCHANGE_NAME, ""); DefaultConsumer defaultConsumer = new DefaultConsumer(channel) { @Override public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException { String msg = new String(body, "UTF-8"); System.out.println("邮件消费者获取消息:" + msg); } }; // 开始监听消息 自动签收 channel.basicConsume(QUEUE_NAME, true, defaultConsumer); } }
SmsConsumer
package com.mayikt.demo03; import com.mayikt.rabbitmq.RabbitMQConnection; import com.rabbitmq.client.*; import java.io.IOException; import java.util.concurrent.TimeoutException; public class SmsConsumer { /** * 定义短信队列 */ private static final String QUEUE_NAME = "fanout_email_sms"; /** * 定义交换机的名称 */ private static final String EXCHANGE_NAME = "fanout_exchange"; public static void main(String[] args) throws IOException, TimeoutException { System.out.println("短信消费者..."); // 创建我们的连接 Connection connection = RabbitMQConnection.getConnection(); // 创建我们通道 final Channel channel = connection.createChannel(); // 关联队列消费者关联队列 channel.queueBind(QUEUE_NAME, EXCHANGE_NAME, ""); DefaultConsumer defaultConsumer = new DefaultConsumer(channel) { @Override public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException { String msg = new String(body, "UTF-8"); System.out.println("短信消费者获取消息:" + msg); } }; // 开始监听消息 自动签收 channel.basicConsume(QUEUE_NAME, true, defaultConsumer); } }
四、路由模式
1、图示
注释:Direct Exchange – 处理路由键。需要将一个队列绑定到交换机上,要求该消息与一个特定的路由键完全匹配。这是一个完整的匹配。如果一个队列绑定到该交换机上要求路由键 “dog”,则只有被标记为“dog”的消息才被转发,不会转发dog.puppy,也不会转发dog.guard,只会转发dog。
ProducerDirect
package com.mayikt.demo04; import com.mayikt.rabbitmq.RabbitMQConnection; import com.rabbitmq.client.Channel; import com.rabbitmq.client.Connection; import java.io.IOException; import java.util.concurrent.TimeoutException; public class ProducerDirect { /** * 定义交换机的名称 */ private static final String EXCHANGE_NAME = "direct_exchange"; public static void main(String[] args) throws IOException, TimeoutException { // 创建Connection Connection connection = RabbitMQConnection.getConnection(); // 创建Channel Channel channel = connection.createChannel(); // 通道关联交换机 channel.exchangeDeclare(EXCHANGE_NAME, "direct", true); String msg = "每特教育6666"; channel.basicPublish(EXCHANGE_NAME, "email", null, msg.getBytes()); //只发送给邮件这个队列 channel.close(); connection.close(); } }
MailConsumer
package com.mayikt.demo04; import com.mayikt.rabbitmq.RabbitMQConnection; import com.rabbitmq.client.*; import java.io.IOException; import java.util.concurrent.TimeoutException; public class MailConsumer { /** * 定义邮件队列 */ private static final String QUEUE_NAME = "direct_email_queue"; /** * 定义交换机的名称 */ private static final String EXCHANGE_NAME = "direct_exchange"; public static void main(String[] args) throws IOException, TimeoutException { System.out.println("邮件消费者..."); // 创建我们的连接 Connection connection = RabbitMQConnection.getConnection(); // 创建我们通道 final Channel channel = connection.createChannel(); // 关联队列消费者关联队列 channel.queueBind(QUEUE_NAME, EXCHANGE_NAME, "email"); DefaultConsumer defaultConsumer = new DefaultConsumer(channel) { @Override public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException { String msg = new String(body, "UTF-8"); System.out.println("邮件消费者获取消息:" + msg); } }; // 开始监听消息 自动签收 channel.basicConsume(QUEUE_NAME, true, defaultConsumer); } }
SmsConsumer
package com.mayikt.demo04; import com.mayikt.rabbitmq.RabbitMQConnection; import com.rabbitmq.client.*; import java.io.IOException; import java.util.concurrent.TimeoutException; public class SmsConsumer { /** * 定义短信队列 */ private static final String QUEUE_NAME = "direct_sms_queue"; /** * 定义交换机的名称 */ private static final String EXCHANGE_NAME = "direct_exchange"; public static void main(String[] args) throws IOException, TimeoutException { System.out.println("短信消费者..."); // 创建我们的连接 Connection connection = RabbitMQConnection.getConnection(); // 创建我们通道 final Channel channel = connection.createChannel(); // 关联队列消费者关联队列 channel.queueBind(QUEUE_NAME, EXCHANGE_NAME, "sms"); DefaultConsumer defaultConsumer = new DefaultConsumer(channel) { @Override public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException { String msg = new String(body, "UTF-8"); System.out.println("短信消费者获取消息:" + msg); } }; // 开始监听消息 自动签收 channel.basicConsume(QUEUE_NAME, true, defaultConsumer); } }
ProducerTopic
package com.mayikt.demo06;
import com.mayikt.rabbitmq.RabbitMQConnection;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import java.io.IOException;
import java.util.concurrent.TimeoutException;
public class ProducerTopic {
/**
* 定义交换机的名称
*/
private static final String EXCHANGE_NAME = "topic_exchange";
public static void main(String[] args) throws IOException, TimeoutException {
// 创建Connection
Connection connection = RabbitMQConnection.getConnection();
// 创建Channel
Channel channel = connection.createChannel();
// 通道关联交换机
channel.exchangeDeclare(EXCHANGE_NAME, "topic", true);
String msg = "每特教育6666";
channel.basicPublish(EXCHANGE_NAME, "mayikt.sms", null, msg.getBytes());
channel.close();
connection.close();
}
}
MailConsumer
package com.mayikt.demo06;
import com.mayikt.rabbitmq.RabbitMQConnection;
import com.rabbitmq.client.*;
import java.io.IOException;
import java.util.concurrent.TimeoutException;
public class MailConsumer {
/**
* 定义邮件队列
*/
private static final String QUEUE_NAME = "topic_email_queue";
/**
* 定义交换机的名称
*/
private static final String EXCHANGE_NAME = "topic_exchange";
public static void main(String[] args) throws IOException, TimeoutException {
System.out.println("邮件消费者...");
// 创建我们的连接
Connection connection = RabbitMQConnection.getConnection();
// 创建我们通道
final Channel channel = connection.createChannel();
// 关联队列消费者关联队列
channel.queueBind(QUEUE_NAME, EXCHANGE_NAME, "mayikt.*");
DefaultConsumer defaultConsumer = new DefaultConsumer(channel) {
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
String msg = new String(body, "UTF-8");
System.out.println("邮件消费者获取消息:" + msg);
}
};
// 开始监听消息 自动签收
channel.basicConsume(QUEUE_NAME, true, defaultConsumer);
}
}
SmsConsumer
package com.mayikt.demo06;
import com.mayikt.rabbitmq.RabbitMQConnection;
import com.rabbitmq.client.*;
import java.io.IOException;
import java.util.concurrent.TimeoutException;
public class SmsConsumer {
/**
* 定义短信队列
*/
private static final String QUEUE_NAME = "topic_sms_queue";
/**
* 定义交换机的名称
*/
private static final String EXCHANGE_NAME = "topic_exchange";
public static void main(String[] args) throws IOException, TimeoutException {
System.out.println("短信消费者...");
// 创建我们的连接
Connection connection = RabbitMQConnection.getConnection();
// 创建我们通道
final Channel channel = connection.createChannel();
// 关联队列消费者关联队列
channel.queueBind(QUEUE_NAME, EXCHANGE_NAME, "meite.*");
DefaultConsumer defaultConsumer = new DefaultConsumer(channel) {
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
String msg = new String(body, "UTF-8");
System.out.println("短信消费者获取消息:" + msg);
}
};
// 开始监听消息 自动签收
channel.basicConsume(QUEUE_NAME, true, defaultConsumer);
}
}
RPC模式
https://blog.csdn.net/hry2015/article/details/79199294
1. RPC客户端启动后,创建一个匿名、独占的、回调的队列 2. RPC客户端设置消息的2个属性:replyTo和correlationId,然后将消息发送到队列rpc_queue 3. RPC服务端在队列rpc_queue上等待消息。RPC服务端处理完收到消息后,然后将处理结果封装成消息发送到replyTo指定的队列上,并且此消息带上correlationId(此值为收到消息里的correlationId) 4. RPC客户端在队列replyTo上等待消息,当收到消息后,它会判断收到消息的correlationId。如果值和自己之前发送的一样,则这个值就是RPC的处理结果
在rabbitmq情况下:RabbitMQ如何保证消息不丢失
使用消息确认机制+持久技术
A.消费者确认收到消息机制
channel.basicConsume(QUEUE_NAME, false, defaultConsumer);
注:第二个参数值为false代表关闭RabbitMQ的自动应答机制,改为手动应答。
在处理完消息时,返回应答状态,true表示为自动应答模式。
channel.basicAck(envelope.getDeliveryTag(), false);
B.生产者确认投递消息成功 使用Confirm机制 或者事务消息
Confirm机制 同步或者是异步的形式
2.RabbitMQ默认创建是持久化的
代码中设置 durable为true
参数名称详解:
durable是否持久化 durable为持久化、 Transient 不持久化
autoDelete 是否自动删除,当最后一个消费者断开连接之后队列是否自动被删除,可以通过RabbitMQ Management,查看某个队列的消费者数量,当consumers = 0时队列就会自动删除
- 使用rabbitmq事务消息;
channel.txSelect();
|
相关核心代码
生产者
public class Producer { channel.confirmSelect();
|
消费者
public class Consumer {
|
SpringBoot整合RabbitMQ
两个消费者,一个生产者
Maven依赖
<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>com.mayikt</groupId> <artifactId>mayikt-sp-rabbitmq</artifactId> <packaging>pom</packaging> <version>1.0-SNAPSHOT</version> <modules> <module>mayikt-producer</module> <module>email-consumer</module> <module>sms-consumer</module> </modules> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.0.0.RELEASE</version> </parent> <dependencies> <!-- springboot-web组件 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <!-- 添加springboot对amqp的支持 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-amqp</artifactId> </dependency> <dependency> <groupId>org.apache.commons</groupId> <artifactId>commons-lang3</artifactId> </dependency> <!--fastjson --> <dependency> <groupId>com.alibaba</groupId> <artifactId>fastjson</artifactId> <version>1.2.49</version> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> </dependency> </dependencies> </project>
生产者
yml
spring: rabbitmq: ####连接地址 host: 127.0.0.1 ####端口号 port: 5672 ####账号 username: guest ####密码 password: guest ### virtual-host: /myVirtualHost server: port: 9092
package com.mayikt.config; 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.stereotype.Component; /** * @ClassName RabbitMQConfig * @Author 蚂蚁课堂余胜军 QQ644064779 www.mayikt.com * @Version V1.0 **/ @Component public class RabbitMQConfig { /** * 定义交换机 */ private String EXCHANGE_SPRINGBOOT_NAME = "/mayikt_ex"; /** * 短信队列 */ private String FANOUT_SMS_QUEUE = "fanout_sms_queue"; /** * 邮件队列 */ private String FANOUT_EMAIL_QUEUE = "fanout_email_queue"; // 1.注入队列和交换机注入到spring容器中 // 2.关联交换机 <bean id="smsQueue" class="";> /** * 邮件和短信队列注入到spring容器中 * * @return */ @Bean public Queue smsQueue() { return new Queue(FANOUT_SMS_QUEUE); } @Bean public Queue emailQueue() { return new Queue(FANOUT_EMAIL_QUEUE); } @Bean public FanoutExchange fanoutExchange() { return new FanoutExchange(EXCHANGE_SPRINGBOOT_NAME); } /** * 关联交换机 * 根据参数名称 ioc获取 Queue对象 */ @Bean public Binding BindingSmsFanoutExchange(Queue smsQueue, FanoutExchange fanoutExchange) { return BindingBuilder.bind(smsQueue).to(fanoutExchange); } @Bean public Binding BindingEmailFanoutExchange(Queue emailQueue, FanoutExchange fanoutExchange) { return BindingBuilder.bind(emailQueue).to(fanoutExchange); } }
package com.mayikt.entity; import lombok.Data; import org.springframework.stereotype.Component; import java.io.Serializable; /** * @ClassName MsgEntity * @Author 蚂蚁课堂余胜军 QQ644064779 www.mayikt.com * @Version V1.0 **/ @Data public class MsgEntity implements Serializable { private String msgId; private String userId; private String phone; private String email; public MsgEntity(String msgId, String userId, String phone, String email) { this.msgId = msgId; this.userId = userId; this.phone = phone; this.email = email; } }
package com.mayikt.service; import com.mayikt.entity.MsgEntity; import org.springframework.amqp.core.AmqpTemplate; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import java.util.UUID; /** * @ClassName ProducerService * @Author 蚂蚁课堂余胜军 QQ644064779 www.mayikt.com * @Version V1.0 **/ @RestController public class ProducerService { @Autowired private AmqpTemplate amqpTemplate; @RequestMapping("/sendMsg") public void sendMsg() { /** * 参数1 交换机名称 * 参数2 路由key * 参数3 发送内容 */ MsgEntity msgEntity = new MsgEntity(UUID.randomUUID().toString(), "1234", "181111111", "644064779@qq.com"); amqpTemplate.convertAndSend("/mayikt_ex", "", msgEntity); } }
package com.mayikt; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; /** * @ClassName AppProducer * @Author 蚂蚁课堂余胜军 QQ644064779 www.mayikt.com * @Version V1.0 **/ @SpringBootApplication public class AppProducer { public static void main(String[] args) { SpringApplication.run(AppProducer.class); } }
spring: rabbitmq: ####连接地址 host: 127.0.0.1 ####端口号 port: 5672 ####账号 username: guest ####密码 password: guest ### virtual-host: /myVirtualHost server: port: 8081
package com.mayikt.entity; import lombok.Data; import java.io.Serializable; /** * @ClassName MsgEntity * @Author 蚂蚁课堂余胜军 QQ644064779 www.mayikt.com * @Version V1.0 **/ @Data public class MsgEntity implements Serializable { private String msgId; private String userId; private String phone; private String email; public MsgEntity(String msgId, String userId, String phone, String email) { this.msgId = msgId; this.userId = userId; this.phone = phone; this.email = email; } }
package com.mayikt; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; /** * @ClassName AppEmailConsumer * @Author 蚂蚁课堂余胜军 QQ644064779 www.mayikt.com * @Version V1.0 **/ @SpringBootApplication public class AppEmailConsumer { public static void main(String[] args) { SpringApplication.run(AppEmailConsumer.class); } }
package com.mayikt; import com.mayikt.entity.MsgEntity; import lombok.extern.slf4j.Slf4j; import org.springframework.amqp.rabbit.annotation.RabbitHandler; import org.springframework.amqp.rabbit.annotation.RabbitListener; import org.springframework.stereotype.Component; /** * @ClassName FanoutSmsConsumer * @Author 蚂蚁课堂余胜军 QQ644064779 www.mayikt.com * @Version V1.0 **/ @Slf4j @Component @RabbitListener(queues = "fanout_email_queue") public class FanoutEmailConsumer { @RabbitHandler public void process(MsgEntity msgEntity) { log.info("email:msgEntity:" + msgEntity); } }
spring: rabbitmq: ####连接地址 host: 127.0.0.1 ####端口号 port: 5672 ####账号 username: guest ####密码 password: guest ### virtual-host: /myVirtualHost server: port: 8082
package com.mayikt.entity; import lombok.Data; import java.io.Serializable; /** * @ClassName MsgEntity * @Author 蚂蚁课堂余胜军 QQ644064779 www.mayikt.com * @Version V1.0 **/ @Data public class MsgEntity implements Serializable { private String msgId; private String userId; private String phone; private String email; public MsgEntity(String msgId, String userId, String phone, String email) { this.msgId = msgId; this.userId = userId; this.phone = phone; this.email = email; } }
package com.mayikt; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; /** * @ClassName AppEmailConsumer * @Author 蚂蚁课堂余胜军 QQ644064779 www.mayikt.com * @Version V1.0 **/ @SpringBootApplication public class AppSMSConsumer { public static void main(String[] args) { SpringApplication.run(AppSMSConsumer.class); } }
package com.mayikt; import com.mayikt.entity.MsgEntity; import lombok.extern.slf4j.Slf4j; import org.springframework.amqp.rabbit.annotation.RabbitHandler; import org.springframework.amqp.rabbit.annotation.RabbitListener; import org.springframework.stereotype.Component; /** * @ClassName FanoutSmsConsumer * @Author 蚂蚁课堂余胜军 QQ644064779 www.mayikt.com * @Version V1.0 **/ @Slf4j @Component @RabbitListener(queues = "fanout_sms_queue") public class FanoutEmailConsumer { @RabbitHandler public void process(MsgEntity msgEntity) { log.info("sms:msgEntity:" + msgEntity); } }
需要注意的是springboot可以自动生成交换机和队列,无需手动配置。
生产者如何获取消费结果
RabbitMQ实战解决方案
下面的代码演示的是生产者生产消息给mq中间件(通过服务的方式),消费者订阅消费,并插入数据库,生产者通过接口和id判断数据库有没有相对应的消息来判断有没有被消费。
yml
这里面加入了自定义消费失败重试策略
spring: rabbitmq: ####连接地址 host: 127.0.0.1 ####端口号 port: 5672 ####账号 username: guest ####密码 password: guest ### 地址 virtual-host: /myVirtualHost listener: simple: retry: ####开启消费者(程序出现异常的情况下会)进行重试 enabled: true ####最大重试次数 max-attempts: 5 ####重试间隔时间 initial-interval: 3000 acknowledge-mode: manual datasource: url: jdbc:mysql://localhost:3306/test?useUnicode=true&characterEncoding=UTF-8 username: root password: root driver-class-name: com.mysql.cj.jdbc.Driver server: port: 9001
package com.mayikt.config; 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.stereotype.Component; /** * @ClassName RabbitMQConfig * @Author 蚂蚁课堂余胜军 QQ644064779 www.mayikt.com * @Version V1.0 **/ @Component public class RabbitMQConfig { /** * 定义交换机 */ private String EXCHANGE_SPRINGBOOT_NAME = "/mayikt_order"; /** * 订单队列 */ private String FANOUT_ORDER_QUEUE = "fanout_order_queue"; /** * 配置orderQueue * * @return */ @Bean public Queue orderQueue() { return new Queue(FANOUT_ORDER_QUEUE); } /** * 配置fanoutExchange * * @return */ @Bean public FanoutExchange fanoutExchange() { return new FanoutExchange(EXCHANGE_SPRINGBOOT_NAME); } // 绑定交换机 orderQueue @Bean public Binding bindingOrderFanoutExchange(Queue orderQueue, FanoutExchange fanoutExchange) { return BindingBuilder.bind(orderQueue).to(fanoutExchange); } }
消费者处理了重复提交的问题,但是这种方法仍有风险,可以靠唯一id解决。
int i = 1 / 0; 可以演示失败重试。
package com.mayikt.consumer; import com.alibaba.fastjson.JSONObject; import com.mayikt.entity.OrderEntity; import com.mayikt.manager.OrderManager; import com.mayikt.mapper.OrderMapper; import com.rabbitmq.client.Channel; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.StringUtils; import org.springframework.amqp.core.Message; import org.springframework.amqp.rabbit.annotation.RabbitHandler; import org.springframework.amqp.rabbit.annotation.RabbitListener; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; import java.io.IOException; /** * @ClassName fanout_sms_queue * @Author 蚂蚁课堂余胜军 QQ644064779 www.mayikt.com * @Version V1.0 **/ @Slf4j @Component @RabbitListener(queues = "fanout_order_queue") public class FanoutOrderConsumer { @Autowired private OrderManager orderManager; @Autowired private OrderMapper orderMapper; @RabbitHandler public void process(OrderEntity orderEntity, Message message, Channel channel) throws IOException { // try { log.info(">>orderEntity:{}<<", orderEntity.toString()); String orderId = orderEntity.getOrderId(); if (StringUtils.isEmpty(orderId)) { return; } OrderEntity dbOrderEntity = orderMapper.getOrder(orderId); if (dbOrderEntity != null) {//重试的过程中,为了避免业务逻辑重复执行,建议提前全局id提前查询,如果存在的情况下,就无需再继续做该流程。 log.info("另外消费者已经处理过该业务逻辑"); channel.basicAck(message.getMessageProperties().getDeliveryTag(), false); return; } // int i = 1 / 0; int result = orderManager.addOrder(orderEntity); log.info(">>插入数据库中数据成功<<"); channel.basicAck(message.getMessageProperties().getDeliveryTag(), false); // } catch (Exception e) { // // 记录该消息日志形式 存放数据库db中、后期通过定时任务实现消息补偿、人工实现补偿 // // //将该消息存放到死信队列中,单独写一个死信消费者实现消费。 // } } }
package com.mayikt.entity; import lombok.Data; import java.io.Serializable; @Data public class OrderEntity implements Serializable { private int id; private String orderName; private String orderId; public OrderEntity(String orderName, String orderId) { this.orderName = orderName; this.orderId = orderId; } public OrderEntity() { } }
package com.mayikt.manager; import com.mayikt.entity.OrderEntity; import com.mayikt.mapper.OrderMapper; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; import org.springframework.transaction.annotation.Transactional; /** * @ClassName OrderManager * @Author 蚂蚁课堂余胜军 QQ644064779 www.mayikt.com * @Version V1.0 **/ @Component public class OrderManager { @Autowired private OrderMapper orderMapper; @Transactional public int addOrder(OrderEntity orderEntity) { return orderMapper.addOrder(orderEntity); } }
package com.mayikt.mapper; import com.mayikt.entity.OrderEntity; import org.apache.ibatis.annotations.Insert; import org.apache.ibatis.annotations.Select; public interface OrderMapper { @Insert("insert order_info values (null,#{orderName},#{orderId})") int addOrder(OrderEntity orderEntity); @Select("SELECT * from order_info where orderId=#{orderId} ") OrderEntity getOrder(String orderId); }
package com.mayikt.producer; import com.alibaba.fastjson.JSONObject; import com.mayikt.entity.OrderEntity; import lombok.extern.slf4j.Slf4j; import org.springframework.amqp.rabbit.core.RabbitTemplate; import org.springframework.amqp.rabbit.support.CorrelationData; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; /** * @ClassName OrderProducer * @Author 蚂蚁课堂余胜军 QQ644064779 www.mayikt.com * @Version V1.0 **/ @Component @Slf4j public class OrderProducer implements RabbitTemplate.ConfirmCallback { @Autowired private RabbitTemplate rabbitTemplate; @Override public void confirm(CorrelationData correlationData, boolean b, String s) { String id = correlationData.getId(); log.info("id:" + id); System.out.println(id); } /** * 使用mq发送消息 * * @param orderName * @param orderId */ public void sendMsg(String orderName, String orderId) { OrderEntity orderEntity = new OrderEntity(orderName, orderId); rabbitTemplate.convertAndSend("/mayikt_order", "", orderEntity, message -> { return message; }); // CorrelationData correlationData = new CorrelationData(); // correlationData.setId(JSONObject.toJSONString(orderEntity)); // rabbitTemplate.convertAndSend("/mayikt_order", "", orderEntity,correlationData); } }
package com.mayikt.service; import com.alibaba.fastjson.JSONObject; import com.mayikt.entity.OrderEntity; import com.mayikt.mapper.OrderMapper; import com.mayikt.producer.OrderProducer; import lombok.extern.slf4j.Slf4j; import org.springframework.amqp.AmqpException; import org.springframework.amqp.core.Message; import org.springframework.amqp.core.MessagePostProcessor; import org.springframework.amqp.rabbit.core.RabbitTemplate; import org.springframework.amqp.rabbit.support.CorrelationData; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Component; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; @Slf4j @RestController public class OrderService { @Autowired private OrderMapper orderMapper; @Autowired private OrderProducer orderProducer; @RequestMapping("/sendOrder") public String sendOrder() { // 生成全局id String orderId = System.currentTimeMillis() + ""; log.info("orderId:{}", orderId); String orderName = "每特教育svip课程报名"; orderProducer.sendMsg(orderName, orderId); return orderId; } /** * 前端主动根据orderId定时查询 * * @param orderId * @return */ @RequestMapping("/getOrder") public Object getOrder(String orderId) { System.out.println(orderId); OrderEntity order = orderMapper.getOrder(orderId); if (order == null) { return "该订单没有被消费或者订单号错误!"; } return order; } }
RabbitMQ死信队列
死信队列产生的背景
RabbitMQ死信队列俗称,备胎队列;消息中间件因为某种原因拒收该消息后,可以转移到死信队列中存放,死信队列也可以有交换机和路由key等。
产生死信队列的原因
- 消息投递到MQ中存放 消息已经过期 消费者没有及时的获取到我们消息,消息如果存放到mq服务器中过期之后,会转移到备胎死信队列存放。
- 队列达到最大的长度 (队列容器已经满了)
- 3. 消费者消费多次消息失败,就会转移存放到死信队列中
代码整合 参考 mayikt-springboot-rabbitmq|#中order-dead-letter-queue项目
死信队列的架构原理
死信队列和普通队列区别不是很大
普通与死信队列都有自己独立的交换机和路由key、队列和消费者。
区别:
1.生产者投递消息先投递到我们普通交换机中,普通交换机在将该消息投到
普通队列中缓存起来,普通队列对应有自己独立普通消费者。
2.如果生产者投递消息到普通队列中,普通队列发现该消息一直没有被消费者消费
的情况下,在这时候会将该消息转移到死信(备胎)交换机中,死信(备胎)交换机
对应有自己独立的 死信(备胎)队列 对应独立死信(备胎)消费者。
死信队列应用场景
1.30分钟订单超时设计
- Redis过期key :
- 死信延迟队列实现:
采用死信队列,创建一个普通队列没有对应的消费者消费消息,在30分钟过后
就会将该消息转移到死信备胎消费者实现消费。
备胎死信消费者会根据该订单号码查询是否已经支付过,如果没有支付的情况下
则会开始回滚库存操作。
spring: rabbitmq: ####连接地址 host: 127.0.0.1 ####端口号 port: 5672 ####账号 username: guest ####密码 password: guest ### 地址 virtual-host: /myVirtualHost server: port: 8080 ###模拟演示死信队列 mayikt: dlx: exchange: mayikt_dlx_exchange queue: mayikt_order_dlx_queue routingKey: dlx ###备胎交换机 order: exchange: mayikt_order_exchange queue: mayikt_order_queue routingKey: mayikt.order
package com.mayikt; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; /** * @ClassName AppDeadLetter * @Author 蚂蚁课堂余胜军 QQ644064779 www.mayikt.com * @Version V1.0 **/ @SpringBootApplication public class AppDeadLetter { public static void main(String[] args) { SpringApplication.run(AppDeadLetter.class); } }
package com.mayikt.producer; import org.springframework.amqp.AmqpException; import org.springframework.amqp.core.Message; import org.springframework.amqp.core.MessagePostProcessor; import org.springframework.amqp.rabbit.core.RabbitTemplate; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; /** * @ClassName OrderProducer * @Author 蚂蚁课堂余胜军 QQ644064779 www.mayikt.com * @Version V1.0 **/ @RestController public class OrderProducer { @Autowired private RabbitTemplate rabbitTemplate; /** * 订单交换机 */ @Value("${mayikt.order.exchange}") private String orderExchange; /** * 订单路由key */ @Value("${mayikt.order.routingKey}") private String orderRoutingKey; @RequestMapping("/sendOrder") public String sendOrder() { String msg = "每特教育牛逼"; rabbitTemplate.convertAndSend(orderExchange, orderRoutingKey, msg, message -> { // 设置消息过期时间 10秒过期 message.getMessageProperties().setExpiration("10000"); return message; }); return "success"; } }
package com.mayikt.consumer; import lombok.extern.slf4j.Slf4j; import org.springframework.amqp.rabbit.annotation.RabbitListener; import org.springframework.stereotype.Component; @Slf4j @Component public class OrderDlxConsumer { /** * 死信队列监听队列回调的方法 * * @param msg */ @RabbitListener(queues = "mayikt_order_dlx_queue") public void orderConsumer(String msg) { log.info(">死信队列消费订单消息:msg{}<<", msg); } }
注释掉模式消费失败
//package com.mayikt.consumer; // //import lombok.extern.slf4j.Slf4j; //import org.springframework.amqp.rabbit.annotation.RabbitListener; //import org.springframework.stereotype.Component; // ///** // * 订单消费者 // */ //@Component //@Slf4j //public class OrderConsumer { // // /** // * 监听队列回调的方法 // * // * @param msg // */ // @RabbitListener(queues = "mayikt_order_queue") // public void orderConsumer(String msg) { // log.info(">>正常订单消费者消息MSG:{}<<", msg); // } //}
package com.mayikt.config; 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.beans.factory.annotation.Value; import org.springframework.context.annotation.Bean; import org.springframework.stereotype.Component; import java.util.HashMap; import java.util.Map; @Component public class DeadLetterMQConfig { /** * 订单交换机 */ @Value("${mayikt.order.exchange}") private String orderExchange; /** * 订单队列 */ @Value("${mayikt.order.queue}") private String orderQueue; /** * 订单路由key */ @Value("${mayikt.order.routingKey}") private String orderRoutingKey; /** * 死信交换机 */ @Value("${mayikt.dlx.exchange}") private String dlxExchange; /** * 死信队列 */ @Value("${mayikt.dlx.queue}") private String dlxQueue; /** * 死信路由 */ @Value("${mayikt.dlx.routingKey}") private String dlxRoutingKey; /** * 声明死信交换机 * * @return DirectExchange */ @Bean public DirectExchange dlxExchange() { return new DirectExchange(dlxExchange); } /** * 声明死信队列 * * @return Queue */ @Bean public Queue dlxQueue() { return new Queue(dlxQueue); } /** * 声明订单业务交换机 * * @return DirectExchange */ @Bean public DirectExchange orderExchange() { return new DirectExchange(orderExchange); } /** * 声明订单队列 * * @return Queue */ @Bean public Queue orderQueue() { // 订单队列绑定我们的死信交换机 Map<String, Object> arguments = new HashMap<>(2); arguments.put("x-dead-letter-exchange", dlxExchange); arguments.put("x-dead-letter-routing-key", dlxRoutingKey); return new Queue(orderQueue, true, false, false, arguments); } /** * 绑定死信队列到死信交换机 * * @return Binding */ @Bean public Binding binding() { return BindingBuilder.bind(dlxQueue()) .to(dlxExchange()) .with(dlxRoutingKey); } /** * 绑定订单队列到订单交换机 * * @return Binding */ @Bean public Binding orderBinding() { return BindingBuilder.bind(orderQueue()) .to(orderExchange()) .with(orderRoutingKey); } }
下面的代码是使用springboot演示订阅模式实现异步发送邮件和短信,当然生产者和消费者进行拆开成微服务。
微服务形式
spring: rabbitmq: ####连接地址 host: 127.0.0.1 ####端口号 port: 5672 ####账号 username: guest ####密码 password: guest ### 地址 virtual-host: /myVirtualHost
package com.mayikt; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; /** * @ClassName App * @Author 蚂蚁课堂余胜军 QQ644064779 www.mayikt.com * @Version V1.0 **/ @SpringBootApplication public class App { public static void main(String[] args) { SpringApplication.run(App.class); } }
package com.mayikt.producer; import org.springframework.amqp.core.AmqpTemplate; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; /** * @ClassName FanoutProducer * @Author 蚂蚁课堂余胜军 QQ644064779 www.mayikt.com * @Version V1.0 **/ @RestController public class FanoutProducer { @Autowired private AmqpTemplate amqpTemplate; /** * 发送消息 * * @return */ @RequestMapping("/sendMsg") public String sendMsg(String msg) { /** * 1.交换机名称 * 2.路由key名称 * 3.发送内容 */ amqpTemplate.convertAndSend("/mayikt_ex", "", msg); return "success"; } }
package com.mayikt.consumer; import lombok.extern.slf4j.Slf4j; import org.springframework.amqp.rabbit.annotation.RabbitHandler; import org.springframework.amqp.rabbit.annotation.RabbitListener; import org.springframework.stereotype.Component; /** * @ClassName fanout_sms_queue * @Author 蚂蚁课堂余胜军 QQ644064779 www.mayikt.com * @Version V1.0 **/ @Slf4j @Component @RabbitListener(queues = "fanout_sms_queue") public class FanoutSmsConsumer { @RabbitHandler public void process(String msg) { log.info(">>短信消费者消息msg:{}<<", msg); } }
package com.mayikt.consumer; import lombok.extern.slf4j.Slf4j; import org.springframework.amqp.rabbit.annotation.RabbitHandler; import org.springframework.amqp.rabbit.annotation.RabbitListener; import org.springframework.stereotype.Component; /** * @ClassName FanoutEmailConsumer * @Author 蚂蚁课堂余胜军 QQ644064779 www.mayikt.com * @Version V1.0 **/ @Slf4j @Component @RabbitListener(queues = "fanout_email_queue") public class FanoutEmailConsumer { @RabbitHandler public void process(String msg) { log.info(">>邮件消费者消息msg:{}<<", msg); } }
package com.mayikt.config; 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.stereotype.Component; /** * @ClassName RabbitMQConfig * @Author 蚂蚁课堂余胜军 QQ644064779 www.mayikt.com * @Version V1.0 **/ @Component public class RabbitMQConfig { /** * 定义交换机 */ private String EXCHANGE_SPRINGBOOT_NAME = "/mayikt_ex"; /** * 短信队列 */ private String FANOUT_SMS_QUEUE = "fanout_sms_queue"; /** * 邮件队列 */ private String FANOUT_EMAIL_QUEUE = "fanout_email_queue"; /** * 配置smsQueue * * @return */ @Bean public Queue smsQueue() { return new Queue(FANOUT_SMS_QUEUE); } /** * 配置emailQueue * * @return */ @Bean public Queue emailQueue() { return new Queue(FANOUT_EMAIL_QUEUE); } /** * 配置fanoutExchange * * @return */ @Bean public FanoutExchange fanoutExchange() { return new FanoutExchange(EXCHANGE_SPRINGBOOT_NAME); } // 绑定交换机 sms @Bean public Binding bindingSmsFanoutExchange(Queue smsQueue, FanoutExchange fanoutExchange) { return BindingBuilder.bind(smsQueue).to(fanoutExchange); } // 绑定交换机 email @Bean public Binding bindingEmailFanoutExchange(Queue emailQueue, FanoutExchange fanoutExchange) { return BindingBuilder.bind(emailQueue).to(fanoutExchange); } }
RabbitMQ消息幂等问题
RabbitMQ消息自动重试机制
- 当我们消费者处理执行我们业务代码的时候,如果抛出异常的情况下
在这时候mq会自动触发重试机制,默认的情况下rabbitmq是无限次数的重试。
需要人为指定重试次数限制问题
- 在什么情况下消费者需要实现重试策略?
A.消费者获取消息后,调用第三方接口,但是调用第三方接口失败呢?是否需要重试?
该情况下需要实现重试策略,网络延迟只是暂时调用不通,重试多次有可能会调用通。
B.消费者获取消息后,因为代码问题抛出数据异常,是否需要重试?
该情况下是不需要实现重试策略,就算重试多次,最终还是失败的。可以将日志存放起来,后期通过定时任务或者人工补偿形式。如果是重试多次还是失败消息,需要重新发布消费者版本实现消费。
可以使用死信队列
Mq在重试的过程中,有可能会引发消费者重复消费的问题。
Mq消费者需要解决 幂等性问题
幂等性 保证数据唯一
方式1:
生产者在投递消息的时候,生成一个全局唯一id,放在我们消息中。
Msg id=123456
Msg id=123456
Msg id=123456
消费者获取到我们该消息,可以根据该全局唯一id实现去重复。
全局唯一id 根据业务来定的 订单号码作为全局的id
实际上还是需要再db层面解决数据防重复。
业务逻辑是在做insert操作 使用唯一主键约束
业务逻辑是在做update操作 使用乐观锁
- 当消费者业务逻辑代码中,抛出异常自动实现重试 (默认是无数次重试)
- 应该对RabbitMQ重试次数实现限制,比如最多重试5次,每次间隔3s;重试多次还是失败的情况下,存放到死信队列或者存放到数据库表中记录后期人工补偿
- 消费者获取消息后,调用第三方接口,但是调用第三方接口失败呢?是否需要重试 ?
- 消费者获取消息后,应该代码问题抛出数据异常,是否需要重试?
如何合理选择消息重试
总结:如果消费者处理消息时,因为代码原因抛出异常是需要从新发布版本才能解决的,那么就不需要重试,重试也解决不了该问题的。存放到死信队列或者是数据库表记录、后期人工实现补偿。
详细的策略解决幂等性和保证幂等性的方法
https://www.cnblogs.com/javalyy/p/8882144.html
什么是幂等性
HTTP/1.1中对幂等性的定义是:一次和多次请求某一个资源对于资源本身应该具有同样的结果(网络超时等问题除外)。也就是说,其任意多次执行对资源本身所产生的影响均与一次执行的影响相同。
什么情况下需要幂等
业务开发中,经常会遇到重复提交的情况,无论是由于网络问题无法收到请求结果而重新发起请求,或是前端的操作抖动而造成重复提交情况。 在交易系统,支付系统这种重复提交造成的问题有尤其明显,比如:
-
用户在APP上连续点击了多次提交订单,后台应该只产生一个订单;
-
向支付宝发起支付请求,由于网络问题或系统BUG重发,支付宝应该只扣一次钱。 很显然,声明幂等的服务认为,外部调用者会存在多次调用的情况,为了防止外部多次调用对系统数据状态的发生多次改变,将服务设计成幂等。
保证幂等策略
幂等需要通过唯一的业务单号来保证。也就是说相同的业务单号,认为是同一笔业务。使用这个唯一的业务单号来确保,后面多次的相同的业务单号的处理逻辑和执行效果是一致的。 下面以支付为例,在不考虑并发的情况下,实现幂等很简单:①先查询一下订单是否已经支付过,②如果已经支付过,则返回支付成功;如果没有支付,进行支付流程,修改订单状态为‘已支付’。
防重复提交策略
上述的保证幂等方案是分成两步的,第②步依赖第①步的查询结果,无法保证原子性的。在高并发下就会出现下面的情况:第二次请求在第一次请求第②步订单状态还没有修改为‘已支付状态’的情况下到来。既然得出了这个结论,余下的问题也就变得简单:把查询和变更状态操作加锁,将并行操作改为串行操作。
乐观锁
如果只是更新已有的数据,没有必要对业务进行加锁,设计表结构时使用乐观锁,一般通过version来做乐观锁,这样既能保证执行效率,又能保证幂等。例如: UPDATE tab1 SET col1=1,version=version+1 WHERE version=#version#
不过,乐观锁存在失效的情况,就是常说的ABA问题,不过如果version版本一直是自增的就不会出现ABA的情况。(从网上找了一张图片很能说明乐观锁,引用过来,出自Mybatis对乐观锁的支持)
防重表
使用订单号orderNo做为去重表的唯一索引,每次请求都根据订单号向去重表中插入一条数据。第一次请求查询订单支付状态,当然订单没有支付,进行支付操作,无论成功与否,执行完后更新订单状态为成功或失败,删除去重表中的数据。后续的订单因为表中唯一索引而插入失败,则返回操作失败,直到第一次的请求完成(成功或失败)。可以看出防重表作用是加锁的功能。
分布式锁
这里使用的防重表可以使用分布式锁代替,比如Redis。订单发起支付请求,支付系统会去Redis缓存中查询是否存在该订单号的Key,如果不存在,则向Redis增加Key为订单号。查询订单支付已经支付,如果没有则进行支付,支付完成后删除该订单号的Key。通过Redis做到了分布式锁,只有这次订单订单支付请求完成,下次请求才能进来。相比去重表,将放并发做到了缓存中,较为高效。思路相同,同一时间只能完成一次支付请求。
token令牌
这种方式分成两个阶段:申请token阶段和支付阶段。 第一阶段,在进入到提交订单页面之前,需要订单系统根据用户信息向支付系统发起一次申请token的请求,支付系统将token保存到Redis缓存中,为第二阶段支付使用。 第二阶段,订单系统拿着申请到的token发起支付请求,支付系统会检查Redis中是否存在该token,如果存在,表示第一次发起支付请求,删除缓存中token后开始支付逻辑处理;如果缓存中不存在,表示非法请求。 实际上这里的token是一个信物,支付系统根据token确认,你是你妈的孩子。不足是需要系统间交互两次,流程较上述方法复杂。
支付缓冲区
把订单的支付请求都快速地接下来,一个快速接单的缓冲管道。后续使用异步任务处理管道中的数据,过滤掉重复的待支付订单。优点是同步转异步,高吞吐。不足是不能及时地返回支付结果,需要后续监听支付结果的异步返回。