1,RabbitMQ的介绍
1,RabbitMQ的简介
RabbitMQ是一个在AMQP(高级消息队列协议)基础上完成的,可复用的企业消息系统,是当前最主流的消息中间件之一。erlang语言开发,具有高并发特性,吞吐量到万级,MQ功能比较完备,健壮、稳定、易用、跨平台。
2,RabbitMQ的特点
1,可靠性
RabbitMQ使用一些机制来保证可靠性,如持久化、传输确认、发布确认。(保证消息不丢失)
2,灵活的路由
在消息进入队列之前,通过Exchange来路由消息的。对于典型的路由功能,RabbitMQ已经提供了一些内置的Exchange来实现。针对更复杂的路由功能,可以将多个Exchange绑定在一起,也通过插件机制实现自己的Exchange 。
3,消息集群
多个RabbitMQ服务器可以组成一个集群,形成一个逻辑Broker。
4,高可用
队列可以在集群中的机器上进行镜像,使得在部分节点出问题的情况下队列仍然可用。
5,多种协议
RabbitMQ支持多种消息队列协议,比如STOMP、MQTT等等。
6,管理界面
RabbitMQ提供了一个易用的用户界面,使得用户可以监控和管理消息Broker的许多方面。
7,跟踪机制
如果消息异常,RabbitMQ提供了消息跟踪机制,使用者可以找出发生了什么。
8,插件机制
RabbitMQ提供了许多插件,来从多方面进行扩展,也可以编写自己的插件。
3,RabbitMQ工作原理介绍
1,Broker
表示消息队列服务器实体(RabbitMQ服务器)。
2,Exchange(交换机)
交换器,用来接收生产者发送的消息并将这些消息路由给服务器中的队列。
3,Queue(队列)
消息队列,用来保存消息到发送给消费者。它是消息的容器,也是消息的终点。一个消息可投入一个或多个队列。消息一直在队列里面,等待消费者连接到这个队列将其取走。
4,Binding(路由key)
绑定,用于交换器和消息队列之间的关联。一个绑定就是基于路由键(路由key)将交换器和消息队列连接起来的路由规则,所以可以将交换器理解成一个由绑定构成的路由表。
5,Virtual Host(包含了交换机 路由key 队列 )
虚拟主机,表示一批交换器、消息队列和相关对象。虚拟主机是共享相同的身份认证和加密环境的独立服务器域。每个vhost本质上就是一个mini版的RabbitMQ服务器,拥有自己的队列、交换器、绑定和权限机制。vhost是AMQP概念的基础,必须在连接时指定,RabbitMQ默认的vhost是/。当多个不同的用户使用同一个RabbitMQ Server提供的服务时,可以划分出多个vhost,每个用户在自己的 vhost创建交换器和队列等。
6,Connection(连接)
网络连接,比如一个TCP连接。
7,Channel(管道,信道)
信道,多路复用连接中的一条独立的双向数据流通道。信道是建立在真实的TCP连接内地虚拟连接,AMQP 命令都是通过信道发出去的,不管是发布消息、订阅队列还是接收消息,这些动作都是通过信道完成。因为对于操作系统来说建立和销毁TCP都是非常昂贵的开销,所以引入了信道的概念,以复用一条TCP连接。
8,Message(消息)
消息,它由消息头(放属性)和消息体(具体内容)组成。消息头则由一系列的可选属性组成,这些属性包括routing-key(路由键)、priority(相对于其他消息的优先权)、delivery-mode(指出消息可能需要持久性存储)等。
9,Publisher(生产者)
消息的生产者,也是一个向交换器发布消息的客户端应用程序。
10>Consumer(消费者)
消息的消费者,表示一个从消息队列中取得消息的客户端应用程序。
2,RabbitMQ的安装
1,配置docker
1,配置镜像加速器
向docker中添加配置文件daemon.json配置镜像加速器:
tee /etc/docker/daemon.json <<-'EOF'
{
"registry-mirrors": ["https://fymtl0tv.mirror.aliyuncs.com"]
}
EOF
systemctl daemon-reload
systemctl restart docker
2,配置自启动
设置Docker随Linux一起启动:
systemctl enable docker
2, docker安装RabbitMQ
1,拉取镜像
docker pull rabbitmq
docker pull rabbitmq:management
2, 运行RabbitMQ容器
5672是RabbitMQ服务器端口;15672是RabbitMQ控制台端口,可用于访问web管理界面。
docker run --name rabbitmq -p 15672:15672 -p 5672:5672 -d rabbitmq:3-management
停止/启动RabbitMQ容器:
docker stop/start rabbitmq
开放5672和15672端口:
firewall-cmd --zone=public --add-port=5672/tcp --permanent
firewall-cmd --zone=public --add-port=15672/tcp --permanent
firewall-cmd --reload
3,测试访问
访问RabbitMQ的web管理界面测试RabbitMQ是否安装成功:http://LinuxIp:15672
用户名:guest ------密码:guest
3,Rabbitmq的web管理界面设置
1,创建虚拟主机
2,创建用户
3, 给用户分配虚拟主机
4,Rabbitmq在idea中的原生使用
1,创建maven项目并引入依赖
<dependencies>
<!--导入RabbitMQ的依赖-->
<dependency>
<groupId>com.rabbitmq</groupId>
<artifactId>amqp-client</artifactId>
<version>5.9.0</version>
</dependency>
</dependencies>
2,hello模式的使用
一个生产者对应一个消费者,生产者向队列中生产消息,消费者从队列中消费消息。
1,创建生产者主方法
public class Producer {
public static void main(String[] args) throws Exception {
//创建连接工厂
ConnectionFactory connectionFactory = new ConnectionFactory();
//配置连接工厂
connectionFactory.setHost("192.168.40.128");//RabbitMQ服务器ip
connectionFactory.setPort(5672);//RabbitMQ服务器端口
connectionFactory.setUsername("mmy");//用户名
connectionFactory.setPassword("123456");//密码
connectionFactory.setVirtualHost("/myhost");//虚拟主机
//创建连接
Connection con = connectionFactory.newConnection();
//获取通道(多路复用连接,在连接中开出一条通道)
Channel channel = con.createChannel();
/*
声明一个队列:
参数一:队列名称
参数二:队列是否持久化
参数三:通道是否独占这个队列
参数四:当队列中没有消息的时候是否自动删除队列
参数五:其它设置,没有就给null
*/
channel.queueDeclare("hello.queue", true, false, false, null);
/*
发送消息:
参数一:交换机的名称,没有就给""
参数二:路由key,没有就给队列名称
参数三:消息属性,没有就给null
参数四:消息内容 byte[]
*/
channel.basicPublish("", "hello.queue", null, "我是hello的第一个消息".
getBytes());
//关闭资源(后开先关)
channel.close();
con.close();
System.out.println("消息已发出...");
}
}
2,创建消费者主方法
public class Customer {
public static void main(String[] args) throws Exception {
//创建连接工厂
ConnectionFactory connectionFactory = new ConnectionFactory();
//配置连接工厂
connectionFactory.setHost("192.168.40.128");//RabbitMQ服务器ip
connectionFactory.setPort(5672);//RabbitMQ服务器端口
connectionFactory.setUsername("mmy");//用户名
connectionFactory.setPassword("123456");//密码
connectionFactory.setVirtualHost("/myhost");//虚拟主机
//创建连接
Connection con = connectionFactory.newConnection();
//获取通道(多路复用连接,在连接中开出一条通道)
Channel channel = con.createChannel();
/*
消费消息:
参数一:队列名称
参数二:消息消费成功的确认模式,true自动模式,false手动模式
参数三:消费者,当获取到消息的时候会自动执行其回调函数
*/
channel.basicConsume("hello.queue", true, new DefaultConsumer(channel){
/*
回调函数:
参数三:消息头中的属性
参数四:消息体
*/
@Override
public void handleDelivery(String consumerTag, Envelope envelope,
AMQP.BasicProperties properties, byte[] body)
throws IOException {
System.out.println(new String(body));
}
});
//回调是异步的,让线程休眠,回调执行结束再关闭资源
Thread.sleep(1000);
//关闭资源(后开先关)
channel.close();
con.close();
}
}
3,封装重复代码创建RabbitmqUtil
public class RabbitmqUtil {
private static ConnectionFactory connectionFactory;
//实例化并配置连接工厂
static{
connectionFactory = new ConnectionFactory();
connectionFactory.setHost("192.168.9.128");
connectionFactory.setPort(5672);
connectionFactory.setUsername("mmy");
connectionFactory.setPassword("123456");
connectionFactory.setVirtualHost("/myhost");
}
//获取连接的静态封装方法
public static Connection getConnection(){
try {
return connectionFactory.newConnection();
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
//关闭资源的静态封装方法
public static void close(Channel channel, Connection con){
if(channel!=null){
try {
channel.close();
} catch (Exception e) {
e.printStackTrace();
}
}
if(con!=null){
try {
con.close();
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
3,work模式的使用
一个生产者对应多个消费者,但是一条消息只能由一个消费者获得;
轮询分发:将消息队列中的消息,依次发送给所有消费者,一个消息只能被一个消费者获取。
1,创建消费者A主方法
public class CustomerA {
public static void main(String[] args) throws Exception{
//获取连接
Connection con = RabbitmqUtil.getConnection();
//获取通道
Channel channel = con.createChannel();
//声明队列 -- 生产者和消费者,哪方先运行在哪方声明队列
channel.queueDeclare("work.queue", true, false, false, null);
//消费消息
channel.basicConsume("work.queue", true, new DefaultConsumer(channel){
//回调函数
@Override
public void handleDelivery(String consumerTag, Envelope envelope,
AMQP.BasicProperties properties, byte[] body)
throws IOException {
System.out.println("第一个消费者:"+new String(body));
}
});
//让线程休眠,回调执行结束再关闭资源
Thread.sleep(1000*60);
//关闭资源
RabbitmqUtil.close(channel, con);
}
}
2,创建消费者B主方法
public class CustomerB {
public static void main(String[] args) throws Exception{
//获取连接
Connection con = RabbitmqUtil.getConnection();
//获取通道
Channel channel = con.createChannel();
//声明队列
channel.queueDeclare("work.queue", true, false, false, null);
//消费消息
channel.basicConsume("work.queue", true, new DefaultConsumer(channel){
//回调函数
@Override
public void handleDelivery(String consumerTag, Envelope envelope,AMQP.BasicProperties properties, byte[] body)throws IOException {
System.out.println("第二个消费者:"+new String(body));
}
});
//让线程休眠,回调执行结束再关闭资源
Thread.sleep(1000*60);
//关闭资源
RabbitmqUtil.close(channel, con);
}
}
3,创建生产者主方法
public class Producer {
public static void main(String[] args) throws Exception{
//获取连接
Connection con = RabbitmqUtil.getConnection();
//获取通道
Channel channel = con.createChannel();
//生产10条消息
for (int i = 1; i <=10 ; i++) {
channel.basicPublish("", "work.queue", null, ("这是第"+i+"条消息").getBytes());
}
//关闭资源
RabbitmqUtil.close(channel, con);
}
}
4,消息的手动确认机制
public class CustomerA {
public static void main(String[] args) throws Exception{
//获取连接
Connection con = RabbitmqUtil.getConnection();
//获取通道
Channel channel = con.createChannel();
//声明队列 -- 生产者和消费者,哪方先运行在哪方声明队列
channel.queueDeclare("work.queue", true, false, false, null);
//消费消息
channel.basicConsume("work.queue", true, new DefaultConsumer(channel){
int i = 1;
//回调函数
@Override
public void handleDelivery(String consumerTag, Envelope envelope,
AMQP.BasicProperties properties, byte[] body)
throws IOException {
//每消费一条消息i就自增一次,当i大于2时抛出异常,停止消费
if(i>2){
throw new RuntimeException();
}
System.out.println("第一个消费者:"+new String(body));
i++;
}
});
//让线程休眠,回调执行结束再关闭资源
Thread.sleep(1000*60);
//关闭资源
RabbitmqUtil.close(channel, con);
}
}
4,fanout(广播)模式的使用
一个生产者将消息首先发送到交换器,交换器绑定到多个队列,然后被监听该队列的消费者所接收并消费。两个消费者获得了同一条消息,如果没有队列绑定交换机,则消息将丢失,因为交换机没有存储能力,消息只能存储在队列中。
1,创建消费者A的主方法
public class CustomerA {
public static void main(String[] args) throws Exception{
//获取连接
Connection con = RabbitmqUtil.getConnection();
//获取通道
Channel channel = con.createChannel();
/*
声明一个交换机:
生产者和消费者,哪方先运行在哪方声明交换机
参数一:交换机的名称
参数二:交换机的类型,BuiltinExchangeType.FANOUT广播类型
*/
channel.exchangeDeclare("fanoutEx", BuiltinExchangeType.FANOUT);
//声明一个临时队列--消费者连接关闭,队列就会被自动删除--返回值临时队列名称
String queueName = channel.queueDeclare().getQueue();
/*
将队列和交换机绑定:
参数一:队列名称
参数二:交换机名称
参数三:路由Key,没有给""
*/
channel.queueBind(queueName, "fanoutEx", "");
//消费消息
channel.basicConsume(queueName, true, new DefaultConsumer(channel){
//回调函数
@Override
public void handleDelivery(String consumerTag, Envelope envelope,
AMQP.BasicProperties properties, byte[] body)
throws IOException {
System.out.println("消费者A:"+new String(body));
}
});
//让线程休眠,回调执行结束再关闭资源
Thread.sleep(1000*60);
//关闭资源
RabbitmqUtil.close(channel, con);
}
}
2,创建消费者B的主方法
public class CustomerB {
public static void main(String[] args) throws Exception{
//获取连接
Connection con = RabbitmqUtil.getConnection();
//获取通道
Channel channel = con.createChannel();
/*
声明一个交换机:
生产者和消费者,哪方先运行在哪方声明交换机
参数一:交换机的名称
参数二:交换机的类型,BuiltinExchangeType.FANOUT广播类型
*/
channel.exchangeDeclare("fanoutEx", BuiltinExchangeType.FANOUT);
//声明一个临时队列--消费者连接关闭,队列就会被自动删除--返回值临时队列名称
String queueName = channel.queueDeclare().getQueue();
/*
将队列和交换机绑定:
参数一:队列名称
参数二:交换机名称
参数三:路由Key,没有给""
*/
channel.queueBind(queueName, "fanoutEx", "");
//消费消息
channel.basicConsume(queueName, true, new DefaultConsumer(channel){
//回调函数
@Override
public void handleDelivery(String consumerTag, Envelope envelope,
AMQP.BasicProperties properties, byte[] body)
throws IOException {
System.out.println("消费者B:"+new String(body));
}
});
//让线程休眠,回调执行结束再关闭资源
Thread.sleep(1000*60);
//关闭资源
RabbitmqUtil.close(channel, con);
}
}
3,创建生产者的主方法
public class Producer {
public static void main(String[] args) throws Exception{
//获取连接
Connection con = RabbitmqUtil.getConnection();
//获取通道
Channel channel = con.createChannel();
/*
发送消息到交换机:
参数一:交换机的名称
参数二:路由key,没有给""
参数三:消息属性,没有给null
参数四:消息内容 byte[]
*/
channel.basicPublish("fanoutEx", "", null, "这是fanout的一个消息".getBytes());
//关闭资源
RabbitmqUtil.close(channel, con);
}
}
5,routing(路由)模式
生产者将消息发送到direct(直连)交换器,在绑定队列和交换器的时候有一个路由key,生产者发送的消息会指定一个路由key,那么消息只会发送到相应相同key的队列,接着监听该队列的消费者消费消息。
1,创建消费者A的主方法
public class CustomerA {
public static void main(String[] args) throws Exception{
//获取连接
Connection con = RabbitmqUtil.getConnection();
//获取通道
Channel channel = con.createChannel();
/*
声明交换机:
参数一:交换机名称
参数二:交换机类型,BuiltinExchangeType.DIRECT直连类型
*/
channel.exchangeDeclare("directEx", BuiltinExchangeType.DIRECT);
//声明一个临时队列
String queueName = channel.queueDeclare().getQueue();
/*
将队列和交换绑定,同时给队列定义路由key,交换机会匹配路由key将消息
存放到对应的队列中:
参数一:队列名称
参数二:交换机名称
参数三:路由Key
*/
channel.queueBind(queueName, "directEx", "k1");
//消费消息
channel.basicConsume(queueName, true, new DefaultConsumer(channel){
//回调用函数
@Override
public void handleDelivery(String consumerTag, Envelope envelope,
AMQP.BasicProperties properties, byte[] body)
throws IOException {
System.out.println("消费者A:"+new String(body));
}
});
//让线程休眠,回调执行结束再关闭资源
Thread.sleep(1000*60);
//关闭资源
RabbitmqUtil.close(channel, con);
}
}
2,创建消费者B的主方法
public class CustomerB {
public static void main(String[] args) throws Exception{
//获取连接
Connection con = RabbitmqUtil.getConnection();
//获取通道
Channel channel = con.createChannel();
/*
声明交换机:
参数一:交换机名称
参数二:交换机类型,BuiltinExchangeType.DIRECT直连类型
*/
channel.exchangeDeclare("directEx", BuiltinExchangeType.DIRECT);
//声明一个临时队列
String queueName = channel.queueDeclare().getQueue();
/*
将队列和交换绑定,同时给队列定义路由key,交换机会匹配路由key将消息
存放到对应的队列中:
参数一:队列名称
参数二:交换机名称
参数三:路由Key
给该队列定义了两个路由key k1和k2,那么交换机会将匹配到路由key k1
和k2的消息都存放到该队列中;
*/
channel.queueBind(queueName, "directEx", "k1");
channel.queueBind(queueName, "directEx", "k2");
//消费消息
channel.basicConsume(queueName, true, new DefaultConsumer(channel){
//回调用函数
@Override
public void handleDelivery(String consumerTag, Envelope envelope,
AMQP.BasicProperties properties, byte[] body)
throws IOException {
System.out.println("消费者B:"+new String(body));
}
});
//让线程休眠,回调执行结束再关闭资源
Thread.sleep(1000*60);
//关闭资源
RabbitmqUtil.close(channel, con);
}
}
3,创建生产者的主方法
public class Producer {
public static void main(String[] args) throws Exception{
//获取连接
Connection con = RabbitmqUtil.getConnection();
//获取通道
Channel channel = con.createChannel();
//发送消息
//消费者A和消费者B绑定的队列指定的路由Key都有k1,所有此消息往消费者A和消费者B绑定
//的队列都会发送
channel.basicPublish("directEx", "k1", null, "这是k1能看到的消息".getBytes());
//消费者B绑定的队列指定的路由Key有k2,所有此消息只会往消费者B绑定的队列发送
channel.basicPublish("directEx", "k2", null, "这是k2能看到的消息".getBytes());
//关闭资源
RabbitmqUtil.close(channel, con);
}
}
5,Rabbitmq在springboot框架中的使用
1,环境配置
创建消费者和生产者两个模块
配置文件application.yml
spring:
rabbitmq:
host: 192.168.40.128
port: 5672
username: mmy
password: 123456
virtual-host: /myhost
2,hello模式的使用
1,创建生产者配置类
@Configuration
public class ProducerConfig {
//配置一个Queue的对象,一个Queue对象即为一个队列,构造器参数为队列名称
@Bean
public Queue helloQueue(){
return new Queue("hello.boot.queue");
}
}
2,创建生产者测试类
@SpringBootTest
class RabbitmqProducerApplicationTests {
//注入RabbitMQ模板
@Autowired
private RabbitTemplate rabbitTemplate;
@Test
void contextLoads() {
//向队列hello.boot.queue中发送消息,参数一队列名称,参数二消息内容
rabbitTemplate.convertAndSend("hello.boot.queue", "这是boot的hello模式的消息");
}
}
3,创建消费者监听类
@Component
public class HelloListener {
/*
一个监听方法即为一个消费者:
1)监听方法必须public且无返回值;
2)@RabbitListener(queues = "hello.boot.queue")
监听名称为hello.boot.queue的队列,即从名称为hello.boot.queue队列中消费消息;
3)参数Message message消息对象,包含消息头和消息体;
4)参数Channel channel通道
*/
@RabbitListener(queues = "hello.boot.queue")
public void handlerHelloMsg(Message message, Channel channel){
System.out.println("消费者:"+new String(message.getBody()));
}
}
3,work模式的使用
1,创建消费者配置类
@Configuration
public class CustomerConfig {
//创建个队列,队列名称为work.boot.queue
@Bean
public Queue workQueue(){
return new Queue("work.boot.queue");
}
}
2,创建消费者监听类
@Component
public class WorkListener {
//一个监听方法即为一个消费者
//从名称为work.boot.queue的队列中消费消息
@RabbitListener(queues = "work.boot.queue")
public void handlerWorkMsg1(Message message, Channel channel){
System.out.println("消费者A:"+new String(message.getBody()));
}
//从名称为work.boot.queue的队列中消费消息
@RabbitListener(queues = "work.boot.queue")
public void handlerWorkMsg2(Message message, Channel channel){
System.out.println("消费者B:"+new String(message.getBody()));
}
}
3,创建生产者测试类
@SpringBootTest
class WorkTest {
//注入RabbitMQ模板
@Autowired
private RabbitTemplate rabbitTemplate;
@Test
void contextLoads() {
//向队列work.boot.queue中发送10条消息
for(int i=1;i<=10;i++){
rabbitTemplate.convertAndSend("work.boot.queue", "work模式第"+i+"个消息");
}
}
}
4,fanout广播模式的使用
1,创建消费者配置类
@Configuration
public class CustomerConfig {
//配置一个FanoutExchange的对象,即为一个广播类型的交换机,构造器参数为交换机名称
@Bean
public FanoutExchange fanoutEx(){
return new FanoutExchange("boot.fanoutEx");
}
//创建个队列,队列名称为fanout.boot.queue1
@Bean
public Queue fanoutQueue1(){
return new Queue("fanout.boot.queue1");
}
//将队列fanout.boot.queue1和交换机boot.fanoutEx绑定
@Bean
public Binding fanoutQueue1Bind(){
return BindingBuilder.bind(fanoutQueue1()).to(fanoutEx());
}
//再创建个队列,队列名称为fanout.boot.queue2
@Bean
public Queue fanoutQueue2(){
return new Queue("fanout.boot.queue2");
}
//将队列fanout.boot.queue2和交换机boot.fanoutEx也绑定
@Bean
public Binding fanoutQueue2Bind(){
return BindingBuilder.bind(fanoutQueue2()).to(fanoutEx());
}
}
2,创建消费者监听类
@Component
public class FanoutListener {
//一个监听方法即为一个消费者
/*
@RabbitListener(queues = "fanout.boot.queue1")
从名称为fanout.boot.queue1的队列中消费消息;
*/
@RabbitListener(queues = "fanout.boot.queue1")
public void handlerFanoutMsg1(Message message, Channel channel){
System.out.println("消费者A:"+new String(message.getBody()));
}
/*
@RabbitListener(queues = "fanout.boot.queue2")
从名称为fanout.boot.queue2的队列中消费消息;
*/
@RabbitListener(queues = "fanout.boot.queue2")
public void handlerFanoutMsg2(Message message, Channel channel){
System.out.println("消费者B:"+new String(message.getBody()));
}
}
3,创建生产者测试类
@SpringBootTest
class FanoutTest {
//注入RabbitMQ模板
@Autowired
private RabbitTemplate rabbitTemplate;
@Test
void contextLoads() {
//向交换机boot.fanoutEx中发送消息
rabbitTemplate.convertAndSend("boot.fanoutEx", "", "这是boot的一条广播消息");
}
}
5,routing路由模式的使用
1,创建消费者配置类
@Configuration
public class CustomerConfig {
//配置一个DirectExchange的对象,即为一个直连类型的交换机,构造器参数为交换机名称
@Bean
public DirectExchange directEx(){
return new DirectExchange("boot.directEx");
}
//创建个队列,队列名称为direct.boot.queue1
@Bean
public Queue directQueue1(){
return new Queue("direct.boot.queue1");
}
//将队列direct.boot.queue1和交换机boot.directEx绑定,同时给队列direct.boot.queue1
//定义个路由key为k1
@Bean
public Binding directQueue1BindK1(){
return BindingBuilder.bind(directQueue1()).to(directEx()).with("k1");
}
//再创建个队列,队列名称为direct.boot.queue2
@Bean
public Queue directQueue2(){
return new Queue("direct.boot.queue2");
}
//将队列direct.boot.queue2和交换机boot.directEx绑定,同时给队列direct.boot.queue2
//定义个路由key为k1
@Bean
public Binding directQueue2BindK1(){
return BindingBuilder.bind(directQueue2()).to(directEx()).with("k1");
}
//将队列direct.boot.queue2和交换机boot.directEx绑定,同时给队列direct.boot.queue2
//再定义个路由key为k2
@Bean
public Binding directQueue2BindK2(){
return BindingBuilder.bind(directQueue2()).to(directEx()).with("k2");
}
}
2,创建消费者监听类
@Component
public class RoutingListener {
//一个监听方法即为一个消费者
/*
@RabbitListener(queues = "direct.boot.queue1")
从名称为direct.boot.queue1的队列中消费消息;
*/
@RabbitListener(queues = "direct.boot.queue1")
public void handlerDirectMsg1(Message message, Channel channel){
System.out.println("消费者A:"+new String(message.getBody()));
}
/*
@RabbitListener(queues = "direct.boot.queue2")
从名称为direct.boot.queue2队列中消费消息;
*/
@RabbitListener(queues = "direct.boot.queue2")
public void handlerDirectMsg2(Message message, Channel channel){
System.out.println("消费者B:"+new String(message.getBody()));
}
}
3,创建生产者测试类
@SpringBootTest
class RoutingTest {
//注入RabbitMQ模板
@Autowired
private RabbitTemplate rabbitTemplate;
@Test
void contextLoads() {
//发送消息
//消费者A和消费者B绑定的队列指定的路由Key都有k1,所有此消息往消费者A和消费者B
//绑定的队列都会发送
rabbitTemplate.convertAndSend("boot.directEx", "k1", "这是k1能看到的消息");
//消费者B绑定的队列指定的路由Key有k2,所有此消息只会往消费者B绑定的队列发送
rabbitTemplate.convertAndSend("boot.directEx", "k2", "这是k2能看到的消息");
}
}
6,消息的监视
RabbitMQ提供了一个消费监视功能。(目的是确保消息不丢失,如果丢失了,也能知道在哪丢失的,然后进行人工处理)
1,以routing模式进行演示
1,修改生产者配置文件
#开启消息发送到交换机的监视
spring.rabbitmq.publisher-confirm-type=correlated
#开启消息未发送到队列的监视
spring.rabbitmq.publisher-returns=true
2,修改生产者测试类
@SpringBootTest
class RoutingWatchTest {
//注入RabbitMQ模板
@Autowired
private RabbitTemplate rabbitTemplate;
@Test
void contextLoads() {
//在消息发送前进行监视
/*
给消息发送到交换机设置回调 -- 监视消息发送到了交换机:
调用的是RabbitTemplate的setConfirmCallback()方法,其参数类型是函数式接口
ConfirmCallback,通过Lambda表达式传递一个ConfirmCallback接口的实现类对象,
重写的是其confirm()方法;
confirm()方法有三个参数:
参数一correlationData:表示相关数据,一般都没有;
参数二ack:消息是否到达交换机,true到达,false未到达;
参数三cause:消息未到达交换机的原因
*/
rabbitTemplate.setConfirmCallback((correlationData, ack, cause) -> {
System.out.println(correlationData);
System.out.println(ack);
System.out.println(cause);
});
/*
给消息未发送到队列设置回调 -- 监视消息未发送到队列:
调用的是RabbitTemplate的setReturnCallback()方法,其参数类型是函数式接口
ReturnCallback,通过Lambda表达式传递一个ReturnCallback接口的实现类对象,
重写的是其returnedMessage()方法;
returnedMessage()方法有5个参数:
参数一message:消息
参数二replyCode:失败的状态码
参数三replyText:失败的说明
参数四exchange:交换机
参数五routingKey:路由key
*/
rabbitTemplate.setReturnCallback((message, replyCode, replyText, exchange,
routingKey) -> {
System.out.println(message);
System.out.println(replyCode);
System.out.println(replyText);
System.out.println(exchange);
System.out.println(routingKey);
});
//向交换机boot.directEx绑定的路由key为k2的队列direct.boot.queue2发送消息
rabbitTemplate.convertAndSend("boot.directEx", "k2", "这是k2能看到的消息");
}
}
3,rabbitmq客户端删除交换机
运行结果如下:
启动消费者端重写建立交换机则可运行成功
4,rabbitmq客户端断开交换机与路由key(k2)绑定的队列的关系
2,代码优化
定义实现类,同时实现ConfirmCallback和ReturnCallback接口,并分别重写confirm()和returnedMessage()方法,完成交换机消息到达的监视和队列消息未到达的监视。
1,创建配置类
@Component
public class MsgWatchHandler
implements RabbitTemplate.ConfirmCallback, RabbitTemplate.ReturnCallback {
@Override
public void confirm(CorrelationData correlationData, boolean ack, String cause) {
System.out.println(correlationData);
System.out.println(ack);
System.out.println(cause);
}
@Override
public void returnedMessage(Message message, int replyCode, String replyText,
String exchange, String routingKey) {
System.out.println(message);
System.out.println(replyCode);
System.out.println(replyText);
System.out.println(exchange);
System.out.println(routingKey);
}
//注入RabbitTemplate
@Autowired
private RabbitTemplate rabbitTemplate;
/*
@PostConstruct对Bean对象做后置处理。目的是IOC容器启动,该实现类的Bean对象被创建,
RabbitTemplate的Bean对象注入完成,就执行init()方法;
然后在init()方法中调用RabbitTemplate的setConfirmCallback()和setReturnCallback()
方法,分别对交换机消息到达和队列消息未到达进行监视;
*/
@PostConstruct
public void init(){
rabbitTemplate.setConfirmCallback(this);
rabbitTemplate.setReturnCallback(this);
}
}
7,消息的签收
自动签收:不是很安全,消息消费失败后无法弥补。吞吐量大,1.5M/s - 4M/s。
手动签收:安全,能够保证消息不会丢失。吞吐量较低,1M/s左右。
消息id:生产者在发送消息的时候,可以为消息设置一个消息id,可用于唯一的标识消息。
1,搭建routing模式,消费者配置类
@Configuration
public class CustomerConfig {
//配置一个直连交换机,交换机名称为boot.ackEx
@Bean
public DirectExchange ackEx(){
return new DirectExchange("boot.ackEx");
}
//创建个队列,队列名称为ack.boot.queue
@Bean
public Queue ackQueue(){
return new Queue("ack.boot.queue");
}
//将队列ack.boot.queue和交换机boot.ackEx绑定,同时给队列ack.boot.queue定义个
//路由key为k
@Bean
public Binding ackQueueBindK(){
return BindingBuilder.bind(ackQueue()).to(ackEx()).with("k");
}
}
2,创建消费者监听队列
@Component
public class AckListener {
//注入StringRedisTemplate
@Autowired
private StringRedisTemplate redisTemplate;
/*
@RabbitListener(queues = "ack.boot.queue")
从名称为ack.boot.queue的队列中消费消息;
*/
@RabbitListener(queues = "ack.boot.queue")
public void handlerMsg(Message message, Channel channel){
//拿到消息投递id
long deliveryTag = message.getMessageProperties().getDeliveryTag();
try {
//拿到消息,将消息保存到数据库
//System.out.println(10/0);//制造异常,表示消息保存失败
/*
保存成功了,则签收消息:
参数一:消息的投递id
参数二:false不批量签收
*/
channel.basicAck(deliveryTag, false);
System.out.println("消费者:"+new String(message.getBody()));
} catch (Exception exception){
/*
保存失败或签收失败,则让队列继续投递消息,最多再投递3次,
如果这3次还是保存或签收失败,则让队列不再继续投递:
*/
System.out.println("消费者:失败");
//以消息id为键向redis保存键值对,并让值自增一次,返回值为自增的值即消息投递次数
String messageId = message.getMessageProperties().getMessageId();
Long count = redisTemplate.opsForValue().increment(messageId);
if(count<=3){
try {
/*
如果消息投递次数<=3次,则拒收,让消息重新回到队列,被重新投递:
参数一:消息的投递id
参数二:false不批量签收
参数三:true拒收
*/
channel.basicNack(deliveryTag, false, true);
} catch (IOException e) {
e.printStackTrace();
}
}else{
//如果消息投递次数>3次,则签收,目的是不让队列再继续投递消息了
try {
channel.basicAck(deliveryTag, false);
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}
3,创建生产者测试类
@SpringBootTest
class RoutingAckTest {
//注入RabbitMQ模板
@Autowired
private RabbitTemplate rabbitTemplate;
@Test
void contextLoads() {
//向交换机boot.ackEx绑定的路由key为k的队列ack.boot.queue发送消息
rabbitTemplate.convertAndSend("boot.ackEx", "k", "这是k能看到的消息", message -> {
//给消息设置个id
String id = UUID.randomUUID().toString();
message.getMessageProperties().setMessageId(id);
return message;
});
}
}
4,修改配置文件
#开启手动签收模式
spring.rabbitmq.listener.simple.acknowledge-mode=manual
8,延时队列
1,搭建routing模式消费者配置类
@Configuration
public class CustomerConfig {
//配置一个直连交换机,交换机名称为boot.deadEx
@Bean
public DirectExchange deadEx(){
return new DirectExchange("boot.deadEx");
}
//配置一个延时队列,队列名称为boot.msQueue
@Bean
public Queue msQueue(){
Map<String, Object> map = new HashMap<>();
//延时时间(1分钟)
map.put("x-message-ttl", 1000*60);
//时间到后走哪个交换机
map.put("x-dead-letter-exchange", "boot.deadEx");
//时间到后走哪个路由key绑定的队列
map.put("x-dead-letter-routing-key", "dk");
return new Queue("boot.msQueue", true, false, false, map);
}
}
2,创建监听队列
@Component
public class DeadQueueListener {
//一个监听方法即为一个消费者
/*
@RabbitListener(queues = "boot.msQueue")
从名称为boot.msQueue的队列中消费消息,即从死信队列中消费消息;
*/
@RabbitListener(queues = "boot.msQueue")
public void handlerMsg(Message message, Channel channel){
System.out.println("消费者:"+new String(message.getBody()));
System.out.println("收到消息的时间:"+new Date());
//签收消息
try {
channel.basicAck(message.getMessageProperties().getDeliveryTag(), false);
} catch (IOException e) {
e.printStackTrace();
}
}
}
3,创建生产者测试类
@SpringBootTest
class MsQueueTest {
//注入RabbitMQ模板
@Autowired
private RabbitTemplate rabbitTemplate;
@Test
void contextLoads() {
//向延时队列boot.msQueue发送消息
rabbitTemplate.convertAndSend("boot.msQueue","这是延时消息");
System.out.println("发送消息时间:"+new Date());
}
}
4,发送消息也可指定延时时间(生产者测试类)
@SpringBootTest
class MsQueueTest {
//注入RabbitMQ模板
@Autowired
private RabbitTemplate rabbitTemplate;
@Test
void contextLoads() {
//向延时队列boot.msQueue发送消息
rabbitTemplate.convertAndSend("boot.msQueue","这是延时消息", message -> {
//指定消息发送的演示时间为20秒
message.getMessageProperties().setExpiration("20000");
return message;
});
System.out.println("发送消息时间:"+new Date());
}
}
9,MQ的应用场景
1,异步处理(增加用户体验)
用户注册后,需要发注册邮件和注册短信。传统的做法有两种1.串行的方式;2.并行方式
1,串行方式
2, 并行方式
2,应用解耦(高内聚,低耦合的设计思想)
3, 流量削锋
流量削锋也是消息队列中的常用场景,一般在秒杀或团抢活动中使用广泛。
应用场景:秒杀活动,一般会因为流量过大,导致流量暴增,应用挂掉。为解决这个问题,一般需要在应用前端加入消息队列。
a、可以控制活动的人数
b、可以缓解短时间内高流量压垮应用
4, 日志处理
日志处理是指将消息队列用在日志处理中,比如Kafka的应用,解决大量日志传输的问题。架构简化如下:
5, 消息通讯
1,点对点通讯:
2,聊天室通讯:
10,秒杀实战
1,创建生产者boot项目
1,引入依赖
<!--rabbitmq的依赖-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
<!--redis的依赖-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!--手动引入的hutool工具包的依赖(需要使用bloom过滤器)-->
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.3.9</version>
</dependency>
<!--手动引入fastjson的依赖-->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.78</version>
</dependency>
2,配置文件
#应用名称
spring.application.name=spike-producer
#服务器端口
server.port=8080
#--------------RabbitMQ的配置----------------
#RabbitMQ的服务器ip
spring.rabbitmq.host=192.168.9.128
#RabbitMQ的服务器端口
spring.rabbitmq.port=5672
#用户名
spring.rabbitmq.username=mmy
#密码
spring.rabbitmq.password=123456
#虚拟主机
spring.rabbitmq.virtual-host=/myhost
#------------redis的配置---------------------
#redis服务器的ip(安装redis的linux的ip)
spring.redis.host=192.168.9.128
#redis的端口
spring.redis.port=6379
#如果redis设置了密码则指定密码
spring.redis.password=mmy123
#操作redis的数据库的下标
spring.redis.database=1
3,主运行类
@SpringBootApplication
public class SpikeProducerApplication {
public static void main(String[] args) {
SpringApplication.run(SpikeProducerApplication.class, args);
}
//配置Bloom过滤器
@Bean
public BitMapBloomFilter bitMapBloomFilter(){
//构造器参数为位图大小(可以理解为过滤面的大小)
return new BitMapBloomFilter(100);
}
}
4,控制器类
@RestController
public class SpikeController {
//注入bloom过滤器
@Autowired
private BitMapBloomFilter bitMapBloomFilter;
//注入rabbitmq模板
@Autowired
private RabbitTemplate rabbitTemplate;
//注入redis模板
@Autowired
private StringRedisTemplate redisTemplate;
/*
处理秒杀请求的方法,请求url为/doSpike,并传参用户id userId、商品id goodsId,
向客户端响应字符串文本;
*/
@RequestMapping("/doSpike")
public String doSpike(Integer userId, Integer goodsId){
/*
1.通过bloom过滤器判断用户是否参与过该商品的秒杀:
一个用户对一个商品只能秒杀一次
*/
//通过用户id和商品id生成唯一标识
String spikeId = userId+"-"+goodsId;
//判断bloom过滤器中是否存在该标识,存在则已参与秒杀,不存在则未参与
if(bitMapBloomFilter.contains(spikeId)){
return "您已参加过该商品的抢购...";
}
/*
2.操作redis判断是否还有库存,如果还有则对库存做扣减
*/
//商品库存在redis中以键goods_stock:商品id,值库存数存储;
//对键goods_stock:商品id的值即库存数做递减,并返回递减后的库存数;
Long stock = redisTemplate.opsForValue().decrement("goods_stock:" + goodsId);
if(stock<0){
return "商品已被抢完了,下次早点来哦...";
}
//到此说明秒杀成功了
//3.将用户id和商品id生产的唯一标识添加到bloom过滤器
bitMapBloomFilter.add(spikeId);
/*
4.将订单信息发送到rabbitmq,即将用户id和商品id发送到rabbitmq
*/
//将用户id和商品id存到map再转成json字符串发送给rabbitmq
Map<String, Integer> map = new HashMap<>();
map.put("userId", userId);
map.put("goodsId", goodsId);
String jsonStr = JSON.toJSONString(map);
//发送消息到名称为spike.queue的队列
rabbitTemplate.convertAndSend("spike.queue", jsonStr);
//5.向客户端返回成功结果
return "正在拼命抢购中,请稍后去订单查看...";
}
}
2,创建消费者boot项目
1,引入依赖
<!--rabbitmq的依赖-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
<!--redis的依赖-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!--mybatis的依赖-->
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.1.4</version>
</dependency>
<!--mysql驱动-->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<!--lombok的依赖-->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<!--手动引入fastjson的依赖-->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.78</version>
</dependency>
2,主程序运行类
//指定Mapper接口所在的包,自动扫描Mapper接口
@MapperScan(basePackages = "com.mmy.dao")
@SpringBootApplication
public class SpikeServiceApplication {
public static void main(String[] args) {
SpringApplication.run(SpikeServiceApplication.class, args);
}
}
3,配置文件
#应用名称
spring.application.name=spike-service
#服务器端口
server.port=8081
#-------------------mybatis的配置-----------------------------------------------
#配置数据源
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.url=jdbc:mysql://localhost:3306/db_rabbitmq?serverTimezone=UTC
spring.datasource.username=root
spring.datasource.password=1234
#指定sql映射文件的位置
mybatis.mapper-locations=classpath:mapper/*.xml
#输出日志
mybatis.configuration.log-impl=org.apache.ibatis.logging.slf4j.Slf4jImpl
#开启驼峰命名映射规则
mybatis.configuration.map-underscore-to-camel-case=true
#--------------RabbitMQ的配置----------------
#RabbitMQ的服务器ip
spring.rabbitmq.host=192.168.9.128
#RabbitMQ的服务器端口
spring.rabbitmq.port=5672
#用户名
spring.rabbitmq.username=mmy
#密码
spring.rabbitmq.password=123456
#虚拟主机
spring.rabbitmq.virtual-host=/myhost
#开启手动签收模式
spring.rabbitmq.listener.simple.acknowledge-mode=manual
#------------redis的配置---------------------
#redis服务器的ip(安装redis的linux的ip)
spring.redis.host=192.168.9.128
#redis的端口
spring.redis.port=6379
#如果redis设置了密码则指定密码
spring.redis.password=mmy123
#操作redis的数据库的下标
spring.redis.database=1
4,实体类
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Goods {
private Integer goodsid;
private String goodsname;
private Double price;
private Integer status;
private Integer stock;
private Date createtime;
private Date updatetime;
private Integer spike;
}
5,mapper接口类
public interface GoodsMapper {
//查询所有参与秒杀的商品
public List<Goods> getSpikeGoods();
}
6,mapper映射文件
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.mmy.dao.GoodsMapper">
<!--public List<Goods> getSpikeGoods()-->
<select id="getSpikeGoods" resultType="com.mmy.domain.Goods">
select * from goods where spike = 1
</select>
</mapper>
7,配置类
//实现CommandLineRunner接口并重写run(),run()方法会在boot应用启动时执行;
@Component
public class MysqlToRedis implements CommandLineRunner {
//注入redis模板
@Autowired
private StringRedisTemplate redisTemplate;
//注入GoodsMapper
@Autowired
private GoodsMapper goodsMapper;
//boot应用启动将mysql数据同步到redis
@Override
public void run(String... args) throws Exception {
//查询所有参数秒杀的商品
List<Goods> spikeGoods = goodsMapper.getSpikeGoods();
//将每个参与秒杀的商品,以goods_stock:商品id为键,库存量为值,存储到redis
if(!CollectionUtils.isEmpty(spikeGoods)){
for (Goods goods : spikeGoods) {
Integer id = goods.getGoodsid();
Integer stock = goods.getStock();
redisTemplate.opsForValue().set("goods_stock:"+id, stock.toString());
}
}
}
}