spring boot 集成RabbitMQ

首先介绍下MQ
1:MQ全称为Message Queue, 消息队列(MQ)是一种应用程序对应用程序的通信方法
2:MQ是消费-生产者模型的一个典型的代表,一端往消息队列中不断写入消息,而另一端则可以读取或者订阅队列中的消息。
3:在项目中,将一些无需即时返回且耗时的操作提取出来,进行了异步处理,而这种异步处理的方式大大的节省了服务器的请求响应时间,从而提高了系统的吞吐量。
4:MQ则是遵循了AMQP协议的具体实现和产品。
5:AMQP,即Advanced Message Queuing Protocol,一个提供统一消息服务的应用层标准高级消息队列协议,是应用层协议的一个开放标准,为面向消息的中间件设计。基于此协议的客户端与消息中间件可传递消息,并不受客户端/中间件不同产品,不同的开发语言等条件的限制.Erlang中的实现有 RabbitMQ.
RabbitMQ的 工作流程如下所示

前面是生产者,中间是RabbitMQ,后面是消费者。MQ还分为交换机(exchange)和队列(queues)。两者的关系是队列绑定到交换机上。具体流程是:生产者生产消息发送到交换机,交换机负责把消息转发到与自己绑定的队列上(交换机不负责存储),消费者从自己监听的队列中取出消息并且消费。
这里还存在两个机制,一个是Confirm机制,一种是ACK机制。前一种是生产者发送给MQ时,是否发送成功的确认。后者是消息被消费者拿到是否手动确认MQ删除此消息。
两者都可以不设置,Confirm不设置,表示无论生产者是否成功发送消息到MQ,都不做处理,这里需要自己代码实现。这样的缺点就是,在向某个交换机发送消息时,由于某些原因没成功,又没设置Confirm,造成消息丢失。ACK不设置,表示noack即不确认,只要消费者拿到消息,MQ就会删除队列中的消息,无论消费者是否成功消费。这样的缺点是,当消费者拿到消息后没有消费成功,此消息已经在MQ中删除了,造成消息丢失。这两种机制都在后面的代码中有所体现。
下面介绍下交换机的类型,有四种类型,分别为Direct,Topic,headers,Fanout.
1: Direct是RabbitMQ默认的交换机模式,也是最简单的模式.即创建消息队列的时候,指定一个BindingKey.当发送者发送消息的时候,指定对应的Key.当Key和消息队列的BindingKey一致的时候,消息将会被发送到该消息队列中.
eg:这是一个完整key的匹配。如果一个队列绑定到交换机上要求路由键位"dog",则只有被标记为"dog"的消息才被转发到该队列上,不会转发"dog.1"和"dog.1.2",只会转发"dog"。

2:T opic转发信息主要是依据通配符,队列和交换机的绑定主要是依据一种模式(通配符+字符串),而当发送消息的时候,只有指定的Key和该模式相匹配的时候,消息才会被发送到该消息队列中.
eg:队列需要绑定在一个模式上,符号“#”匹配一个或多个词,符号“*”只能匹配一个词。因为"dog.#"能匹配到"dog.1.2",但是“dog.*”只能匹配到“dog.1”
3:headers也是根据一个规则进行匹配,在消息队列和交换机绑定的时候会指定一组键值对规则,而发送消息的时候也会指定一组键值对规则,当两组键值对规则相匹配的时候,消息会被发送到匹配的消息队列中.
4:Fanout是路由广播的形式,将会把消息发给绑定它的全部队列,即便设置了key,也会被忽略.
spring boot 集成 RabbitMQ
基于maven,创建两个工程,一个生产者和一个消费者


两者的pom文件是一样的,看下生产者的
    <parent>
	    <groupId>org.springframework.boot</groupId>
	    <artifactId>spring-boot-starter-parent</artifactId>
	    <version>1.5.9.RELEASE</version>
    </parent>
    <properties>
        <java.version>1.8</java.version>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    </properties>
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-devtools</artifactId>
            <optional>true</optional>
            <scope>true</scope>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-actuator</artifactId>
        </dependency>
        <!-- 添加springboot对amqp的支持 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-amqp</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-tomcat</artifactId>
            <scope>provided</scope>
        </dependency>
        <dependency>
            <groupId>org.apache.tomcat.embed</groupId>
            <artifactId>tomcat-embed-jasper</artifactId>
            <scope>provided</scope>
        </dependency>
       
    </dependencies>

