rabbitmq学习
1. centos安装rabbitmq
1.1 安装erlang
rabbitmq是由erlang编写的,所以需要erlang的环境
#拉取erlang
wget -P https://github.com/rabbitmq/rabbitmq-server/releases/download/v3.8.9/rabbitmq-server-3.8.9-1.el7.noarch.rpm
#安装 Erlang
rpm -Uvh erlang-23.0-1.el7.x86_64.rpm
#安装 socat
yum install -y socat
1.2 安装rabbitmq
#拉取rabbitmq
wget -P https://github.com/rabbitmq/rabbitmq-server/releases/download/v3.8.9/rabbitmq-server-3.8.9-1.el7.noarch.rpm
#安装RabbitMQ
rpm -Uvh rabbitmq-server-3.8.9-1.el7.noarch.rpm
1.3 启动和关闭
#启动
sudo systemctl start rabbitmq-server
#关闭
sudo systemctl status rabbitmq-server
#查看运行状态
sudo systemctl stop rabbitmq-server
#开机自启动
sudo systemctl enable rabbitmq-server
1.4 安装rabbitmq的web控制台
rabbitmq有一个默认的guest用户,但只能通过localhost访问,所以需要添加一个能够远程访问的用户。
#开启插件
rabbitmq-plugins enable rabbitmq_management
#添加用户
rabbitmqctl add_user admin admin
#为用户分配资源权限
rabbitmqctl set_user_tags admin administrator
1.5 添加防火墙规则
RabbitMQ 服务启动后,还不能进行外部通信,需要将端口添加都防火墙
#添加防火墙暴露端口
firewall-cmd --zone=public --add-port=4369/tcp --permanent
firewall-cmd --zone=public --add-port=5672/tcp --permanent
firewall-cmd --zone=public --add-port=25672/tcp --permanent
firewall-cmd --zone=public --add-port=15672/tcp --permanent
#重启防火墙
firewall-cmd --reload
1.6 测试
浏览器输入:http://ip+端口(15672),例如:http://192.168.235.102:15672
2. springboot集成rabbitmq
pom.xml依赖
<!--rabbitmq-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
application.yml配置
server:
port: 8021
spring:
#给项目来个名字
application:
name: rabbitmq-provider
#配置rabbitMq 服务器
rabbitmq:
host: 127.0.0.1
port: 5672
username: root
password: root
#虚拟host 可以不设置,使用server默认host
virtual-host: JCcccHost
2.1 直连交换机(direct exchange)
通过new Queue()创建一个队列,传递三个核心参数
durable:是否持久化,默认是false,持久化队列:会被存储在磁盘上,当消息代理重启时仍然存在,暂存队列:当前连接有效
exclusive:默认也是false,只能被当前创建的连接使用,而且当连接关闭后队列即被删除。此参考优先级高于durable
autoDelete:是否自动删除,当没有生产者或者消费者使用此队列,该队列会自动删除。
new Queue(“TestDirectQueue”,true,true,false);
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.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class DirectRabbitConfig {
//队列 起名:TestDirectQueue
@Bean
public Queue TestDirectQueue() {
// durable:是否持久化,默认是false,持久化队列:会被存储在磁盘上,当消息代理重启时仍然存在,暂存队列:当前连接有效
// exclusive:默认也是false,只能被当前创建的连接使用,而且当连接关闭后队列即被删除。此参考优先级高于durable
// autoDelete:是否自动删除,当没有生产者或者消费者使用此队列,该队列会自动删除。
// return new Queue("TestDirectQueue",true,true,false);
//一般设置一下队列的持久化就好,其余两个就是默认false
return new Queue("TestDirectQueue",true);
}
//Direct交换机 起名:TestDirectExchange
@Bean
DirectExchange TestDirectExchange() {
// return new DirectExchange("TestDirectExchange",true,true);
return new DirectExchange("TestDirectExchange",true,false);
}
//绑定
//将队列和交换机绑定, 并设置用于匹配键:TestDirectRouting
@Bean
Binding bindingDirect() {
return BindingBuilder.bind(TestDirectQueue()).to(TestDirectExchange()).with("TestDirectRouting");
}
@Bean
DirectExchange lonelyDirectExchange() {
return new DirectExchange("lonelyDirectExchange");
}
}
创建消息接收监听类,DirectReceiver.java,通过RabbitListener监听TestDirectQueue的消息
@Component
//监听的队列名称 TestDirectQueue
@RabbitListener(queues = "TestDirectQueue")
public class DirectReceiver {
@RabbitHandler
public void process(Map testMessage) {
System.out.println("DirectReceiver消费者收到消息 : " + testMessage.toString());
}
}
然后写个简单的接口进行消息推送(根据需求也可以改为定时任务等等,具体看需求),SendMessageController.java:
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
@RestController
public class SendMessageController {
//使用RabbitTemplate,这提供了接收/发送等等方法
@Autowired
RabbitTemplate rabbitTemplate;
@GetMapping("/sendDirectMessage")
public String sendDirectMessage() {
String messageId = String.valueOf(UUID.randomUUID());
String messageData = "test message, hello!";
String createTime = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
Map<String,Object> map=new HashMap<>();
map.put("messageId",messageId);
map.put("messageData",messageData);
map.put("createTime",createTime);
//将消息携带绑定键值:TestDirectRouting 发送到交换机TestDirectExchange
rabbitTemplate.convertAndSend("TestDirectExchange", "TestDirectRouting", map);
return "ok";
}
}
调用后会根据TestDirectRouting找到TestDirectExchange上的TestDirectQueue进行消费
2.2 主题交换机(topic exchange)
核心是路由键,以.分隔的多个字母视为一个单词,*号代表一个单词,#号可以替代零个或多个单词。
@Configuration
public class TopicRabbitConfig {
//绑定键
public final static String man = "topic.man";
public final static String woman = "topic.woman";
@Bean
public Queue firstQueue() {
return new Queue(TopicRabbitConfig.man);
}
@Bean
public Queue secondQueue() {
return new Queue(TopicRabbitConfig.woman);
}
@Bean
TopicExchange exchange() {
return new TopicExchange("topicExchange");
}
//将firstQueue和topicExchange绑定,而且绑定的键值为topic.man
//这样只要是消息携带的路由键是topic.man,才会分发到该队列
@Bean
Binding bindingExchangeMessage() {
return BindingBuilder.bind(firstQueue()).to(exchange()).with(man);
}
//将secondQueue和topicExchange绑定,而且绑定的键值为用上通配路由键规则topic.#
// 这样只要是消息携带的路由键是以topic.开头,都会分发到该队列
@Bean
Binding bindingExchangeMessage2() {
return BindingBuilder.bind(secondQueue()).to(exchange()).with("topic.#");
}
}
创建消息监听类
@Component
public class Receiver {
@RabbitListener(queues = "topic.man")
public void topicProcess(Map testMessage) {
System.out.println("topic.man消费者收到消息 : " + testMessage.toString());
}
@RabbitListener(queues = "topic.woman")
public void process(Map testMessage) {
System.out.println("topic.woman消费者收到消息 : " + testMessage.toString());
}
}
当routingkey传入topic.man时,以上两个队列都会监听到并消费,传入topic.man只有第一个会消费到
rabbitTemplate.convertAndSend("topicExchange", routingKey, manMap);
2.3 消息回调
添加publisher-confirm-type和publisher-returns
spring:
#配置rabbitMq 服务器
rabbitmq:
host: 43.154.193.211
port: 5672
username: admin
password: admin
virtual-host: /hinata
#确认消息已发送到交换机(Exchange)
publisher-confirm-type: correlated
#确认消息已发送到队列(Queue)
publisher-returns: true
添加回调方法,消息只要被 rabbitmq broker 接收到就会触发 confirmCallback 回调,消息未能投递到目标 queue 里将触发回调 returnCallback,未能投递到目标queue的消息会丢失或送至死信队列
@Configuration
public class RabbitConfig {
@Bean
public RabbitTemplate createRabbitTemplate(ConnectionFactory connectionFactory){
RabbitTemplate rabbitTemplate = new RabbitTemplate();
rabbitTemplate.setConnectionFactory(connectionFactory);
//设置开启Mandatory,才能触发回调函数,无论消息推送结果怎么样都强制调用回调函数
rabbitTemplate.setMandatory(true);
rabbitTemplate.setConfirmCallback(new RabbitTemplate.ConfirmCallback() {
@Override
public void confirm(CorrelationData correlationData, boolean ack, String cause) {
System.out.println("ConfirmCallback: "+"相关数据:"+correlationData);
System.out.println("ConfirmCallback: "+"确认情况:"+ack);
System.out.println("ConfirmCallback: "+"原因:"+cause);
}
});
rabbitTemplate.setReturnCallback(new RabbitTemplate.ReturnCallback() {
@Override
public void returnedMessage(Message message, int replyCode, String replyText, String exchange, String routingKey) {
System.out.println("ReturnCallback: "+"消息:"+message);
System.out.println("ReturnCallback: "+"回应码:"+replyCode);
System.out.println("ReturnCallback: "+"回应信息:"+replyText);
System.out.println("ReturnCallback: "+"交换机:"+exchange);
System.out.println("ReturnCallback: "+"路由键:"+routingKey);
}
});
return rabbitTemplate;
}
}
2.4 应答模式
rabbitmq共有三种应答模式
none(无应答模式):在这种模式下,不管消费者异常消费,还是正常消费,MQ服务器中的队列都会自动删除已消费的消息
auto(自动应答模式):当mq的应答模式配置为auto,或者没有进行配置时,系统会默认为自动应答模式。在这种情况下,只要我们的消费者,在消费消息的时候没有抛出异常,那服务端MQ会认为,消息消费正常,删除队列中的消息;如果消费过程中,抛出了异常,消息会进行自动补偿,重会队列头部,再次被拉到消费者的缓冲区(prefetch count),进行重复消费。此时如果缓冲区的大小设置为1,那么整个队列就会被阻塞,unacked也会显示为1(单线程消费的情况下)。
manual(手动应答模式):当设置为应手动应答时,我们需要在消费消息的时候手动告诉MQ我们消费的情况,否者MQ会一直等待消费端的消息,如果一直没有应答,当消费数量达到缓冲区大小(prefetch count)后,队列会全部阻塞,缓冲区中的消息会在客户端重启后再进行一次消费,直到被手动提交。
在application.yml,acknowledge-mode设置应答模式,max-attempts设置最大重试次数,也是auto模式下消费端异常最多重复投递次数,超过后会直接丢弃或放至死信队列。
rabbitmq:
host: 43.154.193.211
port: 5672
username: admin
password: admin
virtual-host: /hinata
#确认消息已发送到交换机(Exchange)
publisher-confirm-type: correlated
#确认消息已发送到队列(Queue)
publisher-returns: true
listener:
simple:
acknowledge-mode: manual # 配置 Consumer 手动提交
retry:
enabled: true
max-attempts: 5
initial-interval: 1000
设置为manual手动提交后,有三种提交方法
1.basicAck:表示成功确认,使用此回执方法后,消息会被rabbitmq broker 删除。
void basicAck(long deliveryTag, boolean multiple)
deliveryTag:表示消息投递序号,每次消费消息或者消息重新投递后,deliveryTag都会增加。手动消息确认模式下,我们可以对指定deliveryTag的消息进行ack、nack、reject等操作。
multiple:是否批量确认,值为 true 则会一次性 ack所有小于当前消息 deliveryTag 的消息。批量确认的意思是假设我先发送三条消息deliveryTag分别是5、6、7,可它们都没有被确认,当我发第四条消息此时deliveryTag为8,multiple设置为 true,会将5、6、7、8的消息全部进行确认。
2.basicNack:表示失败确认,一般在消费消息业务异常时用到此方法,可以将消息重新投递入队列。
void basicNack(long deliveryTag, boolean multiple, boolean requeue)
deliveryTag:表示消息投递序号。
multiple:是否批量确认。
requeue:值为 true 消息将重新入队列,值为false消息将丢失或放入死信队列。
3.basicReject:拒绝消息,与basicNack区别在于不能进行批量操作,其他用法很相似。
void basicReject(long deliveryTag, boolean requeue)
deliveryTag:表示消息投递序号。
requeue:值为 true 消息将重新入队列。
消费端完整代码如下
@Component
public class Receiver {
@RabbitListener(queues = "testDirectQueue")
@RabbitHandler
public void directProcess(Map<String, Object> map, Channel channel, Message message)throws IOException {
try {
System.out.println(1 / 0);
System.out.println("DirectReceiver消费者收到消息 : " + map);
channel.basicAck(message.getMessageProperties().getDeliveryTag(), false);
} catch (Exception e) {
System.out.println("DirectReceiver消费者收到消息,执行异常 : " + map);
channel.basicNack(message.getMessageProperties().getDeliveryTag(), false,false);
}
}
}
2.5 死信队列
“死信”是RabbitMQ中的一种消息机制,当你在消费消息时,如果队列里的消息出现以下情况:
消息被否定确认,使用 channel.basicNack 或 channel.basicReject ,并且此时requeue属性被设置为false。
消息在队列的存活时间超过设置的生存时间(TTL)时间。
消息队列的消息数量已经超过最大队列长度。
那么该消息将成为“死信”。
“死信”消息会被RabbitMQ进行特殊处理,如果配置了死信队列信息,那么该消息将会被丢进死信队列中,如果没有配置,则该消息将会被丢弃。
如图为死信队列结构图
创建配置类DeadLetterRabbitConfig,配置了业务交换机与业务队列绑定,死信队列和死信交换机绑定,其中业务队列配置了死信交换机,当消息出现以上三种情况,会被丢进死信队列中
@Configuration
public class DeadLetterRabbitConfig {
private static final String BUSINESS_EXCHANGE = "businessExchange";
private static final String DEAD_LETTER_EXCHANGE = "deadLetterExchange";
private static final String BUSINESS_QUEUE = "businessQueue";
private static final String DEAD_LETTER_QUEUE = "deadLetterQueue";
private static final String DEAD_LETTER_QUEUE_ROUTING_KEY = "deadLetterQueueRoutingKey";
//业务交换机
@Bean
public FanoutExchange businessExchange(){
return new FanoutExchange(BUSINESS_EXCHANGE);
}
//死信交换机
@Bean
public DirectExchange deadLetterExchange(){
return new DirectExchange(DEAD_LETTER_EXCHANGE);
}
//业务队列
@Bean
public Queue businessQueue(){
HashMap<String, Object> args = new HashMap<>();
//指定死信交换机
args.put("x-dead-letter-exchange",DEAD_LETTER_EXCHANGE);
//指定死信交换机绑定死信队列的路由键
args.put("x-dead-letter-routing-key",DEAD_LETTER_QUEUE_ROUTING_KEY);
return QueueBuilder.durable(BUSINESS_QUEUE).withArguments(args).build();
}
//死信队列
@Bean
public Queue deadLetterQueue(){
return new Queue(DEAD_LETTER_QUEUE);
}
//声明业务交换机和业务队列的绑定
@Bean
public Binding businessBind(){
return BindingBuilder.bind(businessQueue()).to(businessExchange());
}
//声明死信交换机和死信队列的绑定
@Bean
public Binding deadLetterBind(){
return BindingBuilder.bind(deadLetterQueue()).to(deadLetterExchange()).with(DEAD_LETTER_QUEUE_ROUTING_KEY);
}
}
配置消费者Receiver
@Component
public class Receiver {
/**
* 测试死信队列
*
* @param map
* @param channel
* @param message
* @throws IOException
*/
@RabbitListener(queues = "businessQueue")
@RabbitHandler
public void businessProcess(Map<String, Object> map, Channel channel, Message message) throws IOException {
try {
if (map.containsKey("dead-letter")) {
System.out.println(1 / 0);
}
System.out.println("消费者收到消息 : " + map);
channel.basicAck(message.getMessageProperties().getDeliveryTag(), false);
} catch (Exception e) {
System.out.println("消费者收到消息,执行异常 : " + map);
channel.basicNack(message.getMessageProperties().getDeliveryTag(), false, false);
}
}
@RabbitListener(queues = "deadLetterQueue")
@RabbitHandler
public void deadLetterProcess(Map<String, Object> map, Channel channel, Message message) throws IOException {
System.out.println("死信队列收到消息 : " + map);
channel.basicAck(message.getMessageProperties().getDeliveryTag(), false);
}
}
模拟发送消息
@ApiOperation(value = "测试死信队列", notes = "测试死信队列")
@PostMapping("/testDeadLetter")
public String testDeadLetter(String content) {
String messageId = String.valueOf(UUID.randomUUID());
String createTime = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
Map<String, Object> map = new HashMap<>();
map.put("messageId", messageId);
map.put("messageData", content);
map.put("createTime", createTime);
rabbitTemplate.convertAndSend("businessExchange", "", map);
return "ok";
}
最终的执行结果,可以看到再消息basicNack后被扔进了死信队列中
2.6 延时队列
延时队列是在死信队列的基础上设置消息过期时间(TTL)从而达到延时消费的效果,有两种实现方法
1.设置队列的过期时间,增加x-message-ttl参数
@Bean
public Queue businessQueue(){
HashMap<String, Object> args = new HashMap<>();
//指定死信交换机
args.put("x-dead-letter-exchange",DEAD_LETTER_EXCHANGE);
//指定死信交换机绑定死信队列的路由键
args.put("x-dead-letter-routing-key",DEAD_LETTER_QUEUE_ROUTING_KEY);
//指定消息过期时间 单位:毫秒
args.put("x-message-ttl",5000);
return QueueBuilder.durable(BUSINESS_QUEUE).withArguments(args).build();
}
2.设置每条消息的过期时间,在生产端指定每条消息的过期时间
rabbitTemplate.convertAndSend("businessExchange", "", map,hock->{
hock.getMessageProperties().setExpiration("5000");
return hock;
});
将businessQueue的消费端去掉,等到设定的时间后,会丢弃至死信队列中,监听死信队列完成业务处理。第二种方法并不能保证消息能在指定的时间被丢弃,RabbitMQ 只会检查第一个消息是否过期,如果过期则丢到死信队列。尤其是第一个消息时间的延迟很长 30s,第二个消息的延迟很短 10s,第二个消息也不会优先执行,并且10s过去以后,第二条消息不会立即过期,而是会等第一条消息被消费后,消费第二条消息,才会判断过期。
2.7 插件实现延时队列
以上两种方法都有缺点,通过插件的方式用一个队列和一个交换机就可实现延时队列。首先在服务器上安装插件,将其上传至/usr/lib/rabbitmq/lib/rabbitmq_server-3.8.9/plugins下
链接:https://pan.baidu.com/s/1mNRIWJnL-7NNydKdsUOOUw
提取码:zde2
然后执行
rabbitmq-plugins enable rabbitmq_delayed_message_exchange
添加配置类DelayedQueueRabbitConfig
@Configuration
public class DelayedQueueRabbitConfig {
private static final String DELAYED_EXCHANGE = "delayedExchange";
private static final String DELAYED_QUEUE = "delayedQueue";
private static final String DELAYED_ROUTING_KEY = "delayedRoutingKey";
//延时交换机
@Bean
public CustomExchange delayedExchange() {
HashMap<String, Object> args = new HashMap<>();
args.put("x-delayed-type", "direct");
return new CustomExchange(DELAYED_EXCHANGE, "x-delayed-message", true, false, args);
}
//延时队列
@Bean
public Queue delayedQueue() {
return new Queue(DELAYED_QUEUE);
}
//声明业务交换机和业务队列的绑定
@Bean
public Binding delayedBind() {
return BindingBuilder.bind(delayedQueue()).to(delayedExchange()).with(DELAYED_ROUTING_KEY).noargs();
}
}
消费端添加
@RabbitListener(queues = "delayedQueue")
@RabbitHandler
public void delayedQueueProcess(Map<String, Object> map, Channel channel, Message message) throws IOException {
System.out.println("死信队列收到消息 : " + map + ",时间" + new Date());
channel.basicAck(message.getMessageProperties().getDeliveryTag(), false);
}
模拟发送消息
@ApiOperation(value = "测试延时队列", notes = "测试延时队列")
@PostMapping("/testDelayedQueue")
public String testDelayedQueue(String content, Integer time) {
String messageId = String.valueOf(UUID.randomUUID());
String createTime = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
Map<String, Object> map = new HashMap<>();
map.put("messageId", messageId);
map.put("messageData", content);
map.put("createTime", createTime);
rabbitTemplate.convertAndSend("delayedExchange", "delayedRoutingKey", map, hock -> {
hock.getMessageProperties().setDelay(time);
return hock;
});
return "ok";
}
分别发送 消息1/20000和消息2/2000,最终结果如下