RabbitMQ

简介

       RabbitMQ是一个由erlang开发的AMQP(Advanced Message Queue )的开源实现。AMQP 的出现其实也是应了广大人民群众的需求,虽然在同步消息通讯的世界里有很多公开标准(如 COBAR的 IIOP ,或者是 SOAP 等),但是在异步消息处理中却不是这样,只有大企业有一些商业实现(如微软的 MSMQ ,IBM 的 Websphere MQ 等),因此,在 2006 年的 6 月,Cisco 、Redhat、iMatix 等联合制定了 AMQP 的公开标准。

       RabbitMQ是由RabbitMQ Technologies Ltd开发并且提供商业支持的。该公司在2010年4月被SpringSource(VMWare的一个部门)收购。在2013年5月被并入Pivotal。其实VMWare,Pivotal和EMC本质上是一家的。不同的是VMWare是独立上市子公司,而Pivotal是整合了EMC的某些资源,现在并没有上市。

注意:RabbitMQ是采用erlang语言开发的,所以必须有erlang环境才可以运行

环境安装

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

RabbitMQ管理平台

RabbitMQ 管理平台地址 http://127.0.0.1:15672

默认账号:guest/guest  用户可以自己创建新的账号

Virtual Hosts

      像mysql有数据库的概念并且可以指定用户对库和表等操作的权限。那RabbitMQ呢?RabbitMQ也有类似的权限管理。在RabbitMQ中可以虚拟消息服务器VirtualHost,每个VirtualHost相当月一个相对独立的RabbitMQ服务器,每个VirtualHost之间是相互隔离的。exchange、queue、message不能互通。

       不同的团队,不同的项目有自己独立的virtualhost,好处就是进行相互隔离。适合于大型互联网公司进行使用区分不同的业务逻辑。

RabbitMQ消息确认机制

  • AMQP 事务机制
  • Confirm 模式

事务模式

  • txSelect  将当前channel设置为transaction模式
  •  txCommit  提交当前事务
  •  txRollback  事务回滚

应答模式

  • 自动应答

不在乎消费者对这个消息处理是否成功,都会告诉队列删除该消息。如果处理消息失败,会实现自动补偿(重试)。

channel.basicConsume(QUEUE_NAME, true, defaultConsumer);
  • 手动应答
//生产者端代码不变,消费者端代码这部分就是用于开启手动应答模式的。
channel.basicConsume(QUEUE_NAME, false, defaultConsumer);
//注:第二个参数值为false代表关闭RabbitMQ的自动应答机制,改为手动应答。
//在处理完消息时,返回应答状态,true表示为自动应答模式。
channel.basicAck(envelope.getDeliveryTag(), false);

消费处理完业务逻辑后,手动返回ack(通知),告诉给队列服务器是否删除该消息。

RabbitMQ消息重试机制

  • 消费者在消费消息的时候,如果消费者业务逻辑出现程序异常

     此时应该使用消息重试机制。
  • 消费者获取到消息后,调用第三方接口,但接口暂时无法访问

       此时需要重试机制。

  • 消费者获取到消息后,抛出数据转换异常

      此时不需要重试机制,需要重新发布才能解决。可以采用日志记录+定时任务job健康检查+人工进行补偿

五种形式队列

  • 点对点(简单)的队列
  • 工作(公平性)队列模式
  • 发布订阅模式
  • 路由模式Routing
  • 通配符模式Topics

简单队列

 

推送:消费者已经启动,与队列建立起长连接,一旦生产者向队列投递消息,队列会立马推送给消费者。

拉取:生产者先投递消息给队列,这时候消费者再启动的时候,就会向队列获取消息。

1、pom文件引入

<dependencies>
	<dependency>
		<groupId>com.rabbitmq</groupId>
		<artifactId>amqp-client</artifactId>
		<version>3.6.5</version>
	</dependency>
</dependencies>

2、封装Connection