这里注意一点是添加了springboot的对amqp的支持
配置文件application.properties如下,同样生产者和消费者配置是一样的。
#MQ链接相关---开始
spring.rabbitmq.host=127.0.0.1
spring.rabbitmq.port=5672
spring.rabbitmq.username=admin
spring.rabbitmq.password=admin
spring.rabbitmq.virtualHost=/taotao
#MQ链接相关---结束

#MQ ack相关---开始
#客户端确认模式,客户端发送消息到mq,mq会异步返回是否收到消息。
#默认没有确认,无论发送是否成功,客户端都不会知晓
spring.rabbitmq.publisher-confirms=true
#服务端确认模式,消费者消费成功向mq发送删除已经消费消息的信息。
#默认消费者接收到消息,mq就会删除
spring.rabbitmq.listener.acknowledge-mode=MANUAL
#MQ ack相关---结束

还有一处是一样的RabbitMQConfig类,代码如下:
package cn.rabbitmq.example;

import org.springframework.amqp.core.Binding;
import org.springframework.amqp.core.BindingBuilder;
import org.springframework.amqp.core.Queue;
import org.springframework.amqp.core.TopicExchange;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class RabbitConfig {

	// 创建Topic类型交换机
	@Bean
	public TopicExchange defaultExchange() {
		// 第一个参数交换机名称
		// 第二个参数是否交换机久化,true为持久化,创建后重启依然存在
		// 第三个参数是在交换机不在使用的情况下,自动删除
		return new TopicExchange("TopicExchange", true, false, null);
	}

	// 创建队列
	@Bean
	public Queue queue1() {
		// 第一个参数队列名称
		// 第二个参数是否列队持久化,true为持久化,创建后重启依然存在
		return new Queue("queue3", true);
	}

	@Bean
	public Queue queue2() {
		return new Queue("queue4", true);
	}

	// 交换机与队列绑定,并且设置了路由匹配
	@Bean
	public Binding binding1() {
		return BindingBuilder.bind(queue1()).to(defaultExchange()).with("item.*");
	}

	// 交换机与队列绑定,并且设置了路由匹配
	@Bean
	public Binding binding2() {
		return BindingBuilder.bind(queue2()).to(defaultExchange()).with("item.#");
	}

}

具体的解释在代码中,这里定义了一个交换机和两个队列,并且两个队列都绑定到了交换机上,只是路由通配符不同。
接下来就是发送和消费了。但是其中有些问题是无法避免的。1:生产者发送消息到MQ然后返回,MQ接收到了后异步去向生产者确认,但是在MQ确认接收到时,网路出现问题无法向生产者确认这时应该怎么办?生产者不知道MQ是否收到,这时必须重新发送。2:上一个问题可能导致MQ中的消息是重复的,消费者消费到重复的数据,消费者应该去重。

解决问题1:在本地写个缓存,把每次要发送的数据缓存到本地,生产者接收到MQ发回的确认信息后,删除本地缓存数据。开启一个线程去处理缓存中的数据,一定时间内,遍历缓存数据重新发送。
解决问题2:生产者除了发送消息外,再为每个消息生成个id,消费者根据id是否相同决定是否消费此消息。用到一个map,每次拿到id先判断map中是否存在此id,存在表明已经消费过,直接ack让MQ删除消息,map不存在此id,把id存入map后消费。

生产者本地缓存代码:

public class RetrySendCache {
	private MessageSender messageSender;
	private ConcurrentHashMap<String,MessageWithTime> map=new ConcurrentHashMap();
	private String exchangeName;
	private String key;

	public int getSize(){
		return map.size();
	}
	
	public RetrySendCache(){
		startRetry();
	}
	
	private static class MessageWithTime{
		private long time;
		private Object message;
		public MessageWithTime(long time,Object message){
			this.time=time;
			this.message=message;
		}
		public long getTime() {
			return time;
		}
		public Object getMessage() {
			return message;
		}
		
	}
	public void setSenderInfo(MessageSender messageSender, String exchangeName, String key){
		this.messageSender=messageSender;
		this.exchangeName=exchangeName;
		this.key=key;
	}
	
	public void put(String id,Object message){
		map.put(id, new MessageWithTime(System.currentTimeMillis(), message));
	}
	
	public void remove(String id){
		map.remove(id);
	}
	
