一、什么是消息中间件?
消息中间件基于队列模型实现异步/同步传输数据。
作用:可以实现支撑高并发、异步解耦、流量削峰、降低耦合度。
二、传统的Http请求存在哪些缺点?
1、Http请求基于请求与响应的模型,在高并发的情况下,客户端发送大量的请求达到服务器端有可能会导致我们服务器端处理请求堆积。
2、Tomcat服务器处理每个请求都有自己独立的线程,如果超过最大线程数会将该请求缓存到队列中,如果请求堆积过多的情况下,有可能会导致Tomcat服务器崩溃的问题。所以一般都会在Nginx入口实现限流,整合服务保护框架(例如:Sentinel)。
3、Http请求处理业务逻辑如果比较耗时的情况下,容易造成客户端一直等待,阻塞等待过程中会导致客户端超时发生重试策略,有可能会引发幂等性问题。
注意事项:接口是为Http协议的情况下,最好不要处理比较耗时的业务逻辑,耗时的业务逻辑应该单独交给多线程或者是MQ处理。
三、MQ应用场景有哪些?
1、异步发送短信。
2、异步发送新人优惠券。
3、异步处理一些比较耗时的操作。
四、为什么需要使用MQ?
MQ可以实现高并发、异步解耦、流量削峰、降低耦合度。
会员注册业务逻辑
1、同步发送Http请求
客户端发送请求到达服务器端,服务器端实现会员注册业务逻辑,
(1)insertMember() --插入会员数据 1s
(2)sendSms()----发送登录短信提醒 3s
(3)sendCoupons()----发送新人优惠券 3s
总共响应需要6s时间,可能会导致客户端阻塞6s时间,对用户体验不是很好。
2、多线程处理业务逻辑
用户向数据库中插入一条数据之后,在单独开启一个线程异步发送短信和优惠操作。
客户端只需要等待1s时间
优点:适合于小项目实现异步。
缺点:有可能会消耗服务器CPU资源资源。
3、MQ处理业务逻辑
先向数据库中插入一条会员数据,让后再向MQ中投递一个消息,MQ服务器端在将消息推送给消费者异步解耦处理发送短信和优惠券。
五、MQ与多线程之间的区别
MQ可以实现异步/解耦/流量削峰问题;
多线程也可以实现异步,但是多线程的上下文切换会消耗到CPU资源,没有实现解耦。
六、MQ消息中间件名词
Producer 生产者:投递消息到MQ服务器端。
Consumer 消费者:从MQ服务器端获取消息处理业务逻辑。
Broker:MQ服务器端。
Topic 主题:分类业务逻辑发送短信主题、发送优惠券主题。
Queue:存放消息模型 队列 先进先出 后进后出原则 数组/链表。
Message:生产者投递消息报文,例如:JSON。
七、使用MQ架构常见的一些问题
1、生产者投递消息给MQ服务器端,MQ服务器端需要缓存该消息,如果MQ服务器端宕机之后,消息如何保证不丢失?
持久化机制。
2、如果MQ接收到生产者投递消息,如果消费者不在的情况下,该消息是否会丢失?
不会丢失,消息确认机制,必须要消费者消费该消息成功之后,再通知给MQ服务器端删除该消息。
3、MQ服务器端将该消息推送消费者与消费者主动拉取消息的区别?
MQ服务器端将该消息推送消费者:消费者已经和mq服务器保持长连接。
消费者主动拉取消息:消费者第一次刚启动的时候。
4、MQ如何实现抗高并发思想?
MQ消费者根据自身能力情况 ,拉取MQ服务器端消息消费。默认的情况下是取出一条消息。
缺点:存在延迟的问题
5、需要考虑mq消费者提高速率的问题:
如何消费者提高速率:消费者实现集群、消费者批量获取消息即可。
八、手写简单版MQ
1、基于多线程方式实现MQ
package com.example.demo;
import com.alibaba.fastjson.JSONObject;
import java.util.concurrent.LinkedBlockingQueue;
public class DemoThreadMQ {
/**
* 消息队列
*/
private static final LinkedBlockingQueue<JSONObject> MSG_QUEUE = new LinkedBlockingQueue<>();
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);
//存入消息
MSG_QUEUE.offer(data);
}
} catch (Exception e) {
}
}
}, "生产者");
producerThread.start();
//消费者线程
Thread consumerThread = new Thread(new Runnable() {
@Override
public void run() {
try {
while (true) {
//取出消息
JSONObject data = MSG_QUEUE.poll();
if (data != null) {
System.out.println(Thread.currentThread().getName() + "," +data);
}
}
} catch (Exception e) {
}
}
}, "消费者");
consumerThread.start();
}
}
2、基于Netty实现MQ
消费者Netty客户端与NettyServer端MQ服务器端保持长连接,MQ服务器端保存消费者连接。
生产者Netty客户端发送请求给NettyServer端MQ服务器端,MQ服务器端再将该消息内容发送给消费者。
body:{"msg":{"userId":"123456","age":"23"},"type":"producer","topic":""}
(1)、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.example</groupId>
<artifactId>demo-mq</artifactId>
<version>1.0-SNAPSHOT</version>
<properties>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<dependencies>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.62</version>
</dependency>
<dependency>
<groupId>io.netty</groupId>
<artifactId>netty-all</artifactId>
<version>4.0.23.Final</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.62</version>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<version>3.11</version>
</dependency>
</dependencies>
</project>
(2)、NettyServer端
package com.example.demo;
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.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.BlockingDeque;
import java.util.concurrent.LinkedBlockingDeque;
public class NettyMQServer {
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 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 static class ChildChannelHandler extends ChannelInitializer<SocketChannel> {
@Override
protected void initChannel(SocketChannel ch) throws Exception {
// 设置异步回调监听
ch.pipeline().addLast(new ServerHandler());
}
}
public static void main(String[] args) throws Exception {
int port = 9008;
new NettyMQServer().bind(port);
}
private static final String TYPE_CONSUMER = "consumer";
private static final String TYPE_PRODUCER = "producer";
private static BlockingDeque<String> MSG_QUEUE = new LinkedBlockingDeque<>();
private static List<ChannelHandlerContext> CTX_LIST = new ArrayList<>();
// 生产者投递消息的:topicName
public static class ServerHandler 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) {
// 保存消费者连接
CTX_LIST.add(ctx);
// 主动拉取mq服务器端缓存中没有被消费的消息
String data = MSG_QUEUE.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");
MSG_QUEUE.offer(msg);
//从CTX_LIST取出消费者连接,需要将该消息推送消费者
CTX_LIST.forEach(ctx -> {
// 将该消息发送给消费者
String data = MSG_QUEUE.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, StandardCharsets.UTF_8);
System.out.println("body = " + body);
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();
}
}
}
(3)、消费者NettyClient端
package com.example.demo;
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;
import java.nio.charset.StandardCharsets;
public class NettyMQConsumer {
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 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;
NettyMQConsumer client = new NettyMQConsumer();
try {
client.connect(port, "127.0.0.1");
} catch (Exception e) {
e.printStackTrace();
}
}
public static 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, StandardCharsets.UTF_8);
System.out.println("客户端接收到服务器端请求:" + body);
}
// tcp属于双向传输
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
ctx.close();
}
}
}
(4)、生产者NettyClient端
package com.example.demo;
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;
import java.nio.charset.StandardCharsets;
public class NettyMQProducer {
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 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;
NettyMQProducer client = new NettyMQProducer();
try {
client.connect(port, "127.0.0.1");
} catch (Exception e) {
e.printStackTrace();
}
}
public static 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, StandardCharsets.UTF_8);
System.out.println("客户端接收到服务器端请求:" + body);
}
// tcp属于双向传输
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
ctx.close();
}
}
}
九、主流MQ对比
特性 | ActiveMQ | RabbitMQ | RocketMQ | kafka |
开发语言 | java | erlang | java | scala |
单机吞吐量 | 万级 | 万级 | 10万级 | 10万级 |
时效性 | ms级 | us级 | ms级 | ms级以内 |
可用性 | 高(主从架构) | 高(主从架构) | 非常高(分布式架构) | 非常高(分布式架构) |
功能特性 | 成熟的产品,在很多公司得到应用;有较多的文档;各种协议支持较好 | 基于erlang开发,所以并发能力很强,性能极其好,延时很低管理界面较丰富 | MQ功能比较完备,扩展性佳 | 只支持主要的MQ功能,像一些消息查询,消息回溯等功能没有提供,毕竟是为大数据准备的,在大数据领域应用广。 |
十、RabbitMQ基本介绍
RabbitMQ是实现了高级消息队列协议(AMQP)的开源消息代理软件(亦称面向消息的中间件),RabbitMQ服务器是用Erlang语言编写的。
RabitMQ官方网站:RabbitMQ: easy to use, flexible messaging and streaming — RabbitMQ
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。
Installing on Windows — RabbitMQ
4、RabbitMQ安装步骤
首先安装Erlang,双击Erlang_otp_win64_19.1.exe
配置Erlang环境变量:
再安装RabbitMQ,双击rabbitmq-server-3.6.9.exe
十二、如何启动RabbitMQ?
十三、启动RabbitMQ常见问题
如果rabbitmq 启动成功无法访问 管理平台页面
进入到C:\DevelopTools\RabbitMQ\RabbitMQ Server\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:分类。创建队列,存放消息,队列属于哪个分类下的。创建队列时需指明队列是属于哪个分类的。类似于Kafka中的分区。
Exchange:交换机。分发我们的消息在哪个队列存放起来,类似于Nginx做路由。
每个Virtual Hosts当中都有自己独立的队列和交换机。Virtual Hosts主要的目的是相当于分类的去存放消息,能够做个区分,每个分类里面的交换机和消息都是不互通的。
15672---rabbitmq控制台管理平台 http协议
25672rabbitmq 集群通信端口号
Amqp 5672 rabbitmq内部通信的一个端口号
十六、RabbitMQ入门案例(RabbitMQ简单队列的使用)
简单队列就是一个点对点的通讯,也就是说在这个时候,我们需要在MQ服务器端创建一个队列,这个时候我们需要去指明这个队列它是在哪个VisualHosts下面,如果想去创建一个队列,首先要创建VisualHosts,再去创建队列。
首先需要再RabbitMQ平台创建Virtual Hosts 和队列。
/demoVirtualHosts
----订单队列
----支付队列
流程:
1、在RabbitMQ平台创建一个队列
新添加一个VisualHosts,命名为demoVisualHosts,注意最左边要加/
新添加的VisualHosts需要指明账号的权限
点击/demoVirtualHosts,进入此页面
点击按钮,设置权限
此时就有权限了
创建一个队列,名称为:demo-queue
点击按钮,创建队列
2、编写生产者代码
package com.example;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.ConnectionFactory;
import java.io.IOException;
import java.util.concurrent.TimeoutException;
public class RabbitMQConnection {
/**
* 获取连接
*
* @return
*/
public static Connection getConnection() throws IOException, TimeoutException {
// 1、创建连接
ConnectionFactory connectionFactory = new ConnectionFactory();
// 2、设置连接地址
connectionFactory.setHost("127.0.0.1");
// 3、设置端口号:
connectionFactory.setPort(5672);
// 4、设置账号和密码
connectionFactory.setUsername("guest");
connectionFactory.setPassword("guest");
// 5、设置VirtualHost
connectionFactory.setVirtualHost("/demoVirtualHosts");
return connectionFactory.newConnection();
}
}
package com.example;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import java.io.IOException;
import java.util.concurrent.TimeoutException;
public class Producer {
private static final String QUEUE_NAME = "demo-queue";
public static void main(String[] args) throws IOException, TimeoutException {
// 1.创建连接
Connection connection = RabbitMQConnection.getConnection();
// 2.设置通道
Channel channel = connection.createChannel();
// 3.设置消息
String msg = "RabbitMQ从放弃到入门";
System.out.println("msg:" + msg);
channel.basicPublish("", QUEUE_NAME, null, msg.getBytes());
channel.close();
connection.close();
}
}
3、编写消费者代码
package com.example;
import com.rabbitmq.client.*;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.concurrent.TimeoutException;
public class Consumer {
private static final String QUEUE_NAME = "demo-queue";
public static void main(String[] args) throws 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, StandardCharsets.UTF_8);
System.out.println("消费者获取消息:" + msg);
}
};
// 3.监听队列
//autoAck:true,自动签收,表示消费者只要能成功获取到MQ中的消息,这个时候MQ服务器端就自动的把消息从队列中移除掉。
//一般实际开发中,都会把autoAck改为false,手动签收的模式,
channel.basicConsume(QUEUE_NAME, true, defaultConsumer);
}
}
4、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.example</groupId>
<artifactId>demo-mq</artifactId>
<version>1.0-SNAPSHOT</version>
<properties>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<dependencies>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.62</version>
</dependency>
<dependency>
<groupId>io.netty</groupId>
<artifactId>netty-all</artifactId>
<version>4.0.23.Final</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.62</version>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<version>3.11</version>
</dependency>
<dependency>
<groupId>com.rabbitmq</groupId>
<artifactId>amqp-client</artifactId>
<version>3.6.5 </version>
</dependency>
</dependencies>
</project>
十七、RabbitMQ如何保证消息不丢失
Mq如何保证消息不丢失:
1、生产者角色
确保生产者投递消息到MQ服务器端成功。
Ack 消息确认机制
同步或者异步的形式
方式1:Confirms
方式2:事务消息
2、消费者角色
在rabbitmq情况下:
必须要将消息消费成功之后,才会将该消息从mq服务器端中移除。
在kafka中的情况下:
不管是消费成功还是消费失败,该消息都不会立即从mq服务器端移除。
3、MQ服务器端
在默认的情况下 都会对队列中的消息实现持久化,持久化硬盘。
使用消息确认机制+持久化技术
A.消费者确认收到消息机制
channel.basicConsume(QUEUE_NAME, false, defaultConsumer);
注:第二个参数值为false代表关闭RabbitMQ的自动应答机制,改为手动应答。
在处理完消息时,返回应答状态,true表示为自动应答模式。
channel.basicAck(envelope.getDeliveryTag(), false);
B.生产者确认投递消息成功 使用Confirm机制 或者事务消息
public class Producer {
private static final String QUEUE_NAME = "demo-queue";
/**
* 生产者消息确认机制
* @param args
*/
public static void main(String[] args) {
try {
// 1.创建连接
Connection connection = RabbitMQConnection.getConnection();
// 2.设置通道
Channel channel = connection.createChannel();
//开启确认消息机制
channel.confirmSelect();
// 3.设置消息
String msg = "RabbitMQ从放弃到入门";
System.out.println("msg:" + msg);
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) {
e.printStackTrace();
}
}
}
Confirm机制 同步或者是异步的形式
4、RabbitMQ默认创建是持久化的
代码中设置 durable为true
参数名称详解:
durable是否持久化 durable为持久化、 Transient 不持久化
autoDelete 是否自动删除,当最后一个消费者断开连接之后队列是否自动被删除,可以通过RabbitMQ Management,查看某个队列的消费者数量,当consumers = 0时队列就会自动删除
5、使用RabbitMQ事务消息
package com.example.demo01;
import com.example.RabbitMQConnection;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import java.io.IOException;
import java.util.concurrent.TimeoutException;
public class Producer {
private static final String QUEUE_NAME = "demo-queue";
/**
* RabbitMQ事务消息
* @param args
*/
public static void main(String[] args) throws IOException, TimeoutException {
// 1.创建连接
Connection connection = RabbitMQConnection.getConnection();
// 2.设置通道
Channel channel = connection.createChannel();
try {
// 3.发送消息
String msg = "RabbitMQ从放弃到入门";
channel.txSelect();
channel.basicPublish("", QUEUE_NAME, null, msg.getBytes());
int i = 1 / 0;
channel.txCommit();
System.out.println("消息投递成功");
channel.close();
connection.close();
} catch (Exception e) {
if (channel != null) {
channel.txRollback();
}
e.printStackTrace();
}
}
}
package com.example.demo01;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.ConnectionFactory;
import java.io.IOException;
import java.util.concurrent.TimeoutException;
public class RabbitMQConnection {
/**
* 获取连接
*
* @return
*/
public static Connection getConnection() throws IOException, TimeoutException {
// 1、创建连接
ConnectionFactory connectionFactory = new ConnectionFactory();
// 2、设置连接地址
connectionFactory.setHost("127.0.0.1");
// 3、设置端口号:
connectionFactory.setPort(5672);
// 4、设置账号和密码
connectionFactory.setUsername("guest");
connectionFactory.setPassword("guest");
// 5、设置VirtualHost
connectionFactory.setVirtualHost("/demoVirtualHosts");
return connectionFactory.newConnection();
}
}
package com.example.demo01;
import com.example.demo01.RabbitMQConnection;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import java.io.IOException;
import java.util.concurrent.TimeoutException;
public class Producer {
private static final String QUEUE_NAME = "demo-queue";
/**
* 生产者消息确认机制
* @param args
*/
public static void main01(String[] args) {
try {
// 1.创建连接
Connection connection = RabbitMQConnection.getConnection();
// 2.设置通道
Channel channel = connection.createChannel();
//开启确认消息机制
channel.confirmSelect();
// 3.设置消息
String msg = "RabbitMQ从放弃到入门";
System.out.println("msg:" + msg);
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) {
e.printStackTrace();
}
}
/**
* RabbitMQ事务消息
* @param args
*/
public static void main(String[] args) throws IOException, TimeoutException {
// 1.创建连接
Connection connection = RabbitMQConnection.getConnection();
// 2.设置通道
Channel channel = connection.createChannel();
try {
// 3.发送消息
String msg = "RabbitMQ从放弃到入门";
channel.txSelect();
channel.basicPublish("", QUEUE_NAME, null, msg.getBytes());
int i = 1 / 0;
channel.txCommit();
System.out.println("消息投递成功");
channel.close();
connection.close();
} catch (Exception e) {
if (channel != null) {
channel.txRollback();
}
e.printStackTrace();
}
}
}
package com.example.demo01;
import com.example.demo01.RabbitMQConnection;
import com.rabbitmq.client.*;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.concurrent.TimeoutException;
public class Consumer {
private static final String QUEUE_NAME = "demo-queue";
public static void main(String[] args) throws 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, StandardCharsets.UTF_8);
System.out.println("消费者获取消息:" + msg);
//消费者完成,消费者通知给MQ服务器端删除该消息
channel.basicAck(envelope.getDeliveryTag(), false);
}
};
// 3.监听队列
//autoAck false:手动应答模式 true:自动签收模式
channel.basicConsume(QUEUE_NAME, true, defaultConsumer);
}
}
十八、RabbitMQ工作队列
默认的传统队列是为均摊消费,存在不公平性;如果每个消费者速度不一样的情况下,均摊消费是不公平的,应该是能者多劳。
采用工作队列
在通道中只需要设置basicQos为1即可,表示MQ服务器每次只会给消费者推送1条消息必须手动ack确认之后才会继续发送。如果消费者没有确认消息被消费,MQ服务器端是不会继续发新的消息给消费者。
channel.basicQos(1);
十九、RabbitMQ交换机类型
Direct exchange(直连交换机)
Fanout exchange(扇型交换机)
Topic exchange(主题交换机)
Headers exchange(头交换机)
/Virtual Hosts---区分不同的团队
----队列 存放消息
----交换机 路由消息存放在那个队列中 类似于nginx
---路由key 分发规则
1、Fanout发布订阅模式
生产者发送一条消息,经过交换机转发到多个不同的队列,多个不同的队列对应多个不同的消费者。
原理:
1、需要创建两个队列 ,每个队列对应一个消费者;
2、队列需要绑定我们交换机;
3、生产者投递消息到交换机中,交换机再将消息分配给两个队列中都存放起来;
4、消费者从队列中获取这个消息。
(1)、创建队列
(2)、 创建交换机,并指定交换机类型为fanout
(3)、生产者代码
package com.example.demo03;
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);
String msg = "RabbitMQ从放弃到入门";
channel.basicPublish(EXCHANGE_NAME, "", null, msg.getBytes());
channel.close();
connection.close();
}
}
(4)、消费者代码
①、邮件消费者
package com.example.demo03;
import com.rabbitmq.client.*;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
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, StandardCharsets.UTF_8);
System.out.println("邮件消费者获取消息:" + msg);
}
};
// 开始监听消息 自动应答
channel.basicConsume(QUEUE_NAME, true, defaultConsumer);
}
}
②、短信消费者
package com.example.demo03;
import com.rabbitmq.client.*;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.concurrent.TimeoutException;
public class SmsConsumer {
/**
* 定义短信队列
*/
private static final String QUEUE_NAME = "fanout_sms_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, StandardCharsets.UTF_8);
System.out.println("短信消费者获取消息:" + msg);
}
};
// 开始监听消息 自动应答
channel.basicConsume(QUEUE_NAME, true, defaultConsumer);
}
}
2、Direct路由模式
当交换机类型为direct类型时,根据队列绑定的路由键转发到具体的队列中存放消息。
(1)、创建队列
(2)、创建交换机
(3)、生产者代码
package com.example.demo04;
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 = "RabbitMQ从放弃到入门";
channel.basicPublish(EXCHANGE_NAME, "sms", null, msg.getBytes());
channel.close();
connection.close();
}
}
(4)、消费者代码
package com.example.demo04;
import com.rabbitmq.client.*;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
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, StandardCharsets.UTF_8);
System.out.println("邮件消费者获取消息:" + msg);
}
};
// 开始监听消息 自动签收
channel.basicConsume(QUEUE_NAME, true, defaultConsumer);
}
}
package com.example.demo04;
import com.rabbitmq.client.*;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
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, StandardCharsets.UTF_8);
System.out.println("短信消费者获取消息:" + msg);
}
};
// 开始监听消息 自动签收
channel.basicConsume(QUEUE_NAME, true, defaultConsumer);
}
}
3、Topic主题模式
当交换机类型为topic类型时,根据队列绑定的路由建模糊转发到具体的队列中存放。
#号表示支持匹配多个词;
*号表示只能匹配一个词。
(1)、创建队列
(2)、 创建交换机
(3)、 生产者代码
package com.example.demo06;
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 = "RabbitMQ从放弃到入门";
channel.basicPublish(EXCHANGE_NAME, "demo.sms", null, msg.getBytes());
channel.close();
connection.close();
}
}
(4)、消费者代码
package com.example.demo06;
import com.rabbitmq.client.*;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
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, "demo.*");
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, StandardCharsets.UTF_8);
System.out.println("邮件消费者获取消息:" + msg);
}
};
// 开始监听消息 自动签收
channel.basicConsume(QUEUE_NAME, true, defaultConsumer);
}
}
package com.example.demo06;
import com.rabbitmq.client.*;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
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, "test.*");
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, StandardCharsets.UTF_8);
System.out.println("短信消费者获取消息:" + msg);
}
};
// 开始监听消息 自动签收
channel.basicConsume(QUEUE_NAME, true, defaultConsumer);
}
}
二十、SpringBoot整合RabbitMQ
1、整体项目结构
2、demo-producer结构
3、email-consumer结构
4、sms-consumer结构
5、 Maven依赖
(1)、父工程 demo-springboot-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.example</groupId>
<artifactId>demo-springboot-rabbitmq</artifactId>
<version>1.0-SNAPSHOT</version>
<packaging>pom</packaging>
<modules>
<module>demo-producer</module>
<module>email-consumer</module>
<module>sms-consumer</module>
</modules>
<properties>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<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>
(2)、demo-producer
<?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>
<parent>
<groupId>com.example</groupId>
<artifactId>demo-springboot-rabbitmq</artifactId>
<version>1.0-SNAPSHOT</version>
</parent>
<artifactId>demo-producer</artifactId>
<properties>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
</project>
(3)、email-consumer
<?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>
<parent>
<groupId>com.example</groupId>
<artifactId>demo-springboot-rabbitmq</artifactId>
<version>1.0-SNAPSHOT</version>
</parent>
<artifactId>email-consumer</artifactId>
<properties>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
</project>
(4)、sms-consumer
<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/maven-v4_0_0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.example</groupId>
<artifactId>demo-springboot-rabbitmq</artifactId>
<version>1.0-SNAPSHOT</version>
</parent>
<artifactId>sms-consumer</artifactId>
<properties>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
</project>
6、相关代码
(1)、demo-producer
①、config
package com.example.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;
/**
* 1、注入队列、交换机到Spring容器中
* 2、关联交换机
*/
@Component
public class RabbitMQConfig {
/**
* 定义交换机
*/
public static final String EXCHANGE_NAME = "/demo_ex";
/**
* 短信队列
*/
public static final String FANOUT_SMS_QUEUE = "fanout_sms_queue";
/**
* 邮件队列
*/
public static final String FANOUT_EMAIL_QUEUE = "fanout_email_queue";
/**
* 配置smsQueue
* <bean id="smsQueue" class = "org.springframework.amqp.core.Queue"></bean>。
* 使用@Bean注解,默认id名称为方法名称:smsQueue。class为返回类型:Queue
* @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_NAME);
}
/**
* 短信队列绑定到交换机
*
* 根据参数名称,从ioc容器中获取Queue对象,参数名称smsQueue,要与上面注入队列的方法名称smsQueue一致。
*
* @param smsQueue
* @param fanoutExchange
* @return
*/
@Bean
public Binding bindingSmsFanoutExchange(Queue smsQueue, FanoutExchange fanoutExchange) {
return BindingBuilder.bind(smsQueue).to(fanoutExchange);
}
/**
* 邮件队列绑定到交换机
*
* @param emailQueue
* @param fanoutExchange
* @return
*/
@Bean
public Binding bindingEmailFanoutExchange(Queue emailQueue, FanoutExchange fanoutExchange) {
return BindingBuilder.bind(emailQueue).to(fanoutExchange);
}
}
②、controller
package com.example.controller;
import com.example.service.ProducerService;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequiredArgsConstructor
public class ProducerController {
private final ProducerService producerService;
@RequestMapping(value = "/sendMsg", method = RequestMethod.GET)
public void sendMsg() {
producerService.senMsg();
}
}
③、service
package com.example.service;
import com.example.config.RabbitMQConfig;
import com.example.entity.MsgEntity;
import lombok.RequiredArgsConstructor;
import org.springframework.amqp.core.AmqpTemplate;
import org.springframework.stereotype.Service;
import java.util.UUID;
@Service
@RequiredArgsConstructor
public class ProducerService {
private final AmqpTemplate amqpTemplate;
public void senMsg() {
MsgEntity msgEntity = MsgEntity.builder()
.msgId(UUID.randomUUID().toString())
.userId("1234")
.phone("18111111111")
.email("123@qq.com").build();
/**
* 参数1:交换机名称
* 参数2:路由key
* 参数3:发送的内容
*/
amqpTemplate.convertAndSend(RabbitMQConfig.EXCHANGE_NAME, "", msgEntity);
}
}
④、entity
package com.example.entity;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.io.Serializable;
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class MsgEntity implements Serializable {
/**
* 消息id
*/
private String msgId;
/**
* 用户id
*/
private String userId;
/**
* 手机号
*/
private String phone;
/**
* 邮箱
*/
private String email;
}
⑤、application.yml
server:
port: 8080
spring:
rabbitmq:
####连接地址
host: 127.0.0.1
####端口号
port: 5672
####账号
username: guest
####密码
password: guest
### 地址
virtual-host: /demoVirtualHosts
(2)、email-consumer
①、consumer
package com.example;
import com.example.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;
@Slf4j
@Component //注入到Spring的容器中
@RabbitListener(queues = "fanout_email_queue") //指定需要监听的队列的名称
public class FanoutEmailConsumer {
@RabbitHandler //这个注解加上过后,就能够消费MQ中的消息
public void process(MsgEntity msgEntity) {
log.info("email, msgEntity: {}", msgEntity);
}
}
entity同上
②、application.yml
server:
port: 8081
spring:
rabbitmq:
####连接地址
host: 127.0.0.1
####端口号
port: 5672
####账号
username: guest
####密码
password: guest
### 地址
virtual-host: /demoVirtualHosts
(3)、sms-consumer
①、consumer
package com.example;
import com.example.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;
@Slf4j
@Component //注入到Spring的容器中
@RabbitListener(queues = "fanout_sms_queue") //指定需要监听的队列的名称
public class FanoutSmsConsumer {
@RabbitHandler //这个注解加上过后,就能够消费MQ中的消息
public void process(MsgEntity msgEntity) {
log.info("sms, msgEntity: {}", msgEntity);
}
}
entity同上
②、application.yml
server:
port: 8082
spring:
rabbitmq:
####连接地址
host: 127.0.0.1
####端口号
port: 5672
####账号
username: guest
####密码
password: guest
### 地址
virtual-host: /demoVirtualHosts
二十一、生产者如何获取消费者消费消息的结果
(一)、根据业务来定
消费者消费成功结果:能够在数据库中插入一条数据
(二)、Rocketmq 自带全局消息id,能够根据该全局消息获取消费结果
原理: 生产者投递消息到mq服务器,mq服务器端在这时候返回一个全局的消息id,
当我们消费者消费该消息成功之后,消费者会给我们mq服务器端发送通知标记该消息
消费成功。这个思想在RabbitMQ中没有实现。
生产者获取到该消息全局id,每隔2s时间调用mq服务器端接口查询该消息是否
有被消费成功。
1.异步返回一个全局id,前端使用ajax定时主动查询;
2.在rocketmq中,自带根据消息id查询是否消费成功
1、根据全局id主动获取消费者结果
(1)、新建工程:order-producer-consumer
结构如图所示:
(2)、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>
<parent>
<groupId>com.example</groupId>
<artifactId>demo-springboot-rabbitmq</artifactId>
<version>1.0-SNAPSHOT</version>
</parent>
<artifactId>order-producer-consumer</artifactId>
<properties>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<dependencies>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>1.1.1</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
</dependencies>
</project>
(3)、application.yml
spring:
rabbitmq:
####连接地址
host: 127.0.0.1
####端口号
port: 5672
####账号
username: guest
####密码
password: guest
### 地址
virtual-host: /demoVirtualHosts
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: 123456
driver-class-name: com.mysql.jdbc.Driver
(4)、config
package com.example.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;
@Component
public class RabbitMQConfig {
/**
* 定义交换机
*/
private static final String EXCHANGE_NAME = "/demo_order";
/**
* 订单队列
*/
private static final 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_NAME);
}
/**
* 绑定交换机 orderQueue
*
* @param orderQueue
* @param fanoutExchange
* @return
*/
@Bean
public Binding bindingOrderFanoutExchange(Queue orderQueue, FanoutExchange fanoutExchange) {
return BindingBuilder.bind(orderQueue).to(fanoutExchange);
}
}
(5)、consumer
package com.example.consumer;
import com.example.entity.OrderEntity;
import com.example.manager.OrderManager;
import com.example.mapper.OrderMapper;
import com.rabbitmq.client.Channel;
import lombok.RequiredArgsConstructor;
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.stereotype.Component;
import java.io.IOException;
@Slf4j
@Component
@RabbitListener(queues = "fanout_order_queue")
@RequiredArgsConstructor
public class FanoutOrderConsumer {
private final OrderManager orderManager;
private final 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) {
log.info("其他消费者已经处理过该业务逻辑");
channel.basicAck(message.getMessageProperties().getDeliveryTag(), false);
return;
}
int result = orderManager.addOrder(orderEntity);
// int i = 1 / 0;
log.info("插入数据库中数据成功");
channel.basicAck(message.getMessageProperties().getDeliveryTag(), false);
// } catch (Exception e) {
// // 记录该消息日志形式 存放数据库db中、后期通过定时任务实现消息补偿、人工实现补偿
//
// //将该消息存放到死信队列中,单独写一个死信消费者实现消费。
// }
}
}
(6)、controller
package com.example.controller;
import com.example.entity.OrderEntity;
import com.example.mapper.OrderMapper;
import com.example.producer.OrderProducer;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@Slf4j
@RestController
@RequiredArgsConstructor
public class OrderController {
private final OrderMapper orderMapper;
private final OrderProducer orderProducer;
@GetMapping("/sendOrder")
public String sendOrder() {
// 生成全局id
String orderId = System.currentTimeMillis() + "";
log.info("orderId:{}", orderId);
String orderName = "<<RabbitMQ从放弃到入门>>";
orderProducer.sendMsg(orderName, orderId);
//把订单id返回给客户端
return orderId;
}
/**
* 前端主动根据orderId定时查询
*
* @param orderId
* @return
*/
@GetMapping("/getOrder")
public Object getOrder(String orderId) {
OrderEntity order = orderMapper.getOrder(orderId);
if (order == null) {
return "该订单没有被消费或者订单号错误!";
}
return order;
}
}
(7)、entity
package com.example.entity;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.io.Serializable;
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class OrderEntity implements Serializable {
private int id;
private String orderName;
private String orderId;
}
(8)、manager
package com.example.manager;
import com.example.entity.OrderEntity;
import com.example.mapper.OrderMapper;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;
@Component
@RequiredArgsConstructor
public class OrderManager {
private final OrderMapper orderMapper;
@Transactional
public int addOrder(OrderEntity orderEntity) {
return orderMapper.addOrder(orderEntity);
}
}
(9)、mapper
package com.example.mapper;
import com.example.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);
}
(10)、producer
package com.example.producer;
import com.example.entity.OrderEntity;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.amqp.rabbit.support.CorrelationData;
import org.springframework.stereotype.Component;
@Slf4j
@Component
@RequiredArgsConstructor
public class OrderProducer implements RabbitTemplate.ConfirmCallback {
private final RabbitTemplate rabbitTemplate;
@Override
public void confirm(CorrelationData correlationData, boolean b, String s) {
String id = correlationData.getId();
log.info("id: {} ", id);
}
/**
* 使用mq发送消息
*
* @param orderName
* @param orderId
*/
public void sendMsg(String orderName, String orderId) {
OrderEntity orderEntity = OrderEntity.builder().orderName(orderName).orderId(orderId).build();
rabbitTemplate.convertAndSend("/demo_order", "", orderEntity, message -> message);
}
}
二十二、RabbitMQ死信队列
1、什么是死信队列?
RabbitMQ死信队列,俗称备胎队列;消息中间件因为某种原因拒收该消息后,可以转移到死信队列中存放,死信队列也可以有交换机和路由key等。
2、死信队列产生的原因
(1)、消息投递到MQ中存放 消息已经过期 消费者没有及时的获取到我们消息,消息如果存放到mq服务器中过期之后,会转移到备胎死信队列存放。
(2)、队列达到最大的长度 (队列容器已经满了)
(3)、消费者消费多次消息失败,就会转移存放到死信队列中
3、死信队列架构原理
死信队列和普通队列区别不是很大
普通与死信队列都有自己独立的交换机和路由key、队列和消费者。
区别:
(1)、生产者投递消息先投递到我们普通交换机中,普通交换机在将该消息投到普通队列中缓存起来,普通队列对应有自己独立普通消费者。
(2)、如果生产者投递消息到普通队列中,普通队列发现该消息一直没有被消费者消费的情况下,在这时候会将该消息转移到死信交换机中,死信交换机对应有自己独立的死信队列,死信队列对应有自己独立的死信消费者。
4、SpringBoot整合RabbitMQ死信队列
(1)、新建工程order-dead-letter-queue
项目结构如图所示:
(2)、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>
<parent>
<groupId>com.example</groupId>
<artifactId>demo-springboot-rabbitmq</artifactId>
<version>1.0-SNAPSHOT</version>
</parent>
<artifactId>order-dead-letter-queue</artifactId>
<properties>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
</project>
(3)、application.yml
spring:
rabbitmq:
####连接地址
host: 127.0.0.1
####端口号
port: 5672
####账号
username: guest
####密码
password: guest
### 地址
virtual-host: /demoVirtualHosts
server:
port: 8080
###模拟演示死信队列
demo:
dlx:
exchange: demo_dlx_exchange
queue: demo_order_dlx_queue
routingKey: dlx
###备胎交换机
order:
exchange: demo_order_exchange
queue: demo_order_queue
routingKey: demo.order
(4)、config
package com.example.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("${demo.order.exchange}")
private String orderExchange;
/**
* 订单队列
*/
@Value("${demo.order.queue}")
private String orderQueue;
/**
* 订单路由key
*/
@Value("${demo.order.routingKey}")
private String orderRoutingKey;
/**
* 死信交换机
*/
@Value("${demo.dlx.exchange}")
private String dlxExchange;
/**
* 死信队列
*/
@Value("${demo.dlx.queue}")
private String dlxQueue;
/**
* 死信路由
*/
@Value("${demo.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);
}
}
(5)、consumer
package com.example.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 = "demo_order_queue")
public void orderConsumer(String msg) {
log.info("正常订单消费者消息:{}", msg);
}
}
package com.example.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 = "demo_order_dlx_queue")
public void orderConsumer(String msg) {
log.info("死信队列消费订单消息: {}", msg);
}
}
(6)、producer
package com.example.producer;
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;
@RestController
public class OrderProducer {
@Autowired
private RabbitTemplate rabbitTemplate;
/**
* 订单交换机
*/
@Value("${demo.order.exchange}")
private String orderExchange;
/**
* 订单路由key
*/
@Value("${demo.order.routingKey}")
private String orderRoutingKey;
@RequestMapping("/sendOrder")
public String sendOrder() {
String msg = "《RabbitMQ从放弃到入门》";
rabbitTemplate.convertAndSend(orderExchange, orderRoutingKey, msg, message -> {
// 设置消息过期时间 10秒过期
message.getMessageProperties().setExpiration("10000");
return message;
});
return "success";
}
}
5、订单30分钟超时设计实现方案
(1)、Redis过期key :下订单,扣减库存,通过往Redis里面写key,当这个key30分钟过期后,会发一个事件通知给客户端,客户端将订单id传给服务端,服务端根据订单的id查询该订单有没有支付,没有支付的情况下,就会把库存进行还原。
(2)、死信延迟队列实现:
采用死信队列,创建一个普通队列,没有对应的消费者消费消息,在30分钟过后就会将该消息转移到死信备胎消费者实现消费。备胎死信消费者会根据该订单号码查询是否已经支付过,如果没有支付的情况下则会开始回滚库存操作,同时将订单状态改为已超时。
二十三、RabbitMQ消息幂等性问题
1、RabbitMQ消息自动重试机制
当我们消费者处理执行我们业务代码的时候,如果抛出异常的情况下,在这时候mq会自动触发重试机制,默认的情况下rabbitmq是无限次数的重试。
Mq在重试的过程中,有可能会引发消费者重复消费的问题。
Mq消费者需要解决幂等性问题,在重试的过程当中,消费者有可能会拿到相同的消息,进行数据入库,有可能会导致数据在数据库中重复。
幂等性:保证数据唯一。
需要人为指定重试次数限制问题。
RabbitMQ如何开启重试策略:
2、在什么情况下消费者需要实现重试策略?
(1)消费者获取消息后,调用第三方接口,但是调用第三方接口失败呢?是否需要重试?
该情况下需要实现重试策略,网络延迟只是暂时调用不通,重试多次有可能会调用通。
(2)消费者获取消息后,因为代码问题抛出数据异常,是否需要重试?
该情况下是不需要实现重试策略,就算重试多次,最终还是失败的。可以将日志存放起来,后期通过定时任务或者人工补偿形式。如果是重试多次还是失败消息,需要重新发布消费者版本实现消费
可以使用死信队列。
package com.example.consumer;
import com.example.entity.OrderEntity;
import com.example.manager.OrderManager;
import com.example.mapper.OrderMapper;
import com.rabbitmq.client.Channel;
import lombok.RequiredArgsConstructor;
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.stereotype.Component;
import java.io.IOException;
@Slf4j
@Component
@RabbitListener(queues = "fanout_order_queue")
@RequiredArgsConstructor
public class FanoutOrderConsumer {
private final OrderManager orderManager;
private final OrderMapper orderMapper;
@RabbitHandler
public void process(OrderEntity orderEntity, Message message, Channel channel) throws IOException {
try {
log.info("orderEntity: {} ", orderEntity.toString());
int result = orderManager.addOrder(orderEntity);
int i = 1 / 0;
log.info("插入数据库中数据成功");
} catch (Exception e) {
// 记录该消息日志形式 存放数据库db中、后期通过定时任务实现消息补偿、人工实现补偿
// 将该消息存放到死信队列中,单独写一个死信消费者实现消费。
}
}
}
3、MQ如何解决消息幂等性问题
方式1:
生产者在投递消息的时候,生成一个全局唯一id,放在我们消息中。全局唯一id 根据业务来定的 订单号码作为全局的id。
Msg id=123456
Msg id=123456
Msg id=123456
消费者获取到该消息,可以根据该全局唯一id实现去重复。
实际上还是需要在db层面解决数据防重复。
业务逻辑是在做insert操作 使用唯一主键约束。
业务逻辑是在做update操作 使用乐观锁。
二十四、RabbitMQ手动ack模式
1、SpringBoot开启消息确认机制
2、消费者ack代码
package com.example.consumer;
import com.example.entity.OrderEntity;
import com.example.manager.OrderManager;
import com.example.mapper.OrderMapper;
import com.rabbitmq.client.Channel;
import lombok.RequiredArgsConstructor;
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.stereotype.Component;
import java.io.IOException;
@Slf4j
@Component
@RabbitListener(queues = "fanout_order_queue")
@RequiredArgsConstructor
public class FanoutOrderConsumer {
private final OrderManager orderManager;
private final 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) {
log.info("其他消费者已经处理过该业务逻辑");
channel.basicAck(message.getMessageProperties().getDeliveryTag(), false);
return;
}
int result = orderManager.addOrder(orderEntity);
int i = 1 / 0;
log.info("插入数据库中数据成功");
channel.basicAck(message.getMessageProperties().getDeliveryTag(), false);
} catch (Exception e) {
// 记录该消息日志形式 存放数据库db中、后期通过定时任务实现消息补偿、人工实现补偿
// 将该消息存放到死信队列中,单独写一个死信消费者实现消费。
}
}
}