public class MQConnectionUtils {
	// 创建新的MQ连接
	public static Connection newConnection() throws IOException, TimeoutException {
		// 1.创建连接工厂
		ConnectionFactory factory = new ConnectionFactory();
		// 2.设置连接地址
		factory.setHost("127.0.0.1");
		// 3.设置用户名称
		factory.setUsername("admin");
		// 4.设置用户密码
		factory.setPassword("admin");
		// 5.设置amqp协议端口号
		factory.setPort(5672);
		// 6.设置VirtualHost地址
		factory.setVirtualHost("/admin_host");
		Connection connection = factory.newConnection();
		return connection;
	}
}

3、生产者

// 简单队列生产者
public class Producer {

	// 队列名称
	private static final String QUEUE_NAME = "my_queue";

	public static void main(String[] args) throws IOException, TimeoutException {

		// 1.创建一个新的连接
		Connection connection = MQConnectionUtils.newConnection();
		// 2.创建通道
		Channel channel = connection.createChannel();
		// 3.创建一个队列
		channel.queueDeclare(QUEUE_NAME, false, false, false, null);
		channel.basicQos(1);
		for (int i = 1; i <= 50; i++) {
			// 4.创建msg
			String msg = "my_msg_" + i;
			// 5.生产者发送消息者
			channel.basicPublish("", QUEUE_NAME, null, msg.getBytes());
		}
		// 关闭通道和连接
		channel.close();
		connection.close();
	}
}

4、消费者

public class Consumer1 {
	// 队列名称
	private static final String QUEUE_NAME = "my_queue";

	public static void main(String[] args) throws IOException, TimeoutException {
		// 1.创建一个新的连接
		Connection connection = MQConnectionUtils.newConnection();
		// 2.创建通道
		final Channel channel = connection.createChannel();
		// 3.消费者关联队列
		channel.queueDeclare(QUEUE_NAME, false, false, false, null);
		DefaultConsumer defaultConsumer = new DefaultConsumer(channel) {
			// 监听获取消息
			@Override
			public void handleDelivery(String consumerTag, Envelope envelope, BasicProperties properties, byte[] body)
					throws IOException {
				String msg = new String(body, "UTF-8");
				System.out.println("消费者获取生产者消息:" + msg);
			}

		};
		//4.设置应答模式 如果为true情况下 表示为自动应答模式 false 表示为手动应答
		channel.basicConsume(QUEUE_NAME, false, defaultConsumer);
	}
}

消费者集群的情况下,默认采用均摊消费。

假如生产者生产10个消息,那么消费1和消费者2个字消费5个。

公平队列(能者多劳)

       目前消息转发机制是平均分配,这样就会出现俩个消费者,奇数的任务很耗时,偶数的任何工作量很小,造成的原因就是近当消息到达队列进行转发消息。并不在乎有多少任务消费者并未传递一个应答给RabbitMQ。仅仅盲目转发所有的奇数给一个消费者,偶数给另一个消费者。

公平队列的原理

       队列服务器向消费者发送消息的时候,消费者采用ACK手动应答模式,队列服务器必须要收到消费者发送ack结果,才会继续发送下一个消息。

生产者

public class Producer {
	private static final String QUEUE_NAME = "test_queue";

	public static void main(String[] args) throws IOException, TimeoutException {
		// 1.获取连接
		Connection newConnection = MQConnectionUtils.newConnection();
		// 2.创建通道
		Channel channel = newConnection.createChannel();
		// 3.创建队列声明
		channel.queueDeclare(QUEUE_NAME, false, false, false, null);
		channel.basicQos(1);// 保证一次只分发一次 限制发送给同一个消费者 不得超过一条消息
		for (int i = 1; i <= 50; i++) {
			String msg = "test_yushengjun" + i;
			System.out.println("生产者发送消息:" + msg);
			// 4.发送消息
			channel.basicPublish("", QUEUE_NAME, null, msg.getBytes());
		}
		channel.close();
		newConnection.close();
	}
}

消费者01

