提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档
RabbitMq 的概念及应用(原生api、springboot、Springcloud Stream整合篇)
提示:以下是本篇文章正文内容,下面案例可供参考
一、RabbitMQ的介绍
1、rabbitMQ架构图
2、名词解释:
channel:信道。生产,消费,发布订阅都需要信道
connection:创建tcp链接,使生产者和消费者与MQ相链接
virtual host:虚拟主机。可以有多个。可以根据virtual host 进行开发环境,测试环境,生产环境的数据隔离
broker:rabbitMQ实体
exchange:交换机。生产者发送消息给exchange,exchange通过binding key 绑定queue。
binding:绑定exchange和queue
queue:队列。接收exchange发送过来的message
routingkey:生产者通过routingkey 指定消息发送到那个exchange中
二、配置及安装
1、安装(此处使用docker进行安装)
docker-compose.yml
version: "3"
services:
rabbitmq:
image: //镜像名称+版本
container_name: rabbitMQ //容器名称
restart: always //启动docker就运行这个容器
volumes:
-./data/:/var/lib/rabbitmq/ //数据卷的挂载
port
-5672:5672 //端口映射
-15672:15672
// 这个指令可以查找当前所有的镜像版本
docker search rabbit
//在yml文件,当前页输入以下指令,自动拉取rabbitMQ镜像
docker-compose up -d
//拉取完毕后,用下面指令,查看是否创建完毕
curl localhost:5672 //出现AMQP 表示创建成功
2、开启可视化功能
docker exec -it rabbitMQ bash //进入容器内部
cd opt/rabbitMQ/sbin //进入sbin目录
./rabbit-plugins enable rabbitmq_management //开启成功
三、RabbitMQ的使用
<dependencies>
<dependency>
<groupId>com.rabbitmq</groupId>
// 原生的rabbitMQ 依赖的jar包
<artifactId>amqp-client</artifactId>
<version>5.14.2</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
// 构建springboot 工程
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
</dependencies>
提供一个统一的连接工厂
@Data
@Component
@ConfigurationProperties(prefix = "rabbit")
public class ConnectionFactory {
private static String host ;
private static Integer port;
private static String username;
private static String password;
private static String virtualHost;
public static Connection getConnection() throws Exception {
com.rabbitmq.client.ConnectionFactory factory = new com.rabbitmq.client.ConnectionFactory();
factory.setHost(host);
factory.setUsername(username);
factory.setPassword(password);
factory.setVirtualHost(virtualHost);
factory.setPort(port);
Connection connection = factory.newConnection();
return connection;
}
}
1、WorkQueue模式(工作队列模式)
特点:不需要指定交换机,这里使用的是默认的交换机(AMQP default)。
一个生产者,多个消费者,每个消息只能被消费一次。
为了避免消息积压,可以设置多个消费者。
同时,为了提高效率,防止个别消费者消费速度慢,可以调用 basicQos 方法,设置其中的参数prefetchCount =1 ,表示每次只分发一个,消费完后,再继续分发。确保消费的性能。
public class WorkQueue {
private static String queueName = "workQueue";
public String publish(){
try {
//获取链接
Channel channel = getChannel();
//发布消息
//交换机名字(这里使用的是默认的交换机)、队列名字、其他的消息头信息、消息内容
channel.basicPublish("",queueName,null,"hello".getBytes());
} catch (Exception e) {
e.printStackTrace();
}
return "ok";
}
public String consumer1() throws IOException {
try {
//获取链接
Channel channel = getChannel();
//每次拉取几个消息进行消费,可以适当提高,不同的性能,可以消费不同数量的消息
channel.basicQos(1);
//发布消息
//交换机名字(这里使用的是默认的交换机)、队列名字、其他的消息头信息、消息内容
DefaultConsumer consumer = new DefaultConsumer(channel){
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
System.out.println("1号开始进行消费" + new String(body));
//手动ack
channel.basicAck(envelope.getDeliveryTag() , false);
}
};
channel.basicConsume(queueName ,false , consumer);
} catch (Exception e) {
e.printStackTrace();
}
System.in.read();
return "ok";
}
public String consumer2() throws IOException {
try {
//获取链接
Channel channel = getChannel();
//每次拉取几个消息进行消费,可以适当提高,不同的性能,可以消费不同数量的消息
channel.basicQos(1);
//发布消息
//交换机名字(这里使用的是默认的交换机)、队列名字、其他的消息头信息、消息内容
DefaultConsumer consumer = new DefaultConsumer(channel){
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
System.out.println("2号开始进行消费" + new String(body));
//手动ack
channel.basicAck(envelope.getDeliveryTag() , false);
}
};
channel.basicConsume(queueName ,false , consumer);
} catch (Exception e) {
e.printStackTrace();
}
System.in.read();
return "ok";
}
private Channel getChannel() throws Exception {
Connection connection = ConnectionFactory.getConnection();
//构建channel
Channel channel = connection.createChannel();
//声明一个队列 生产者和消费者 最好都要申明。 申明使用的参数必须一样
// 队列名、是否消息持久化、是否允许多个监听、队列长时间不使用会自动删除、其他参数
channel.queueDeclare(queueName, false, false, false, null);
return channel;
}
}
1、在一个队列中,如果有多个消费者,那么消费者之间对于同一个消息是竞争关系(默认使用轮询策略)。
2、workQueue对于任务过重或者任务较多的情况可以提高处理速度(通过设置channel.basicQos() ,每次批量拉取消息进行消费,提高消费的能力)。
void basicPublish(String exchange, String routingKey, BasicProperties props, byte[] body) throws IOException;
/**
* String contentType, //消息内容的类型
* String contentEncoding, //消息内容的编码格式
* Map<String,Object> headers,//header类型的交换机可以用到
* Integer deliveryMode,//消息持久化 1 不持久化 2 持久化
* Integer priority,//优先级
* String correlationId, //关联id
* String replyTo,//通常用于命名回调队列
* String expiration,//设置过期消息过期时间
* String messageId, //消息id
* Date timestamp, //消息的时间戳
* String type, //类型
* String userId, //用户ID
* String appId, //应用程序id
* String clusterId //集群id
*/
//此处单独补充一下 关于BasicProperties 的介绍。
AMQP.BasicProperties properties = new AMQP.BasicProperties().builder()
.contentType("远程会诊")
.contentEncoding("UTF_8")
.deliveryMode(2)
.build();
channel.basicPublish("",queueName,properties,"hello".getBytes());
// 消费者在进行消费的时候,可以根据contentType 进行过滤,指定消费
2、Publish/Subscribe(发布订阅模式)
一次同时向多个消费者发送消息。
在消费的过程中,多了一个exchange 角色。
exchange 有三种模式:
1、fanout 广播模式 将消息交给所有绑定到交换机的队列(不需要routing key 但是不能为空)
可以有多个消费者
每个消费者都有自己的queue
每个queue都需要进行exchange的绑定
生产者发送消息到exchange
exchange将消息传递给所有绑定的queue,无法进行指定传递。
如此可以实现一条消息被多个消费者消费
生产者:
@RestController
public class PubPublish {
@Autowired
ClientUtil util;
private String exchangeName = "pub-sub";
private String queueName1 = "pub-sub-1";
private String queueName2 = "pub-sub-2";
@GetMapping("pub")
public void pub() throws IOException, TimeoutException {
Connection connect = util.connect();
Channel channel = connect.createChannel();
// 申明一个交换机, 并且将模式调整为fanout模式
// 参数1:交换机名称
// 参数2:交换机的模式
channel.exchangeDeclare(exchangeName, BuiltinExchangeType.FANOUT);
// 申明队列
channel.queueDeclare(queueName1,true,false,false,null);
channel.queueDeclare(queueName2,true,false,false,null);
// 绑定交换机和队列
// 参数1: 队列的名称
// 参数2: 交换机的名称
// 参数3: 发布订阅模式,由于交换机和队列已经绑定,所以routingKey可有可无,不影响
channel.queueBind(queueName1 , exchangeName , "abc");
channel.queueBind(queueName2 , exchangeName , "cba");
// 发送消息
channel.basicPublish(exchangeName , "aaa" , null , "pub-sub".getBytes());
System.out.println("消息发送完毕~~~");
}
}
消费者:
@RestController
public class PubConsum {
@Autowired
ClientUtil util;
private String exchangeName = "pub-sub";
private String queueName1 = "pub-sub-1";
private String queueName2 = "pub-sub-2";
@GetMapping("sub1")
public void sub() throws IOException, TimeoutException {
Connection connect = util.connect();
Channel channel = connect.createChannel();
// 申明一个交换机, 并且将模式调整为fanout模式
// 参数1:交换机名称
// 参数2:交换机的模式
channel.exchangeDeclare(exchangeName, BuiltinExchangeType.FANOUT);
// 申明队列
channel.queueDeclare(queueName1,true,false,false,null);
// 绑定交换机和队列
// 参数1: 队列的名称
// 参数2: 交换机的名称
// 参数3: 发布订阅模式,由于交换机和队列已经绑定,所以routingKey可随意填写,不影响
channel.queueBind(queueName1 , exchangeName , "abc");
DefaultConsumer consumer = new DefaultConsumer(channel) {
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
System.out.println(queueName1 + "开始进行消费: " + new String(body));
}
};
channel.basicConsume(queueName1 , false ,consumer);
System.in.read();
}
@GetMapping("sub2")
public void sub2() throws IOException, TimeoutException {
Connection connect = util.connect();
Channel channel = connect.createChannel();
// 申明一个交换机, 并且将模式调整为fanout模式
// 参数1:交换机名称
// 参数2:交换机的模式
channel.exchangeDeclare(exchangeName, BuiltinExchangeType.FANOUT);
// 申明队列
channel.queueDeclare(queueName2,true,false,false,null);
// 绑定交换机和队列
// 参数1: 队列的名称
// 参数2: 交换机的名称
// 参数3: 发布订阅模式,由于交换机和队列已经绑定,所以routingKey可随意填写,不影响
channel.queueBind(queueName2 , exchangeName , "cba");
DefaultConsumer consumer = new DefaultConsumer(channel) {
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
System.out.println(queueName2 + "开始进行消费: " + new String(body));
}
};
channel.basicConsume(queueName2 , false ,consumer);
System.in.read();
}
}
2、direct定向模式 将消息交给指定routingkey 的队列
队列与交换机的绑定不再随意绑定,需要指定routing key
生产者在传递消息给exchange的时候需要指定routing key
routing key 一致的队列才会收到消息
@RestController
public class RoutPub {
@Autowired
ClientUtil util;
private String exchangeName = "routing";
private String queueName1 = "one";
private String queueName3 = "two";
@GetMapping("rout")
public void pub() throws IOException, TimeoutException {
Connection connect = util.connect();
Channel channel = connect.createChannel();
// 申明一个交换机, 并且将模式调整为DIRECT模式
// 参数1:交换机名称
// 参数2:交换机的模式
channel.exchangeDeclare(exchangeName, BuiltinExchangeType.DIRECT);
// 申明队列
channel.queueDeclare(queueName1,true,false,false,null);
channel.queueDeclare(queueName3,true,false,false,null);
// 绑定交换机和队列
// 参数1: 队列的名称
// 参数2: 交换机的名称
// 参数3: 发布订阅模式,由于交换机和队列已经绑定,所以routingKey可有可无,不影响
channel.queueBind(queueName1 , exchangeName , "红色");
channel.queueBind(queueName3 , exchangeName , "蓝色");
channel.queueBind(queueName3 , exchangeName , "黑色");
// 发送消息
channel.basicPublish(exchangeName , "红色" , null , "一号种子".getBytes());
channel.basicPublish(exchangeName , "蓝色" , null , "二号种子".getBytes());
channel.basicPublish(exchangeName , "绿色" , null , "三号种子".getBytes());
System.out.println("消息发送完毕~~~");
}
}
3、topic通配符 将消息交给符合routing pattern(路由模式)的队列
topic模式和direct模式相同, 都可以指定routing key 不同的是topic 可以使用通配符
exchange(交换机)只负责消息的转发,并不具备存储的能力。因此如果没有任何队里与交换机进行绑定或者没有符合路由规则的队列,那么消息会丢失。
@RestController
public class TopicPub {
@Autowired
ClientUtil util;
private String exchangeName = "routing";
private String queueName1 = "topic-one";
private String queueName3 = "topic-two";
@GetMapping("rout")
public void pub() throws IOException, TimeoutException {
Connection connect = util.connect();
Channel channel = connect.createChannel();
// 申明一个交换机, 并且将模式调整为TOPIC模式
// 参数1:交换机名称
// 参数2:交换机的模式
channel.exchangeDeclare(exchangeName, BuiltinExchangeType.TOPIC);
// 申明队列
channel.queueDeclare(queueName1,true,false,false,null);
channel.queueDeclare(queueName3,true,false,false,null);
// 绑定交换机和队列
// 参数1: 队列的名称
// 参数2: 交换机的名称
// 参数3: 路由规则(*表示占位符,#表示通配符)
channel.queueBind(queueName1 , exchangeName , "*.orange.*");
channel.queueBind(queueName3 , exchangeName , "*.*.hot");
channel.queueBind(queueName3 , exchangeName , "lazy.#");
// 发送消息
channel.basicPublish(exchangeName , "big.orange.apple" , null , "一号种子".getBytes());
channel.basicPublish(exchangeName , "eat.small.hot" , null , "二号种子".getBytes());
channel.basicPublish(exchangeName , "lazy.dog.dog.dog.dog" , null , "三号种子".getBytes());
System.out.println("消息发送完毕~~~");
}
}
四、整合Springboot
配置声明
spring:
rabbitmq:
username: guest
password: guest
host: 127.0.0.1
port: 5672
virtualHost: /
listener:
simple:
acknowledge-mode: manual // 手动ack
pom依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
声明交换机、队列、绑定关系
@Configuration
public class RabbitMQConfig {
// fanout: 广播模式 direct:路由模式 topic:通配符模式
public static final String EXCHANGE = "fanout";
public static final String QUEUE = "my-queue";
public static final String ROUTING_KEY = "*.message.*";
// 申明交换机
@Bean
public Exchange declareExchange(){
return ExchangeBuilder.fanoutExchange(EXCHANGE).build();
}
// 申明队列
@Bean
public Queue declareQUEUE(){
return QueueBuilder.durable(QUEUE).build();
}
// 将队列和交换机进行绑定
@Bean
public Binding declareBinding(){
return BindingBuilder.bind(declareQUEUE()).to(declareExchange()).with(ROUTING_KEY).noargs();
}
}
生产者代码
@RestController
public class Publisher {
@Autowired
RabbitTemplate rabbitTemplate;
public static final String EXCHANGE = "fanout";
public static final String QUEUE = "my-queue";
public static final String ROUTING_KEY = "*.message.*";
@GetMapping("boot")
public void push(){
rabbitTemplate.convertAndSend(EXCHANGE, "hello.message.send", "data", new MessagePostProcessor() {
@Override
public Message postProcessMessage(Message message) throws AmqpException {
MessageProperties properties = message.getMessageProperties();
properties.setAppId("123");
properties.setContentType("自定义");
return message;
}
});
System.out.println("消息发送成功");
}
}
消费者代码如下:
@Component
public class BootConsumer {
public static final String EXCHANGE = "fanout";
public static final String QUEUE = "my-queue";
public static final String ROUTING_KEY = "*.message.*";
@RabbitListener(queues = {QUEUE})
public void consume(Channel channel , Message message) throws IOException {
System.out.println("监听到消息。。。 开始进行消费");
String contentType = message.getMessageProperties().getContentType();
System.out.println(contentType);
System.out.println(message.getMessageProperties().getAppId());
// 需要在yml文件中,配置手动ack 才可以在接收参数中 加入Message
channel.basicAck(message.getMessageProperties().getDeliveryTag() ,false);
System.out.println("发送的消息体为 : " + new String(message.getBody()));
System.out.println("消息消费完毕。。。。。。。");
}
}
五、整合springcloud Stream
pom依赖
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-stream-rabbit</artifactId>
</dependency>
配置文件
spring:
cloud:
# rabbitmq
stream:
binders:
default_rabbit:
type: rabbit
environment:
spring:
rabbitmq:
# local
host: localhost
port: 5672
username: guest
password: guest
virtual-host: /
#########
bindings:
delay-producer:
binder: default_rabbit # 设置要绑定的消息服务的具体设置
destination: exchange.delay #exchange名称,交换模式默认是topic
content-type: application/json
delay-consumer:
binder: default_rabbit # 设置要绑定的消息服务的具体设置
destination: exchange.delay
content-type: application/json
group: message
consumer:
concurrency: 3 # 初始/最少/空闲时 消费者数量。默认1
data-producer:
binder: default_rabbit # 设置要绑定的消息服务的具体设置
destination: exchange.data #exchange名称,交换模式默认是topic
content-type: application/json
data-consumer:
binder: default_rabbit # 设置要绑定的消息服务的具体设置
destination: exchange.data
content-type: application/json
group: message
consumer:
concurrency: 3 # 初始/最少/空闲时 消费者数量。默认1
rabbit:
bindings:
delay-producer:
producer:
delayedExchange: true
delay-consumer:
consumer:
prefetch: 50
# 默认:1。queue的消费者的最大数量。当前消费者数量不足以及时消费消息时, 会动态增加消费者数量, 直到到达最大数量, 即该配置的值.
maxConcurrency: 6
acknowledge-mode: manual # 手动应答
delayedExchange: true
max-attempts: 3 #最大线程数
auto-bind-dlq: true
republishToDlq: true # 默认false。当为true时,死信队列接收到的消息的headers会更加丰富,多了异常信息和堆栈跟踪。
data-consumer:
consumer:
prefetch: 50
# 默认:1。queue的消费者的最大数量。当前消费者数量不足以及时消费消息时, 会动态增加消费者数量, 直到到达最大数量, 即该配置的值.
maxConcurrency: 6 #最大线程数
# acknowledge-mode: manual # 手动应答
max-attempts: 3
# 不是延迟队列的情况下,下面3条可以不用配置
# delayedExchange: true
# auto-bind-dlq: true
# republishToDlq: true # 默认false。当为true时,死信队列接收到的消息的headers会更加丰富,多了异常信息和堆栈跟踪。
#由于此处使用到了延迟队列,所以需要提前在rabbitMQ中开启延迟队列的插件。
#在sbin目录下
./rabbitmq-plugs enable delayed-message-exchange
#如果plugs目录下不存在,则需要手动下载,并拷贝到plugs
生产者
public interface DelayProducer {
String DELAY_QUEUE = "delay-producer";
@Output(DELAY_QUEUE)
MessageChannel delayProducer();
}
public interface DataProducer {
String DATA_QUEUE = "data-producer";
@Output(DATA_QUEUE)
MessageChannel dataProducer();
}
消费者
public interface DelayConsumer {
String ORDER_DELAY_CONSUMER = "delay-consumer";
@Input(ORDER_DELAY_CONSUMER)
SubscribableChannel orderDelayConsumer();
}
public interface DataConsumer {
String ORDER_DATA_CONSUMER = "data-consumer";
@Input(ORDER_DATA_CONSUMER)
SubscribableChannel orderDataConsumer();
}
应用实例:
@RestController
@Slf4j
@Component
@EnableBinding({DelayProducer.class, DelayConsumer.class}) // 必须使用该注解,申明的生产者和消费者才会生效
@RequiredArgsConstructor
public class HowToUseService {
private final DelayProducer delayProducer;
private static String DELAY_CONSULTATION_ORDER_AUTO_CANCEL = "DELAY_CONSULTATION_ORDER_AUTO_CANCEL";
private static String DELAY_CONSULTATION_ORDER_AUTO_CANCEL_CONDITION = "headers['delay-type']=='" + DELAY_CONSULTATION_ORDER_AUTO_CANCEL + "'";
// 模拟生成订单号
AtomicInteger order = new AtomicInteger(0);
/**
* @return void
* @author Halo
* @description 在线问诊下单订并发送到延迟队列
* @date 2021/12/21 16:01
* http://localhost:9102/mq?num=1&timeout=5000&ex=false
*/
@GetMapping("/mq")
public void onlineCons(Integer num, Integer timeout//, boolean ex
) {
for (int i = 0; i < num; i++) {
log.info("创建问诊订单成功........");
log.info("创建远程支付订单成功.........");
log.info("在线问诊订单默认 15分钟未支付 自动取消.");
RecordMsg recordMsg = new RecordMsg();
recordMsg.setOrderNo("123456");
recordMsg.setNode("CreateOrderMsgHandler");
Message<String> build = MessageBuilder.withPayload(JSON.toJSONString(recordMsg))
.setHeader(MqConstant.X_DELAY_HEADER, timeout)
.setHeader(MqConstant.DELAY_TYPE, DELAY_CONSULTATION_ORDER_AUTO_CANCEL)
.build();
boolean b = this.delayProducer.delayProducer().send(build);
log.info("消息发送:{}", b);
}
}
// 在线问诊订单状态监听
// 这里的condition 是用于过滤消息的。 不同的业务场景可以申明多个不同的监听器
@StreamListener(target = DelayConsumer.ORDER_DELAY_CONSUMER, condition = DELAY_CONSULTATION_ORDER_AUTO_CANCEL_CONDITION)
public void receiveDelayOrder(Message<String> entityMessage,
@Header(AmqpHeaders.CHANNEL) Channel channel,
@Header(AmqpHeaders.DELIVERY_TAG) Long deliveryTag) {
log.info("getHeaders : {}", JSON.toJSONString(entityMessage.getHeaders()));
// 尝试次数 次数达到了yml配置次数时 消息进入死信队列
// 如果上面的步骤出现异常 则从新入队列 查询 判断 ... 保证幂等性!
// 默认重复3次进入死信队列
Object deliveryAttempt = entityMessage.getHeaders().get("deliveryAttempt");
log.info("尝试次数 记得判断业务需求 .... 记录日志 , {}", deliveryAttempt);
// 模拟失败
// Object ex = entityMessage.getHeaders().get("ex");
// if (Objects.equals(Boolean.FALSE, ex)) {
// throw new RuntimeException("模拟失败 自动重试3次 后仍然失败进入死信队列 :" + entityMessage.getPayload());
// }
try {
log.info("头信息:{}", JSON.toJSONString(entityMessage.getHeaders()));
log.info("消息是:{}", entityMessage.getPayload());
// 这个地方需要做幂等性 判断
log.info("根据解析的数据的id进行查询数据库 根据需求判断是否签收消息 抛出异常 ...");
log.info("根据解析的数据的id进行查询数据库 判断数据库已存在订单的状态 如果超时 付支付 则更新订单为取消状态 ......");
log.info("根据解析的数据的id进行查询数据库 判断数据库已存在订单的状态 查询支付中心状态 ......等等很多要考虑的地方 自己根据业务需求实现细节");
// 应答模式确认接收到消息,后面那个参数如果是false,就是只确认签收当前消息,如果是true,则签收全部比当前TagId小的消息
channel.basicAck(deliveryTag, false);
} catch (Exception e) {
e.printStackTrace();
}
}
总结
提示:这里主要记录了,rabbit的简单应用,之后会进行进阶学习记录。