	public void startRetry(){
		new Thread(new Runnable() {
	           @Override
		   public void run() {
			while(true){
				try {
					Thread.sleep(30*1000);
				} catch (InterruptedException e) {
					e.printStackTrace();
				}
				for(String key  : map.keySet()){
					long now=System.currentTimeMillis();
					MessageWithTime messageWithTime=map.get(key);
					//超过一定时间没有ack直接删除本地缓存的message,重试两次
					if(messageWithTime.getTime()+3*30*1000<=now){
						remove(key);
					}else if(messageWithTime.getTime()+30*1000<=now){
				SendStatusMessage message = messageSender.send(messageWithTime.getMessage(),exchangeName,key);
					if(message.isFlag()){
						remove(key);
						}
					}
				}
			}
		}
	}).start();
		
	}
}

生产者代码

@Component
public class MessageSender  implements ConfirmCallback,SenderInterface{
	@Autowired
	private RabbitTemplate rabbitTemplate;
	
	static RetrySendCache cache=new RetrySendCache();
	
	
	@PostConstruct
    public void init() {
		rabbitTemplate.setConfirmCallback(this);
    }

	public SendStatusMessage send(Object message,String exchangeName,String key){
		try {
			cache.setSenderInfo(this,exchangeName,key);
			String uuid = UUID.randomUUID().toString();
			cache.put(uuid, message);
			Message msg=new Message(message,uuid);
			rabbitTemplate.convertAndSend(exchangeName, key, FastJsonUtil.objectToString(msg), new CorrelationData(uuid));
		} catch (AmqpException e) {
			e.printStackTrace();
			return new SendStatusMessage(false, "");
		}
		
		return new SendStatusMessage(true, "");
		
	}

	@Override
	public void confirm(CorrelationData correlationData, boolean ack, String cause) {
		if (!ack) { 
			System.err.println("发送失败------: " + cause);
		}else{
			cache.remove(correlationData.getId());
		}
		
	}
}

消费者代码

public abstract class BaseConsumer implements Consumer{
	@Override
	@RabbitListener(queues="#{'${rabbitmq.listener.queue.name}'.split(',')}")
	//以下固定有两个参数,也可以只有message一个参数
	public void consume(Message s, Channel channel) {
		byte[] body = s.getBody();
		MessageDetail obj = FastJsonUtil.stringToMessage(body);
		String message = map.get(obj.getUuid());
		if (StringUtils.isBlank(message)) {
			map.put(obj.getUuid(), obj.getUuid());// 内存不够怎么办,定期清理
			try {
				//消费具体逻辑,子类实现
				logic(new String(body));
				//Delivery Tag 用来标识信道中投递的消息,RabbitMQ 推送消息给 Consumer 时,会附带一个 Delivery Tag,
				//以便 Consumer 可以在消息确认时告诉 RabbitMQ 到底是哪条消息被确认了。
				//RabbitMQ 保证在每个信道中,每条消息的 Delivery Tag 从 1 开始递增
				//basicAck 方法的第二个参数 multiple 取值为 false 时,表示通知 RabbitMQ 当前消息被确认;如果为 true,
				//则额外将比第一个参数指定的 delivery tag 小的消息一并确认
				channel.basicAck(s.getMessageProperties().getDeliveryTag(), false);
			} catch (Exception e) {
				e.printStackTrace();
				try {
					//当消费消息出现异常时,我们需要取消确认,这时我们可以使用 Channel 的 basicReject 方法。
					//第一个参数指定 delivery tag,第二个参数说明如何处理这个失败消息。
					//requeue 值为 true 表示该消息重新放回队列头,值为 false 表示放弃这条消息。
					//一般来说,如果是系统无法处理的异常,我们一般是将 requeue 设为 false,例如消息格式错误,再处理多少次也是异常。
					//调用第三方接口超时这类异常 requeue 应该设为 true。
					channel.basicReject(s.getMessageProperties().getDeliveryTag(),false);
				} catch (IOException e1) {
					e1.printStackTrace();
				}
			}
		} else {
			try {
				//这里并不是出现异常,而是重复的消息是不会消费的,直接通知MQ删除
				channel.basicAck(s.getMessageProperties().getDeliveryTag(), false);
			} catch (IOException e) {
				e.printStackTrace();
			}
		}
	}
	
	protected abstract void  logic(String message);

}

大体代码上,后续还做了简单封装,见下一篇
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

jackson陈

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值