public class Customer1 {
	private static final String QUEUE_NAME = "test_queue";

	public static void main(String[] args) throws IOException, TimeoutException {
		System.out.println("001");
		// 1.获取连接
		Connection newConnection = MQConnectionUtils.newConnection();
		// 2.获取通道
		final Channel channel = newConnection.createChannel();
		channel.queueDeclare(QUEUE_NAME, false, false, false, null);
		channel.basicQos(1);// 保证一次只分发一次 限制发送给同一个消费者 不得超过一条消息
		DefaultConsumer defaultConsumer = new DefaultConsumer(channel) {
			@Override
			public void handleDelivery(String consumerTag, Envelope envelope, BasicProperties properties, byte[] body)
					throws IOException {
				String msgString = new String(body, "UTF-8");
				System.out.println("消费者获取消息:" + msgString);
				try {
					Thread.sleep(1000);
				} catch (Exception e) {

				} finally {
					// 手动回执消息
					channel.basicAck(envelope.getDeliveryTag(), false);
				}
			}
		};
		// 3.监听队列
		channel.basicConsume(QUEUE_NAME, false, defaultConsumer);
	}
}

消费者02

public class Customer2 {
	private static final String QUEUE_NAME = "test_queue";

	public static void main(String[] args) throws IOException, TimeoutException {
		System.out.println("002");
		// 1.获取连接
		Connection newConnection = MQConnectionUtils.newConnection();
		// 2.获取通道
		final Channel channel = newConnection.createChannel();
		channel.queueDeclare(QUEUE_NAME, false, false, false, null);
		channel.basicQos(1);// 保证一次只分发一次 限制发送给同一个消费者 不得超过一条消息
		DefaultConsumer defaultConsumer = new DefaultConsumer(channel) {
			@Override
			public void handleDelivery(String consumerTag, Envelope envelope, BasicProperties properties, byte[] body)
					throws IOException {
				String msgString = new String(body, "UTF-8");
				System.out.println("消费者获取消息:" + msgString);
				try {
					Thread.sleep(500);
				} catch (Exception e) {

				} finally {
					// 手动回执消息
					channel.basicAck(envelope.getDeliveryTag(), false);
				}
			}
		};
		// 3.监听队列
		channel.basicConsume(QUEUE_NAME, false, defaultConsumer);

	}

}

RabbitMQ交换机

传统简单队列工作原理:生产者生产消息直接投递给队列服务器,然后队列服务器以推送或者拉取的方式给到消费者进行消费。

       RabbitMQ高级队列则不同,生产者先投递消息给交换机,交换机根据路由策略转发不同的队列服务器中存放,队列服务器在以推送或者拉取的方式给到消费者进行消费。

交换机的作用

      根据具体的路由策略分发到不同的队列中(有点类似于Nginx)。

注意:交换机没有存储消息功能,如果消息发送到没有绑定消费队列的交换机,消息则丢失。

交换机类型

  • Direct exchange(直连交换机)

     ​​​​​​​  根据消息携带的路由键(routing key)将消息投递给对应队列。

  • Fanout exchange(扇型交换机)

​​​​​​​       将消息路由给绑定到它身上的所有队列。

  • Topic exchange(主题交换机)

​​​​​​​       队列通过路由键绑定到交换机上,然后,交换机根据消息里的路由值,将消息路由给一个或多个绑定队列。

  • Headers exchange(头交换机)

       ​​​​​​​类似主题交换机,但是头交换机使用多个消息属性来代替路由键建立路由规则。通过判断消息头的值能否与指定的绑定相匹配来确立路由规则。

发布订阅模式

      一个生产者发送消息,多个消费者获取消息(同样的消息),包括一个生产者,一个交换机,多个队列,多个消费者

大致思路:

  • 一个生产者,多个消费者
  • 每一个消费者都有自己的一个队列
  • 生产者没有直接发消息到队列中,而是发送到交换机
  • 每个消费者的队列都绑定到交换机上
  • 消息通过交换机到达每个消费者的队列

