消息队列之RabbitMQ:
1.1.1. 什么是 MQ
MQ(message queue),从字面意思上看,本质是个队列,FIFO 先入先出,只不过队列中存放的内容是 message 而已,还是一种跨进程的通信机制,用于上下游传递消息。在互联网架构中,MQ 是一种非常常 见的上下游“逻辑解耦+物理解耦”的消息通信服务。使用了 MQ 之后,消息发送上游只需要依赖 MQ,不 用依赖其他服务。
1.1.2. 为什么要用 MQ
1、流量消峰:缺点访问速度下降,优点不会导致服务器宕机
举个例子,如果订单系统最多能处理一万次订单,这个处理能力应付正常时段的下单时绰绰有余,正
常时段我们下单一秒后就能返回结果。但是在高峰期,如果有两万次下单操作系统是处理不了的,只能限
制订单超过一万后不允许用户下单。
使用消息队列做缓冲,我们可以取消这个限制,把一秒内下的订单分
散成一段时间来处理,这时有些用户可能在下单十几秒后才能收到下单成功的操作,但是比不能下单的体
验要好。
2、应用解耦:
以电商应用为例,应用中有订单系统、库存系统、物流系统、支付系统。用户创建订单后,如果耦合
调用库存系统、物流系统、支付系统,任何一个子系统出了故障,都会造成下单操作异常。当转变成基于
消息队列的方式后,系统间调用的问题会减少很多,比如物流系统因为发生故障,需要几分钟来修复。在
这几分钟的时间里,物流系统要处理的内存被缓存在消息队列中,用户的下单操作可以正常完成。当物流
系统恢复后,继续处理订单信息即可,中单用户感受不到物流系统的故障,提升系统的可用性。
3、异步处理
有些服务间调用是异步的,例如 A 调用 B,B 需要花费很长时间执行,但是 A 需要知道 B 什么时候可
以执行完,以前一般有两种方式,A 过一段时间去调用 B 的查询 api 查询。或者 A 提供一个 callback api,
B 执行完之后调用 api 通知 A 服务。这两种方式都不是很优雅,使用消息总线,可以很方便解决这个问题,
A 调用 B 服务后,只需要监听 B 处理完成的消息,当 B 处理完成后,会发送一条消息给 MQ,MQ 会将此
消息转发给 A 服务。这样 A 服务既不用循环调用 B 的查询 api,也不用提供 callback api。同样 B 服务也不
用做这些操作。A 服务还能及时的得到异步处理成功的消息
RabbitMQ 的4大核心概念 :
RabbitMQ 是一个消息中间件:它接受并转发消息。你可以把它当做一个快递站点,当你要发送一个包 裹时,你把你的包裹放到快递站,快递员最终会把你的快递送到收件人那里,按照这种逻辑 RabbitMQ 是 一个快递站,一个快递员帮你传递快件。RabbitMQ 与快递站的主要区别在于,它不处理快件而是接收, 存储和转发消息数据。
1、生产者
产生数据发送消息的程序是生产者
2、交换机
交换机是 RabbitMQ 非常重要的一个部件,一方面它接收来自生产者的消息,另一方面它将消息
推送到队列中。交换机必须确切知道如何处理它接收到的消息,是将这些消息推送到特定队列还是推
送到多个队列,亦或者是把消息丢弃,这个得有交换机类型决定
3、队列
队列是 RabbitMQ 内部使用的一种数据结构,尽管消息流经 RabbitMQ 和应用程序,但它们只能存
储在队列中。队列仅受主机的内存和磁盘限制的约束,本质上是一个大的消息缓冲区。许多生产者可
以将消息发送到一个队列,许多消费者可以尝试从一个队列接收数据。这就是我们使用队列的方式
4、消费者
消费与接收具有相似的含义。消费者大多时候是一个等待接收消息的程序。请注意生产者,消费
者和消息中间件很多时候并不在同一机器上。同一个应用程序既可以是生产者又是可以是消费者
MQ中的异步调用的优缺点:
Linux中安装的流程查看【可以私信w】:
#1.官网地址
https://www.rabbitmq.com/download.html
#2.文件上传:上传RabbitMQ的安装包和工具
/usr/local/src/sofeware/tars
#3.安装文件(分别按照以下顺序安装)
rpm -ivh erlang-21.3-1.el7.x86_64.rpm
yum install socat -y
rpm -ivh rabbitmq-server-3.8.8-1.el7.noarch.rpm
4.常用命令(按照以下顺序执行)
#添加开机启动 RabbitMQ 服务
chkconfig rabbitmq-server on
#启动服务
/sbin/service rabbitmq-server start
#查看服务状态
/sbin/service rabbitmq-server status
#停止服务(选择执行)
/sbin/service rabbitmq-server stop
#关闭虚拟机上的防火墙【这步只适合学习期间】
systemctl stop firewalld.service #一般输入 firew然后按Tab就出来了
#开启 web 管理插件
rabbitmq-plugins enable rabbitmq_management
用默认账号密码(guest)访问地址 http://47.115.185.244:15672/出现权限问题
入门案例:
1、导入依赖:
<!--rabbitmq 依赖客户端-->
<dependency>
<groupId>com.rabbitmq</groupId>
<artifactId>amqp-client</artifactId>
<version>5.8.0</version>
</dependency>
<!--操作文件流的一个依赖-->
<dependency>
<groupId>commons-io</groupId>
<artifactId>commons-io</artifactId>
<version>2.6</version>
</dependency>
2、生产者:【发布任务的,快递员】发布给RabbitMQ
@SpringBootTest
@RunWith(SpringRunner.class)
public class MyProducerTest {
private static final String QUEUE_NAME = "hello";
public void test() throws IOException, TimeoutException {
//建立RabbitMQ连接
Connection rbCon = getConnection();
//根据连接获取发消息的信道,信道才是发消息的
Channel channel = rbCon.createChannel();
/**
* 生成一个队列 zhong参数详讲:
* 1.队列名称
* 2.队列中的消息是否持久化,持久化就到了磁盘中,反之在内存中
* 3.该队列是否提供多个消费者消费,是否支持数据共享,true可多个消费者消费,false只允许一个消费者消费
* 4.最后一个消费者断开连接以后,是否自动删除,true:删除 false:不删除
*/
channel.queueDeclare(QUEUE_NAME, false, false, false, null);
String message = "Hello World!";
/**
* 发送消息:
* 1.发送到哪个交换机 不填
* 2.路由的key值是哪个 本次是队列名称
* 3.其他参数信息
* 4.发送信息的信息体
*/
channel.basicPublish("", QUEUE_NAME, null, message.getBytes());
System.out.println(" [x] Sent '" + message + "'");
//关闭连接和通道
rbCon.close();
channel.close();
}
//根据工厂创建连接
public static Connection getConnection(){
//发消息=====>>>> 创建一个连接工厂
ConnectionFactory con = new ConnectionFactory();
//工厂IP 连接RabbitMQ系列
con.setHost("192.168.59.130");
con.setPort(5672);
con.setUsername("admin");
con.setPassword("admin");
try {
return con.newConnection();
} catch (IOException e) {
e.printStackTrace();
} catch (TimeoutException e) {
e.printStackTrace();
}
return null;
}
}
3、消费者:专门获取生产发来的消息、将任务完成
@SpringBootTest
@RunWith(SpringRunner.class)
public class MyConsumerTest {
private final String QUEUE_NAME = "hello";
@Test
public void testPubliser() throws IOException, TimeoutException {
Connection con = getConnection();
Channel channel = con.createChannel();
channel.queueDeclare(QUEUE_NAME, false, false, false, null);
System.out.println(" [*] Waiting for messages. To exit press CTRL+C");
//声明接收消息回调对象,省略了一个取消接收消息的回调对象
DeliverCallback deliverCallback = (consumerTag, delivery) -> {
String message = new String(delivery.getBody(), "UTF-8" );
System.out.println( " [x] 收到'" + message + "'" );
};
channel.basicConsume(QUEUE_NAME, true , deliverCallback, consumerTag -> { });
con.close();
channel.close();
}
//根据工厂创建连接
public static Connection getConnection(){
//发消息=====>>>> 创建一个连接工厂
ConnectionFactory con = new ConnectionFactory();
//工厂IP 连接RabbitMQ系列
con.setHost("192.168.59.130");
con.setPort(5672);
con.setUsername("admin");
con.setPassword("admin");
try {
return con.newConnection();
} catch (IOException e) {
e.printStackTrace();
} catch (TimeoutException e) {
e.printStackTrace();
}
return null;
}
}
使用WorkQueue模型:
- 使用Spring AMQP接入
- 在RabbitMQ中Queues手动添加队列
使用 prefetch 消费队列的能力,给予消费者分发消息数量
listener:
simple:
prefetch: 1 #设置为WorkQueue模式:每次只能获取一条消息,处理完成才能获取下一个消息
1、导入依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
2、生产者【发布任务】
@SpringBootTest
@RunWith(SpringRunner.class)
public class SpringAmqpTest {
@Autowired
private RabbitTemplate rabbitTemplate;
@Test
public void test01() {
rabbitTemplate.convertAndSend("sim", "hello,spring.amqp");
}
@Test
public void test02() throws InterruptedException {
for (int i = 0; i < 50; i++) {
rabbitTemplate.convertAndSend("simple.hh", "hello,spring.amqp" + i);
Thread.sleep(20);
}
}
}
3、消息队列
@Component
public class SpringRabbitmqListener {
@RabbitListener(queues = "simple.hh")
public void rabbitListener01(String msg) throws InterruptedException{
System.out.println("rabbitListener01...... 消息者收到消息:【" + msg + "】"+ LocalTime.now());
Thread.sleep(20);
}
@RabbitListener(queues = "simple.hh")
public void rabbitListener02(String msg) throws InterruptedException{
System.err.println("rabbitListener02...... 消息者收到消息:【" + msg + "】"+ LocalTime.now());
Thread.sleep(200);
}
}
交换机【Exchange】:
偏出交换机:Fanout
根据指定队列绑定指定的交换机就可以让消费者进行消费
配置队列绑定交换机:【交换机类似于队列的大哥,生产者信息需要经过交换机才可以分发给队列】:【这种方式复杂不建议】
@Configuration
public class FanoutConfig {
//itcast.fanout 交换机名 后端界面exchange中可以看到自己定义的交换机
@Bean
public FanoutExchange fanoutExchange(){
return new FanoutExchange("itcast.fanout");
}
//fanout.queue1 队列1
@Bean
public Queue fanoutQueue1(){
return new Queue("fanout.queue1");
}
//绑定队列1到交换机 参数名必须一致
@Bean
public Binding fanoutBinding1(Queue fanoutQueue1,FanoutExchange fanoutExchange){
return BindingBuilder.bind(fanoutQueue1).to(fanoutExchange);
}
//fanout.queue2 与上面一致
@Bean
public Queue fanoutQueue2(){
return new Queue("fanout.queue2");
}
//绑定队列2到交换机
@Bean
public Binding fanoutBinding2(Queue fanoutQueue2,FanoutExchange fanoutExchange){
return BindingBuilder.bind(fanoutQueue2).to(fanoutExchange);
}
}
1、监听绑定交换机的队列
@RabbitListener(queues = "fanout.queue1")
public void fanoutQueue1(String msg) throws InterruptedException{
System.err.println("消息者收到 fanoutqueue1......的消息:【" + msg + "】"+ LocalTime.now());
Thread.sleep(100);
}
@RabbitListener(queues = "fanout.queue2")
public void fanoutQueue2(String msg) throws InterruptedException{
System.err.println("消息者收到 fanoutqueue2......的消息:【" + msg + "】"+ LocalTime.now());
Thread.sleep(100);
}
2、生产者发送消息
@Test
public void test03(){
rabbitTemplate.convertAndSend("itcast.fanout","","hello,every one");
}
直接交换机:Direct
根据指定routing key和交换机就可以让消费者消费
1、根据Direct交换机 编写消费者:
@RabbitListener(bindings = @QueueBinding(
value = @Queue(name = "direct.queue1"),
exchange = @Exchange(name = "itcast.direct",type = ExchangeTypes.DIRECT),
key = {"red","yellow"}
))
public void listenerDirectQueue1(String msg) throws InterruptedException{
System.err.println("消息者收到 fanoutqueue2......的消息:【" + msg + "】"+ LocalTime.now());
Thread.sleep(100);
}
@RabbitListener(bindings = @QueueBinding(
value = @Queue("direct.queue2"), //队列名称
exchange = @Exchange(value = "itcast.direct",type = ExchangeTypes.DIRECT), //交换机
key = {"green","pink"} //对应的routing ky可以通过这个值来访问
))
public void listenerDirectQueue2(String msg) throws InterruptedException{
System.err.println("消息者收到 listenerDirectQueue2......的消息:【" + msg + "】"+LocalTime.now());
Thread.sleep(100);
}
2.生产者:重要在于routing key
@Test
public void test04(){
rabbitTemplate.convertAndSend("itcast.direct","red","hello,pink");
}
TopicExchange交换机:【Topic和Direct一致,topic只是支持通配符#】
1、消费者:
@RabbitListener(bindings = @QueueBinding(
value = @Queue("topic.queue1"),
exchange = @Exchange(value = "itcast.topic",type = ExchangeTypes.TOPIC),
key = "china.#"
))
public void listenerTopicQueue1(String msg) throws InterruptedException{
System.err.println("消息者收到 listenerTopicQueue1......的消息:【" + msg + "】"+ LocalTime.now());
Thread.sleep(100);
}
@RabbitListener(bindings = @QueueBinding(
value = @Queue("topic.queue1"),
exchange = @Exchange(value = "itcast.topic",type = ExchangeTypes.TOPIC),
key = "#.news"
))
public void listenerTopicQueue2(String msg) throws InterruptedException{
System.err.println("消息者收到 listenerTopicQueue2......的消息:【" + msg + "】"+ LocalTime.now());
Thread.sleep(100);
}
2、生产者:
@Test
public void test05(){
rabbitTemplate.convertAndSend("itcast.topic","chinese.news","中国人民最牛");
}
Spring AMQP是支持发送对象的,而发送对象后变成JDK字节【重点】,需要使用Jackson2序列化
1、消费者
changeTypes.TOPIC),
key = "#.news"
))
public void listenerTopicQueue2(String msg) throws InterruptedException{
System.err.println("消息者收到 listenerTopicQueue2......的消息:【" + msg + "】"+ LocalTime.now());
Thread.sleep(100);
}
2、生产者:
@Test
public void test05(){
rabbitTemplate.convertAndSend("itcast.topic","chinese.news","中国人民最牛");
}
Spring AMQP是支持发送对象的,而发送对象后变成JDK字节【重点】,需要使用Jackson2序列化
声明式队列消息过期时间:
1、编写交换机和队列
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 RabbitConfig {
public static final String JAVABOY_MESSAGE_DELAY_QUEUE_NAME2 = "javaboy_message_delay_queue_name2";
public static final String JAVABOY_MESSAGE_DELAY_EXCHANGE_NAME = "javaboy_message_delay_exchange_name";
@Bean
DirectExchange directExchange() {
return new DirectExchange(JAVABOY_MESSAGE_DELAY_EXCHANGE_NAME,true,false);
}
@Bean
Queue messageDelayQueue2(){
Map<String,Object> args = new HashMap<>();
//设置队列的过期时间
args.put("x-message-ttl",10000);
return new Queue(JAVABOY_MESSAGE_DELAY_QUEUE_NAME2,true,false,false,args);
}
@Bean
Binding directBinding2() {
return BindingBuilder.bind(messageDelayQueue2())
.to(directExchange()).with(JAVABOY_MESSAGE_DELAY_QUEUE_NAME2);
}
}
2、编写生产者
@Controller
public class HelloController {
@Resource
private RabbitTemplate rabbitTemplate;
/**
* 设置队列中消息指定时间消失
*/
@GetMapping("/send2")
public void hello2(){
Message msg = MessageBuilder
.withBody("Hello 黄豪杰该消息设置的是消息队列失效".getBytes()).build();
rabbitTemplate.convertAndSend(RabbitConfig.JAVABOY_MESSAGE_DELAY_EXCHANGE_NAME,
RabbitConfig.JAVABOY_MESSAGE_DELAY_QUEUE_NAME2,msg);
}
}
直接在生产者发送消息时指定形式设置消息过期:
@RestController
public class HelloController {
@Resource
private RabbitTemplate rabbitTemplate;
/**
* 设置队列中在指定时间内消失
*/
@GetMapping("/send")
public void hello(){
//设置消息过期为10s 到达RabbitMQ 10秒没人消费消息直接消失
Message message = MessageBuilder
.withBody("hello,mr.huang给单条消息设置过期时间".getBytes(StandardCharsets.UTF_8))
.setExpiration("10000").build();
rabbitTemplate.send(RabbitMqConfig.JAVABOY_MESSAGE_DELAY_EXCHANGE_NAME,
RabbitMqConfig.JAVABOY_MESSAGE_DELAY_QUEUE_NAME,message);
}
}
说明:当队列中消息指定时间没有被消费,消息会自动删除,这是时候消息会去哪里呢?下面就引来了死信队列,作用非常大:比如用户下单在指定时间内没有支付,这是需要死信队列取消订单等等 www.baidu.com 自行百度
死信队列:
- 绑定死信交换机的队列,就是死信队列
- 其实死信队列和死信交换机与我们上面写的队列交换机本质上是没有区别的,只是概念上的不同而已 ,即死信队列、死信交换机,实际上就是普通的队列、交换机罢了。
1、导pom
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.amqp</groupId>
<artifactId>spring-rabbit-test</artifactId>
</dependency>
</dependencies>
2、该application.yaml
spring:
rabbitmq:
host: 192.168.220.129
username: admin
password: admin
port: 5672
3、编写交换机执行任务的交换机
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;
import java.util.HashMap;
import java.util.Map;
/**
* @author mr.huang [jie88888822@163.com]
* @devTime 2022
* 普通交换机和队列:设置过期时间、指定死信交换机、指定死信队列即可 最后由死信队列消费
*/
@Configuration
public class RabbitConfig {
public static final String MY_DIRECT_EXCHANGE_NAME = "my_direct_exchange_name";
public static final String MY_DIRECT_QUEUE_NAME_01 = "my_direct_queue_name_01";
@Bean
Queue queue(){
Map<String,Object> args = new HashMap<>();
//设置消息有效期,消息到期未被消费,就立马进入到死信交换机,并由死信交换机路由到死信队列
//这里如果改时间需要删除 rabbitmq中的交换机和队列
//参数为:0 生产消息后必须立马消费
args.put("x-message-ttl",10000);
//指定死信交换机
args.put("x-dead-letter-exchange",RabbitDlxConfig.MY_DLX_DIRECT_EXCHANGE_NAME);
//指定死信队列
args.put("x-dead-letter-routing-key",RabbitDlxConfig.MY_DLX_DIRECT_QUEUE_NAME_01);
return new Queue(MY_DIRECT_QUEUE_NAME_01,true,false,false,args);
}
//使用的直接模式交换机 根据routingKey去配对队列,消费者根据交换机队列和routingKey去消费消息
@Bean
DirectExchange directExchange(){
return new DirectExchange(MY_DIRECT_EXCHANGE_NAME,true,false);
}
//让交换机和队列进行绑定关系
@Bean
Binding directBinding(){
return BindingBuilder
.bind(queue()).to(directExchange()).with(MY_DIRECT_QUEUE_NAME_01);
}
}
3、编写死信队列:死信队列就是一个普通的队列,只是一个名词而已
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;
/**
* @author mr.huang [jie88888822@163.com]
* @devTime 2022
* 死信交换机和死信队列:就是普通的交换机和队列,只是用于处理过期数据
*/
@Configuration
public class RabbitDlxConfig {
//死信交换机名字
public static final String MY_DLX_DIRECT_EXCHANGE_NAME = "my_dlx_direct_exchange_name";
//死信队列名字
public static final String MY_DLX_DIRECT_QUEUE_NAME_01 = "my_dlx_direct_queue_name_01";
@Bean
Binding dlxBinding(){
return BindingBuilder
.bind(dlxQueue())
.to(dlxDirectExchange()).with(MY_DLX_DIRECT_QUEUE_NAME_01);
}
/**
* 死信队列
* @return
*/
@Bean
Queue dlxQueue() {
return new Queue(MY_DLX_DIRECT_QUEUE_NAME_01, true, false, false);
}
/**
* 死信交换机
* @return
*/
@Bean
DirectExchange dlxDirectExchange() {
return new DirectExchange(MY_DLX_DIRECT_EXCHANGE_NAME, true, false);
}
}
4、生产者:
@GetMapping("/send")
public void hello(){
rabbitTemplate.convertAndSend(RabbitConfig.MY_DIRECT_EXCHANGE_NAME,
RabbitConfig.MY_DIRECT_QUEUE_NAME_01,"hello 黄豪杰,该消息如果过期会跑到死信队列");
}
5、消费者:
@RabbitListener(queues = RabbitDlxConfig.MY_DLX_DIRECT_QUEUE_NAME_01)
public void msg(Message message){
log.info("死信队列的消息消费成功:{}",message.getPayload());
}
延迟队列
延迟队列就是给予给队列设置延迟时间,一般使用插件完成。 退一万步讲自己去github下载完
https://github.com/rabbitmq/rabbitmq-delayed-message-exchange/releases/tag/3.8.9 注意这个插件要根据RabbitMQ版本来确认
RabbitMQ 发送可靠性问题:
重要问题:
1、确保生产者消息传到了RabbitMQ
2、确保消费者成功消费到消息
解决方式一: 开启事务模式
1、编写Config
@Configuration
public class RabbitConfig {
public static final String JAVABOY_MSG_QUEUE_NAME = "javaboy_msg_queue_name";
public static final String JAVABOY_MSG_EXCHANGE_NAME = "javaboy_msg_exchange_name";
@Bean
Queue msgQueue(){
return new Queue(JAVABOY_MSG_QUEUE_NAME,true,false,false);
}
@Bean
DirectExchange directExchange() {
return new DirectExchange(JAVABOY_MSG_EXCHANGE_NAME,true,false);
}
@Bean
Binding directBinding(){
return BindingBuilder.bind(msgQueue())
.to(directExchange()).with(JAVABOY_MSG_QUEUE_NAME);
}
}
/**
* @author mr.huang [jie88888822@163.com]
* @devTime 2022-08-08
* Rabbit事务管理器
*/
@Configuration
public class TxConfig {
//创建RabbitMQ事务管理器
@Bean
RabbitTransactionManager rabbitTransactionManager(ConnectionFactory connectionFactory){
return new RabbitTransactionManager(connectionFactory);
}
@Bean
RabbitTemplate rabbitTemplate(ConnectionFactory connectionFactory){
RabbitTemplate rabbitTemplate = new RabbitTemplate(connectionFactory);
//手动创建RabbitMQ 用于开启信道事务
rabbitTemplate.setChannelTransacted(true);
return rabbitTemplate;
}
}
2、生产者
@Service
public class MsgService {
@Resource
private RabbitTemplate rabbitTemplate;
@Transactional
public void sendTansaction() {
rabbitTemplate .convertAndSend(RabbitConfig.JAVABOY_MSG_EXCHANGE_NAME,RabbitConfig.JAVABOY_MSG_QUEUE_NAME,"hello,黄豪杰"); int i = 1 / 0;
}
}
@RestController
public class HelloController {
@Resource
private MsgService msgService;
@GetMapping("/send")
public void Hello(){
msgService.sendTansaction();
}
}
这里注意两点:
1. 发送消息的方法上添加 `@Transactional` 注解标记事务。
2. 调用 setChannelTransacted 方法设置为 true 开启事务模式。
当我们开启事务模式之后,RabbitMQ 生产者发送消息会多出四个步骤:
1. 客户端发出请求,将信道设置为事务模式。
2. 服务端给出回复,同意将信道设置为事务模式。
3. 客户端发送消息。
4. 客户端提交事务。
5. 服务端给出响应,确认事务提交。
解决方式二:发送方确认机制( publisher-confirm )
RabbitMQ 还提供了发送方确认机制(publisher confirm)来确保消息发送成功,这种方式,性能要远远高于事务模式,一起来看下。
1、编写application.properties 开启消息发送方确认机制
spring.rabbitmq.publisher-confirm-type=correlated
spring.rabbitmq.publisher-returns=true
2、 编写RabbitMQ消息发送方确认机制
@Slf4j
@Configuration
public class RabbitConfig implements RabbitTemplate.ConfirmCallback,RabbitTemplate.ReturnsCallback {
public static final String JAVABOY_EXCHANGE_NAME = "javaboy_exchange_name";
public static final String JAVABOY_QUEUE_NAME = "javaboy_queue_name";
@Resource
private RabbitTemplate rabbitTemplate;
@Bean
DirectExchange directExchange() {
return new DirectExchange(JAVABOY_EXCHANGE_NAME,true,false);
}
@Bean
Queue directQueue(){
return new Queue(JAVABOY_QUEUE_NAME,true,false,false);
}
@Bean
Binding directBinding(){
return BindingBuilder.bind(directQueue())
.to(directExchange()).with(JAVABOY_QUEUE_NAME);
}
@PostConstruct
public void initRabbitTemplate(){
rabbitTemplate.setConfirmCallback(this);
rabbitTemplate.setReturnsCallback(this);
}
/**
* 消息成功到达交换机,会触发该方法
* @param correlationData
* @param ack 消息确认机制
* @param cause 原因
*/
@Override
public void confirm(CorrelationData correlationData, boolean ack, String cause) {
if(ack){
log.info("{}:消息成功到达交换机",correlationData.getId());
}else{
log.error("{}:消息发送失败", correlationData.getId());
}
}
/**
* 消息未成功到达队列,会触发该方法
* @param returned
*/
@Override
public void returnedMessage(ReturnedMessage returned) {
log.error("{}:消息未成功路由到队列",returned.getMessage().getMessageProperties().getMessageId());
}
}
3、生产者:
@RestController
public class HelloController {
@Resource
private RabbitTemplate rabbitTemplate;
@GetMapping("/send")
public void send(){
//这里交换机故意写错的,所有消息不会到达交换机这时回触发 confirm
//"{}:消息发送失败", correlationData.getId()
rabbitTemplate.convertAndSend("RabbitConfig.JAVABOY_EXCHANGE_NAME"
,RabbitConfig.JAVABOY_QUEUE_NAME,"Hello,Mr.Huang",
new CorrelationData(UUID.randomUUID().toString()));
}
}
发送方的确认机制实现对消息发送到RabbitMQ的状态的一个回调机制,通过交换机回调的ACK的确认机制判断是否消息是否发送successfuly
失败重试分两种情况,一种是压根没找到 MQ 导致的失败重试,另一种是找到 MQ 了,但是消息发送失败了。
两种重试我们分别来看。
自带重试机制 (Spring中的Retry来重试)当RabbitMQ宕机用于重试发送消息给RabbitMQ
1、配置application.yaml
spring:
rabbitmq:
host: 192.168.220.129
username: admin
password: admin
port: 5672
#配置pulisher confirm 发送方确认机制
#配置消息到达交换器的确认回调
publisher-confirm-type: correlated
#配置消息到达队列的回调
publisher-returns: true
#配置retry 重试机制
template:
retry:
#开启重试机制
enabled: true
#重试起始间隔时间
initial-interval: 1000ms
#最大重试次数
max-attempts: 10
#最大重试间隔时间
max-interval: 10000ms
#间隔时间乘数。(这里配置间隔时间乘数为 2,则第一次间隔时间 1 秒,第二次重试间隔时间 2 秒,第三次 4 秒,以此类推)x
multiplier: 2
RabbitMQ 消息消费的可靠性问题:
两种消费思路
RabbitMQ 的消息消费,整体上来说有两种不同的思路:
- 推(push):MQ 主动将消息推送给消费者,这种方式需要消费者设置一个缓冲区去缓存消息,对于消费者而言,内存中总是有一堆需要处理的消息,所以这种方式的效率比较高,这也是目前大多数应用采用的消费方式。
@RabbitListener(queues = RabbitConfig.QUEUE_NAME)
public void handleMsg2(Message message,Channel channel) throws IOException {
//log.info("消费消息成功MsgReceiver2:{}",msg);
//拒接消费该消息,requeue 表示被拒绝的消息是否重新进入队列中
channel.basicReject((Long)message.getHeaders().get(AmqpHeaders.DELIVERY_TAG),true);
}
- 拉(pull):消费者主动从 MQ 拉取消息,这种方式效率并不是很高,不过有的时候如果服务端需要批量拉取消息,倒是可以采用这种方式。
@SpringBootTest
class ConfirmApplicationTest {
@Resource
private RabbitTemplate rabbitTemplate;
@Test
void test01(){
//拉模式:消费消息
Object o = rabbitTemplate.receiveAndConvert(RabbitConfig.JAVABOY_QUEUE_NAME);
System.out.println("o = " + o);
}
}
确保消费成功两种思路
1、自动ACK: 当 autoAck 为 true
2、手动ACK: 当 autoAck 为 false
最终都有可能导致消息被重复消费,所以一般来说我们还需要在处理消息时,解决幂等性问题
一、推模式手动确认(MQ将消息主动的推到消费者去消费)
1、写application.yaml
spring:
rabbitmq:
port: 5672
username: admin
password: admin
host: 192.168.220.129
#要开启ACK手动确认机制,确保消息消费的一个可靠性
listener:
simple:
acknowledge-mode: manual
2、配置交换机和队列
@Configuration
public class RabbitConfig {
public static final String JAVABOY_MSG_QUEUE_NAME = "javaboy_msg_queue_name";
public static final String JAVABOY_MSG_EXCHANGE_NAME = "javaboy_msg_exchange_name";
@Bean
Queue directQueue(){
return new Queue(JAVABOY_MSG_QUEUE_NAME,true,false,false);
}
@Bean
DirectExchange directExchange(){
return new DirectExchange(JAVABOY_MSG_EXCHANGE_NAME,true,false);
}
@Bean
Binding directBinding(){
return BindingBuilder.bind(directQueue())
.to(directExchange()).with(JAVABOY_MSG_EXCHANGE_NAME);
}
}
3、编写生产者
@RestController
public class HellController {
@Resource
private RabbitTemplate rabbitTemplate;
@GetMapping("/send")
public void send(){
rabbitTemplate.convertAndSend(RabbitConfig.JAVABOY_MSG_EXCHANGE_NAME,
RabbitConfig.JAVABOY_MSG_QUEUE_NAME,"hello,mr.haung you open false auto ack");
}
}
4、编写ACK确认消费放
import com.huang.config.RabbitConfig;
import com.rabbitmq.client.Channel;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Component;
import java.io.IOException;
@Component
public class MsgReceiver {
@RabbitListener(queues = RabbitConfig.JAVABOY_MSG_QUEUE_NAME)
public void msgHandler(Message message, Channel channel){
//消息的标记
long deliveryTag = message.getMessageProperties().getDeliveryTag();
try {
//拿到消息
String msg = new String(message.getBody());
System.out.println("msg = " + msg);
int kk = 1 /0;
//如果中间业务没有异常,这ack确认消费成功 false表示消息消费成功,否则表消息丢失进入死信
channel.basicAck(deliveryTag, false);
} catch (Exception e) {
//消费消息失败手动 requeue的true表示重新进入队列再次等待消费
try {
//第三个参数为 true会死循环消费,设为false表示丢失消失,交给死信队列消费
channel.basicNack(deliveryTag, false,false);
} catch (IOException ioException) {
ioException.printStackTrace();
}
}
}
}
二、拉模式手动确认(需要自己手动拉取消息,进行消费)
这里省略了,因为这种方式用的不多,而且只能单条消息的拉
最后我们再来说说消息的幂等性问题。
消费消息幂等性问题(防止重复消费)
大家设想下面一个场景 :
消费者在消费完一条消息后,向 RabbitMQ 发送一个 ack 确认,此时由于网络断开或者其他原因导致 RabbitMQ 并没有收到这个 ack,那么此时 RabbitMQ 并不会将该条消息删除,当重新建立起连接后,消费者还是会再次收到该条消息,这就造成了消息的重复消费。同时,由于类似的原因,消息在发送的时候,同一条消息也可能会发送两次(参见四种策略确保 RabbitMQ 消息发送可靠性!你用哪种?)。种种原因导致我们在消费消息时,一定要处理好幂等性问题。
幂等性问题的处理倒也不难,基本上都是从业务上来处理,我来大概说说思路
采用 Redis,在消费者消费消息之前,现将消息的 id 放到 Redis 中,存储方式如下:
- id-0(正在执行业务)
- id-1(执行业务成功)
如果 ack 失败,在 RabbitMQ 将消息交给其他的消费者时,先执行 setnx,如果 key 已经存在(说明之前有人消费过该消息),获取他的值,如果是 0,当前消费者就什么都不做,如果是 1,直接 ack。
极端情况:第一个消费者在执行业务时,出现了死锁,在 setnx 的基础上,再给 key 设置一个生存时间。生产者,发送消息时,指定 messageId。