队列我们都知道,是一种数据结构,先进先出。那么消息队列就是指,队列中的内容,是一种消息,这个消息是一个抽象的概念,可以指很多东西,比如对象等。
1.解耦:用户下订单,库存扣减。但是如果库存不能正常使用,就会影响用户下订单,所以就可以把用户下订单这个操作放入消息队列中,这样即使库存出问题了,也不会影响用户下订单。
2.消峰:当高并发来临的时候,把多余的高并发放入消息队列中。
3.日志:当系统产生异常的时候,可以把异常放入消息队列。
它是消息队列的一种实现技术,使用它就可以实现消息队列的操作。
1.链接对象
链接分为长连接和短连接,其中长连接就像人类社会的大桥,不会轻易销毁,短连接就是频繁创建和销毁的,进程想要使用rabbitMQ的消息队列,就需要创建连接去连接rabbitMQ。
2.交换机(exchange)
进程不能直接操作消息队列的,它只能先访问交换机,让交换机去操作消息队列。
3.队列(queue)
队列是rabbitmq里接受存储消息对象的组件。必须绑定一个交换机,当交换机接受到客户端发送的消息时,会根据路由逻辑判断当前消息发送给哪个/哪些队列 消息封装成对象,最终发送到队列。
概念:所有可以链接rabbitmq的软件,代码,插件都可以是rabbitmq的客户端(根据客户端不同的功能区分角色)
1.生产者(productor):
A把信息发送到交换机,交换机把信息存入消息队列,那么A就是生产者。
2.消费者(consumer):
消费者监听着消息队列,从消息队列中获取存储的消息进行操作。
3.无角色:
如果客户端连接上rabbitmq,没有发送消息,不需要接收消费消息,就是无角色的客户端。
要使用java客户端,就需要注入依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
1.简单模式:
强调的是消费端结构,每个队列,只被一个消费端监听.
channel是连接rabbitMQ和客户端的桥梁,所以需要优先实例化channel,消息队列也需要通过channel来创建,每一个消息队列必须要绑定一个交换机。生产者在发送消息的时候需要通过channel去找到交换机,根据要求的路由key,去寻找队列,在简单模式中,消息queue的路由key就是queue的名字。
应用场景:手机短信,邮件发送
/**
* 准备连接逻辑 java代码先连接rabbitmq
* 才能发送消息,监听消费消息,声明创建组件
*/
public class SimpleMode {
//创建连接对象
private Channel channel;
@Before
public void initChannel() throws IOException, TimeoutException {
//提供一些连接参数 host port username password
//创建一个connection连接工厂
ConnectionFactory factory=new ConnectionFactory();
//提供属性
factory.setUsername("guest");
factory.setPassword("guest");
factory.setHost("10.9.151.60");
factory.setPort(5672);
//拿到一个连接对象
Connection connection = factory.newConnection();
//从连接对象获取channel赋值给私有属性
channel=connection.createChannel();
}
//只有通过channel能操作rabbitmq,这段代码就是客户端代码
//实现消息的发送
@Test
public void productor() throws Exception {
//准备一个发送的消息
String msg="hello world,rabbitmq";
//声明一个队列 queue01
channel.queueDeclare("queue01",false,false,false,null);
/*
String queue:声明的创建的这个队列的名字 如果已经存在,声明无效
boolean durable:队列是否持久化 持久化队列会在重启rm,宕机回复rm之后一并个回复,没有持久化的就没了
bolean exclusive: 是否专属于当前连接.true表示专属,除了创建声明这个队列的连接可以操作他以外,别人不能用
boolean autoDelete:最后一个channel连接完queue 是否自动删除
Map args:表示队列声明时的各种属性
例如:消息存活时间
最大存储的消息个数
...
*/
//默认绑定,(AMQP default) 特性:任意当前rabbitmq声明的队列
//都会使用队列名称作为路由key绑定这个交换机
//将消息发送给交换机
channel.basicPublish("","queue01",null,msg.getBytes());
/*
String exchange:交换机名称 ""表示发送给(AMQP default)
String routingKey:当前消息发送时携带的路由key
BasicProperties props: 表示封装消息对象message时的各种属性
属性的添加,可以丰富消息的信息,使得处理消费逻辑变得灵活
byte[] body:消息体的二进制数据,java所有对象都可以成为消息体.
由于这种消息数据占用网络带宽传输,存储在queue一段时间,
消息的封装 遵循精简准确
*/
}
//消费端逻辑
@Test
public void consumer01() throws Exception {
//可以通过channel信道连接rabbitmq,监听任何一个
//或者多个队列,连接不断,可以通过方法获取队列中生成的消息
//生成一个客户端消费对象
QueueingConsumer consumer=new QueueingConsumer(channel);
//将consumer绑定监听队列
channel.basicConsume("queue01",consumer);
//可以通过delivery获取消费者监听的队列里的信息
QueueingConsumer.Delivery delivery = consumer.nextDelivery();
//delivery看成是一个包含消息的对象 body properties header
byte[] body = delivery.getBody();//content_type content_encoding
AMQP.BasicProperties properties = delivery.getProperties();
/*properties.getAppId();
properties.getUserId();*/
Integer priority = properties.getPriority();
String contentEncoding = properties.getContentEncoding();
System.out.println("属性encoding:"+contentEncoding);
/*properties.getContentType();*/
String msg=new String(body,"utf-8");
System.out.println(delivery.getEnvelope().getRoutingKey());
System.out.println(msg);
}
}
2.争抢模式:
强调的消费端结构,队列可以由多个消费端同时监听形成争抢
原理和上面的简单模式差不多,唯一的区别就是代码最后加了一个死循环,表示启动了多个消费者去监听同一个端口。
应用场景:抢红包
/**
* 准备连接逻辑 java代码能先连接rabbitmq
* 才能发送消息,监听消费消息,声明创建组件
*/
public class SimpleMode {
//创建连接对象
private Channel channel;
@Before
public void initChannel() throws IOException, TimeoutException {
//提供一些连接参数 host port username password
//创建一个connection连接工厂
ConnectionFactory factory=new ConnectionFactory();
//提供属性
factory.setUsername("guest");
factory.setPassword("guest");
factory.setHost("10.9.151.60");
factory.setPort(5672);
//拿到一个连接对象
Connection connection = factory.newConnection();
//从连接对象获取channel赋值给私有属性
channel=connection.createChannel();
}
//只要通过channel能操作rabbitmq,这段代码就是客户端代码
//实现消息的发送
@Test
public void productor() throws Exception {
//准备一个发送的消息
String msg="hello world,rabbitmq";
//声明一个队列 queue01
channel.queueDeclare("queue01",false,false,false,null);
/*
String queue:声明的创建的这个队列的名字 如果已经存在,声明无效
boolean durable:队列是否持久化 持久化队列会在重启rm,宕机回复rm之后一并个回复,没有持久化的就没了
bolean exclusive: 是否专属于当前连接.true表示专属,除了创建声明这个队列的连接可以操作他以外,别人不能用
boolean autoDelete:最后一个channel连接完queue 是否自动删除
Map args:表示队列声明时的各种属性
例如:消息存活时间
最大存储的消息个数
...
*/
//默认绑定,(AMQP default) 特性:任意当前rabbitmq声明的队列
//都会使用队列名称作为路由key绑定这个交换机
//将消息发送给交换机
channel.basicPublish("","queue01",null,msg.getBytes());
/*
String exchange:交换机名称 ""表示发送给(AMQP default)
String routingKey:当前消息发送时携带的路由key
BasicProperties props: 表示封装消息对象message时的各种属性
属性的添加,可以丰富消息的信息,使得处理消费逻辑变得灵活
byte[] body:消息体的二进制数据,java所有对象都可以成为消息体.
由于这种消息数据占用网络带宽传输,存储在queue一段时间,
消息的封装 遵循精简准确
*/
}
//消费端逻辑
@Test
public void consumer01() throws Exception {
//可以通过channel信道连接rabbitmq,监听任何一个
//或者多个队列,连接不断,可以通过方法获取队列中生成的消息
//生成一个客户端消费对象
QueueingConsumer consumer=new QueueingConsumer(channel);
//将consumer绑定监听队列
//channel.basicConsume("queue01",consumer);
channel.basicConsume("queue01",false,consumer);
//可以通过链接抵用消息消费
QueueingConsumer.Delivery delivery = consumer.nextDelivery();
channel.basicAck(delivery.getEnvelope().getDeliveryTag(),false);
//delivery看成是一个包含消息的对象 body properties header
byte[] body = delivery.getBody();//content_type content_encoding
AMQP.BasicProperties properties = delivery.getProperties();
/*properties.getAppId();
properties.getUserId();*/
Integer priority = properties.getPriority();
String contentEncoding = properties.getContentEncoding();
System.out.println("属性encoding:"+contentEncoding);
/*properties.getContentType();*/
String msg=new String(body,"utf-8");
System.out.println(delivery.getEnvelope().getRoutingKey());
System.out.println(msg);
//如果不自动确认,需要手动返回,告诉rabbitmq确认哪条消息
channel.basicAck(delivery.getEnvelope().getDeliveryTag(),false);
while(true);
}
}
3.路由模式:
强调交换机,将会按照消息的路由key转发消息。前面两个都是注重消费者端的,所以路由key都是queue的名字,这里就可以自定义。创建好channel,声明好queue,把queue绑定到交换机上并且声明好queue的路由key,生产者发送消息,指定交换机指定路由key,就可以发送到对应的queue上了。
需要注意的是这里的交换机类型是direct。
/**
* 完成路由模式测试
* 上海,北京作为2个队列路由key
*/
public class RouteMode {
private Channel channel;
@Before
public void initChannel() throws IOException, TimeoutException {
ConnectionFactory factory=new ConnectionFactory();
factory.setUsername("guest");
factory.setPassword("guest");
factory.setHost("10.9.151.60");
factory.setPort(5672);
Connection connection = factory.newConnection();
channel=connection.createChannel();
}
//准备一批静态常量
private static final String type="direct";//自定义交换机类型
private static final String exName=type+"EX";//自定义交换机名称
//准备2个队列
private static final String q1=type+"q01";
private static final String q2=type+"q02";
//声明所有组件 一个交换机,一个队列,和绑定关系
@Test
public void bind() throws Exception {
//声明队列
channel.queueDeclare(q1,false,false,false,null);
channel.queueDeclare(q2,false,false,false,null);
//声明交换机
channel.exchangeDeclare(exName,type);
//声明绑定关系
//q1绑定exName 使用路由key 上海
channel.queueBind(q1,exName,"上海");
channel.queueBind(q1,exName,"天津");
//q2绑定exName 使用路由key 北京
channel.queueBind(q2,exName,"北京");
}
//发送消息
@Test
public void producter() throws IOException {
//将消息携带一个路由key发送到exName
channel.basicPublish(exName,"上海",null,"上海发送".getBytes());
}
}
4.发布订阅模式:
强调的是一个交换机,会将消息发送给后端所有的队列queue群发
应用场景:广告推送,邮寄群发
使用方法和上面基本一样,唯一的不同就是这里的交换机类型是fanout。
public class FanoutMode {
private Channel channel;
@Before
public void initChannel() throws IOException, TimeoutException {
ConnectionFactory factory=new ConnectionFactory();
factory.setUsername("guest");
factory.setPassword("guest");
factory.setHost("10.9.151.60");
factory.setPort(5672);
Connection connection = factory.newConnection();
channel=connection.createChannel();
}
//准备一批静态常量
private static final String type="fanout";//自定义交换机类型
private static final String exName=type+"EX";//自定义交换机名称
//准备2个队列
private static final String q1=type+"q01";
private static final String q2=type+"q02";
//声明所有组件 一个交换机,一个队列,和绑定关系
@Test
public void bind() throws Exception {
//声明队列
channel.queueDeclare(q1,false,false,false,null);
channel.queueDeclare(q2,false,false,false,null);
//声明交换机
channel.exchangeDeclare(exName,type);
//声明绑定关系
//q1绑定exName 使用路由key 上海
channel.queueBind(q1,exName,"上海");
channel.queueBind(q1,exName,"天津");
//q2绑定exName 使用路由key 北京
channel.queueBind(q2,exName,"北京");
}
//发送消息
@Test
public void producter() throws IOException {
//将消息携带一个路由key发送到exName
channel.basicPublish(exName,"上海",null,"上海发送".getBytes());
}
}
5.主题模式:
其实我更喜欢叫他范围模式,这里queue绑定交换机使用的不再是详细的路由key,而是一个范围。范围内容:
#:任意多级的任意长度字符串
*:表示一级的任意字符串
交换机类型是topic,queue绑定交换机的时候,要使用范围服号表明,其他的和上面一样。
public class TopicMode {
private Channel channel;
@Before
public void initChannel() throws IOException, TimeoutException {
ConnectionFactory factory=new ConnectionFactory();
factory.setUsername("guest");
factory.setPassword("guest");
factory.setHost("10.9.151.60");
factory.setPort(5672);
Connection connection = factory.newConnection();
channel=connection.createChannel();
}
//准备一批静态常量
private static final String type="topic";//自定义交换机类型
private static final String exName=type+"EX";//自定义交换机名称
//准备2个队列
private static final String q1=type+"q01";
private static final String q2=type+"q02";
//声明所有组件 一个交换机,一个队列,和绑定关系
@Test
public void bind() throws Exception {
//声明队列
channel.queueDeclare(q1,false,false,false,null);
channel.queueDeclare(q2,false,false,false,null);
//声明交换机
channel.exchangeDeclare(exName,type);
//声明绑定关系
//q1绑定exName 使用路由key
channel.queueBind(q1,exName,"中国.#");
channel.queueBind(q1,exName,"中国.天津.*");
//q2绑定exName 使用路由key 北京
channel.queueBind(q2,exName,"*.北京.*");
}
//发送消息
@Test
public void producter() throws IOException {
//将消息携带一个路由key发送到exName
channel.basicPublish(exName,"中国.新疆.乌鲁木齐",null,"hahsadlfjasdlfj".getBytes());
}
}
convertAndSend: 输出时没有顺序,不需要等待,直接运行。rabbitMQ不会帮你保存消息。
convertSendAndReceive: 使用此方法,只有确定消费者接收到消息,才会发送下一条信息,每条消息之间会有间隔时间。只有收到了确认信息,才会删除queue中的信息
因为网络传输的不稳定性,当生产者在向MQ发送消息的过程中,MQ没有接收到消息
解决办法:使用AMQP协议提供的一个事务机制,在生产者发送消息之前,通过channel.txSelect开启一个事务,接着发送消息, 如果消息投递server失败,进行事务回滚channel.txRollback,然后重新发送, 如果server收到消息,就提交事务channel.txCommit
但是,很少有人这么干,因为这是同步操作,一条消息发送之后会使发送端阻塞,以等待RabbitMQ-Server的回应,之后才能继续发送下一条消息,生产者生产消息的吞吐量和性能都会大大降低
生产者开启confirm模式之后,你每次写的消息都会分配一个唯一的id,然后如果写入了rabbitmq中,rabbitmq会给你回传一个ack消息,告诉这个消息成功还是失败了。如果rabbitmq没能处理这个消息,会回调你一个nack接口,告诉你这个消息接收失败,可以重试。
发送消息可能会网络失败,失败后要有重试机制,可记录到数据库,采用定期扫描重发的方式。每一条消息都做好日志记录,给数据库保存每一个消息的记录,定期扫描数据库将失败的消息重新发送。
解决办法:
生产者开启confirm模式之后,rabbitmq会给你回传一个ack消息,告诉这个消息成功还是失败了。如果rabbitmq没能处理这个消息,会回调你一个nack接口,告诉你这个消息接收失败,此时修改数据库中的mq状态,进行消息定期重试。配合上面的容错方法
1.注入依赖:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
2.在application配置文件中配置rabbitMQ相关属性:
spring.rabbitmq.host=10.9.151.60
spring.rabbitmq.port=5672
spring.rabbitmq.username=guest
spring.rabbitmq.password=guest
3.自定义声明组件(queue,exchange,绑定关系)
启动类中去做这个声明,这样就可以在系统启动的时候完成初始化声明。
@SpringBootApplication
@EnableEurekaClient//开启客户端的功能注解
public class StarterEurekaClient {
public static void main(String[] args) {
SpringApplication.run(StarterEurekaClient.class,args);
}
//启动类当成声明组件配置类
@Bean//声明一个队列
public Queue queue01(){
//对象包装的属性,会在第一次程序链接rabbitmq时
//调用queueDeclear
return new Queue("testqueue",false,false,false,null);
}
@Bean//声明交换机
public DirectExchange ex01(){
return new DirectExchange("testEX");
}
@Bean//绑定关系
public Binding bind01(){
return BindingBuilder.bind(queue01()).to(ex01()).with("test");
}
}
4.声明生产者:
这里使用的对象是RabbitTemplate的对象,通过该对象就可以把消息发送给指定的exchange,在根据指定的路由key找到对应的queue。其中RabbitTemplate的对象还调用了convertAndSend,保证了只有在消费者接收到了消息,才会进行下一条消息的发送。
@RestController
public class HelloController {
//访问项目,返回工程启动端口
@Value("${server.port}")
private String port;
@RequestMapping("client/hello")
public String sayHi(String name){
return "Hello "+name+". I am from "+port;
//name=王翠花,hello 王翠花. I am from port;
}
@Autowired
private RabbitTemplate template;
//发送一个请求 send String msg发送到已有的队列中
@RequestMapping("send")
public String send(String msg){
//发送msg到队列
MessageProperties properties=new MessageProperties();
properties.setPriority(100);
Message message=new Message(msg.getBytes(),properties);
//只是发送消息
template.send("testEX","test",message);
//channel.basicPublish(exName,routingkey,props,body);
//send方法,客户端关心自定义封装消息过程
//rabbitMQ的消息确认机制
template.convertAndSend("testEX",
"test",msg);
return "succes";
}
}
5.声明消费者:
消费者通过 @RabbitListener(queues=“testqueue”)来监听名为testqueue的queue,这个queues是可以指定多个消息队列,对他们一起监听。
@Component
public class ConsumerConp {
@RabbitListener(queues="testqueue")
public void consume(String msg){
//当前方法就是消费逻辑调用的消费逻辑,底层链接
//拿到消息之后会调用这个吧消息传递给方法参数
System.out.println("消费端接收消息:"+msg);
}
}