​​​​​​​场景以用户发短信发邮件为例,交换机类型采取Fanout Exchange(扇型交换机)。

生产者

1、pom文件引入

    <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>
	</dependencies>

2、配置文件

spring:
  rabbitmq:
  ####连接地址
    host: 127.0.0.1
   ####端口号   
    port: 5672
   ####账号 
    username: guest
   ####密码  
    password: guest
   ### 地址
    virtual-host: /admin_host

3、交换机绑定队列

@Component
public class FanoutConfig {

	// 邮件队列
	private String FANOUT_EMAIL_QUEUE = "fanout_eamil_queue";

	// 短信队列
	private String FANOUT_SMS_QUEUE = "fanout_sms_queue";
	// 交换机名称
	private String EXCHANGE_NAME = "fanoutExchange";

	// 1.定义队列邮件
	@Bean
	public Queue fanOutEamilQueue() {
		return new Queue(FANOUT_EMAIL_QUEUE);
	}

	@Bean
	public Queue fanOutSmsQueue() {
		return new Queue(FANOUT_SMS_QUEUE);
	}

	// 2.定义交换机
	@Bean
	FanoutExchange fanoutExchange() {
		return new FanoutExchange(EXCHANGE_NAME);
	}

	// 3.队列与交换机绑定邮件队列
	@Bean
	Binding bindingExchangeEamil(Queue fanOutEamilQueue, FanoutExchange fanoutExchange) {
		return BindingBuilder.bind(fanOutEamilQueue).to(fanoutExchange);
	}

	// 4.队列与交换机绑定短信队列
	@Bean
	Binding bindingExchangeSms(Queue fanOutSmsQueue, FanoutExchange fanoutExchange) {
		return BindingBuilder.bind(fanOutSmsQueue).to(fanoutExchange);
	}
}

4、生产者投递消息

@Component
public class FanoutProducer {
	@Autowired
	private AmqpTemplate amqpTemplate;

	public void send(String queueName) {
		String msg = "my_fanout_msg:" + new Date();
		System.out.println(msg + ":" + msg);
		amqpTemplate.convertAndSend(queueName, msg);
	}
}

5、控制层代码

@RestController
public class ProducerController {
	@Autowired
	private FanoutProducer fanoutProducer;

	@RequestMapping("/sendFanout")
	public String sendFanout(String queueName) {
		fanoutProducer.send(queueName);
		return "success";
	}
}

消费者

1、pom文件引入

   <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>
	</dependencies>

2、配置文件

spring:
  rabbitmq:
  ####连接地址
    host: 127.0.0.1
   ####端口号   
    port: 5672
   ####账号 
    username: guest
   ####密码  
    password: guest
   ### 地址
    virtual-host: /admin_host

3、邮件消费者

@Component
@RabbitListener(queues = "fanout_eamil_queue")
public class FanoutEamilConsumer {
	@RabbitHandler
	public void process(String msg) throws Exception {
		System.out.println("邮件消费者获取生产者消息msg:" + msg);
	}
}

4、短信消费者

@Component
@RabbitListener(queues = "fanout_sms_queue")
public class FanoutSmsConsumer {
	@RabbitHandler
	public void process(String msg) {
		System.out.println("短信消费者获取生产者消息msg:" + msg);
	}
}

路由模式

       生产者发送消息到交换机并指定一个路由key,消费者队列绑定到交换机时要制定路由key(key匹配就能接受消息,key不匹配就不能接受消息)。

      我们可以把路由key设置为insert ,那么消费者队列key指定包含insert才可以接收消息,消费者队列key定义为update或者delete就不能接收消息。很好的控制了更新,插入和删除的操作。

      交换机采用direct模式。

生产者

与发布订阅唯一的区别就是交换机绑定队列和启动项的环节。

@Component
public class DirectConfig {

	// 邮件队列
	private String DIRECT_EMAIL_QUEUE = "direct_eamil_queue";

	// 短信队列
	private String DIRECT_SMS_QUEUE = "direct_sms_queue";
	// 队列名称
	private String EXCHANGE_NAME = "directExchange";

	// 1.定义队列邮件
	@Bean
	public Queue directEamilQueue() {
		return new Queue(DIRECT_EMAIL_QUEUE);
	}

	@Bean
	public Queue directSmsQueue() {
		return new Queue(DIRECT_SMS_QUEUE);
	}

	// 2.定义交换机
	@Bean
	DirectExchange directExchange() {
		return new DirectExchange(EXCHANGE_NAME);
	}

	// 3.队列与交换机绑定邮件队列
	@Bean
	Binding bindingExchangeEamil(Queue directEamilQueue, DirectExchange directExchange) {
		return BindingBuilder.bind(directEamilQueue).to(directExchange).with("myKey");
	}

	// 4.队列与交换机绑定短信队列
	@Bean
	Binding bindingExchangeSms(Queue directSmsQueue, DirectExchange directExchange) {
		return BindingBuilder.bind(directSmsQueue).to(directExchange).with("myKey");
	}
}

启动项

@Component
public class FanoutProducer {
	@Autowired
	private AmqpTemplate amqpTemplate;

	public void send(String queueName) {
		String msg = "my_direct_msg:" + new Date();
		String routingKey = "myKey";
		System.out.println(msg + ":" + msg);
		amqpTemplate.convertAndSend(queueName,routingKey,msg);
	}
}

通配符模式

       此模式实在路由key模式的基础上,使用了通配符来管理消费者接收消息。生产者P发送消息到交换机X,type=topic,交换机根据绑定队列的routing key的值进行通配符匹配。​​​​​​​

  • 符号#匹配一个或者多个词lazy.# 可以匹配lazy.irs或者lazy.irs.cor
  • 符号*:只能匹配一个词lazy.* 可以匹配lazy.irs或者lazy.cor

跟路由模式很类似,就不过多介绍了。

死信队列

       在定义业务队列的时候,可以考虑指定一个死信交换机,并绑定一个死信队列,当消息变成死信时,该消息就会被发送到该死信队列上,这样就方便我们查看消息失败的原因了。

RabbitMQ解决消息幂等性问题

产生原因

       网络延迟传输中消费出现异常或者是消费延迟消费,会造成MQ进行重试补偿在重试过程中,可能会造成重复消费

解决办法

      ①使用全局MessageID判断消费方是否使用同一个,解决幂等性

      ②或者使用业务逻辑保证唯一(比如订单号码)。

RabbitMQ解决分布式事务问题

情景如下:

       以目前流行点外卖的案例,用户下单后,调用订单服务,让后订单服务调用派单系统通知送外卖人员送单,这时候订单系统与派单系统采用MQ异步通讯。

        RabbitMQ解决分布式事务原理: 采用最终一致性原理。

需要保证以下三要素:

1、确认生产者一定要将数据投递到MQ服务器中(采用MQ消息确认机制)。

2MQ消费者消息能够正确消费消息,采用手动ACK模式,使用补偿机制(注意重试幂等性问题)。

3、如何保证第一个事务先执行,采用补单机制,在创建一个补单消费者进行监听,如果订单没有创建成功,进行补单。

需要注意如下几种情况:

1.如果生产者投递消息到MQ服务成功,但是消费者消费失败了,生产者不需要回滚事务。

    解决方法:消费者采用ACK应答方式,采用MQ补偿重试机制。补偿的过程中,注意幂等性问题。

2.如何确认生产者一定要将数据投递到MQ服务器中?

    采用confirm机制(确认应答机制)

3.如果生产者发送消息到MQ服务器端失败

    解决办法:使用生产者重试机制,进行发消息

4.如何保证一个事务先执行,生产者投递消息到MQ服务器端成功,消费者消费消息成功,但是订单事务回滚了。

    解决办法:通过补单机制​​​​​​​

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值