RabbitMq
<!--rabbitmq的依赖-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
properties文件定义配置
配置文件的参考
server.port=8089
#======================rabbitmq配置================
spring.rabbitmq.host=192.168.1.105
spring.rabbitmq.port=5672
spring.rabbitmq.username=mqRabbit
spring.rabbitmq.password=123456
spring.rabbitmq.virtual-host=/mqRabbit
#消费者重试
spring.rabbitmq.listener.simple.retry.enabled=true
#最大重试次数
spring.rabbitmq.listener.simple.retry.max-attempts=5
#重试间隔时间
spring.rabbitmq.listener.simple.retry.initial-interval=3000
#手动ack
spring.rabbitmq.listener.simple.acknowledge-mode=manual
#生产者消息确认
#spring.rabbitmq.publisher-confirms=true
#spring.rabbitmq.template.mandatory=true
#============================数据库的配置============================
spring.datasource.url=jdbc:mysql://127.0.0.1:3306/redis_learning?useUnicode=true&useJDBCCompliantTimezoneShift=true&useLegacyDatetimeCode=false&serverTimezone=UTC
spring.datasource.username=root
spring.datasource.password=root
spring.datasource.driver-class-name=com.mysql.jdbc.Driver
spring.datasource.type=com.alibaba.druid.pool.DruidDataSource
#=============================MyBaties========================
# mybatis 下划线转驼峰配置,两者都可以
#mybatis.configuration.mapUnderscoreToCamelCase=true
mybatis.configuration.map-underscore-to-camel-case=true
#增加打印sql语句句,⼀一般⽤用于本地开发测试
mybatis.configuration.log-impl=org.apache.ibatis.logging.stdout.StdOutImpl
#配置xml文件
mybatis.mapper-locations=classpath:mapper/*.xml
RabbitMq的java配置
定义不同的Queue,Exchange,bindingExchangeSms 就是可以实现多个不同消息队列搭配
package com.roy.config;
import org.springframework.amqp.core.*;
import org.springframework.amqp.rabbit.config.SimpleRabbitListenerContainerFactory;
import org.springframework.amqp.rabbit.connection.ConnectionFactory;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.autoconfigure.amqp.SimpleRabbitListenerContainerFactoryConfigurer;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* @Authror royLuo
* @Date 2020/4/28 22:51
**/
@Configuration
public class RabbitConfig {
//sms队列
public static final String SMSQUEUE = "sms_queue";
public static final String SMSEXCHANGE = "sms_exchange";
/**
* Queue队列
**/
@Bean
public Queue smsQueue() {
/**
* 1.durable: 队列是否持久化;队列是否可持久化
*
* 2.exclusive: 是否独享、排外的;
*
* 3.autoDelete: 是否自动删除;
* 队列没有任何订阅的消费者时是否自动删除,默认为false。可以通过RabbitMQ Management,查看某个队列的消费者数量。如果为true,当consumers = 0时队列就会自动删除。
* **/
return new Queue(SMSQUEUE, true, false, true);
}
/**
* Exchange
* 交换机
**/
@Bean
public Exchange smsExchange() {
//ExchangeTypes选择交换机的类型。现在选择的是广播模式
return new ExchangeBuilder(SMSEXCHANGE, ExchangeTypes.FANOUT).durable(true).build();
}
@Bean
public Binding bindingExchangeSms(@Qualifier("smsQueue") Queue smsQueue, @Qualifier("smsExchange") Exchange smsExchange) {
//这里使用的是广播模式,所以是没有routingKey。但是也要定义为空字符串
return BindingBuilder.bind(smsQueue).to(smsExchange).with("").noargs();
}
/***
* 代码中使用@RabbitListener注解指定消费方法,默认情况是单线程监听队列,可以观察当队列有多个任务时消费
* 端每次只消费一个消息,单线程处理消息容易引起消息处理缓慢,消息堆积,不能最大利用硬件资源。
* 可以配置mq的容器工厂参数,增加并发处理数量即可实现多线程处理监听队列,实现多线程处理消息。
* **/
//消费者并发数量
public static final int DEFAULT_CONCURRENT = 10;
@Bean("customContainerFactory")
public SimpleRabbitListenerContainerFactory
containerFactory(SimpleRabbitListenerContainerFactoryConfigurer configurer, ConnectionFactory
connectionFactory) {
SimpleRabbitListenerContainerFactory factory = new SimpleRabbitListenerContainerFactory();
factory.setConcurrentConsumers(DEFAULT_CONCURRENT);
factory.setMaxConcurrentConsumers(DEFAULT_CONCURRENT);
configurer.configure(factory, connectionFactory);
return factory;
}
}
消息的发送
package com.roy.service;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.core.MessageBuilder;
import org.springframework.amqp.core.MessageProperties;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.UUID;
/**
* @Authror royLuo
* @Date 2020/4/28 23:01
**/
@Service
public class SendMsgService {
//sms交换机
public static final String SMSEXCHANGE = "sms_exchange";
@Autowired
private RabbitTemplate rabbitTemplate;
public String sendMsg(String msg){
//方式一:直接把msg发送
// rabbitTemplate.convertAndSend(SMSEXCHANGE,"",msg);
//方式二:构建消息
Message message = MessageBuilder
.withBody(msg.getBytes())
.setContentType(MessageProperties.CONTENT_TYPE_JSON)
.setContentEncoding("utf-8")
.setMessageId(UUID.randomUUID() + "").build();
rabbitTemplate.convertAndSend(SMSEXCHANGE,"",message);
return "success";
}
}
消息的消费方
package com.roy.service;
import com.rabbitmq.client.Channel;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.amqp.support.AmqpHeaders;
import org.springframework.messaging.handler.annotation.Headers;
import org.springframework.stereotype.Service;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.Map;
/**
* 消费者端使用@RabbitListener注解实现监听消息
* 1. @RabbitListener 实现原理
* 底层使用AOP进行拦截,如果程序没有抛出异常,自动提交事务
* 如果AOP使用异常通知拦截获取异常信息的话,自动实现补偿机制,该消息会缓存到rabbitmq服务器进行存放,
* 一直重试到不抛出异常为准
* <p>
* 2.修改重试机制策略 一般默认情况下 间隔5秒重试一次
* <p>
* 3.重试机制
* 3.1如何合适选择重试机制
* 3.1.1.消费者获取消息,调用第三方接口,但接口暂时无法访问,需要重试
* 流程:1.1http请求第三方接口,通过判断响应状态。在消费方抛出异常重试
* <p>
* 3.1.2.消费者获取到消息后,抛出数据转换异常,不需要重试
* 流程: 2.捕获异常,记录日志信息。人工接口补偿
* 对于2,我们应该采用日志记录+定时任务job健康检查+人工进行补偿
*
* @Authror royLuo
* @Date 2020/4/28 23:26
**/
@Service
public class CustomerMsgService {
public static final String SMSQUEUE = "sms_queue";
/**
* 并发监听发来的消息
* containerFactory="customContainerFactory"
**/
// @RabbitListener(queues = SMSQUEUE, containerFactory = "customContainerFactory")
/**
* 默认单线程
* 这里使用字段串msg的形式获取消息
* *
**/
@RabbitListener(queues = SMSQUEUE)
public void listerNing1(String msg, @Headers Map<String, Object> heards, Channel channel) {
Thread currentThread = Thread.currentThread();
String name = currentThread.getName();
System.out.println("name:============>>" + name + msg);
/**
* 当消费者这边报异常将不停重新执行代码
int i = 1 / 0;
* **/
//实现手动ack
try {
//手动ack,这里使用手动ack的同时,配置文件也是修改的
Long deliverTag = (Long) heards.get(AmqpHeaders.DELIVERY_TAG);
//手动签收
channel.basicAck(deliverTag, false);
} catch (IOException e) {
e.printStackTrace();
}
}
/**
* 这里使用Message的形式获取消息
**/
@RabbitListener(queues = SMSQUEUE)
public void listerNing2(Message msg, @Headers Map<String, Object> heards, Channel channel) {
try {
//获取消息内容
String mssage = new String(msg.getBody(), StandardCharsets.UTF_8);
//获取消息的id
String messageId = msg.getMessageProperties().getMessageId();
System.out.println("msg:============>>" + mssage);
System.out.println("messageId:============>>" + messageId);
/**
* 当消费者这边报异常将不停重新执行代码
int i = 1 / 0;
* **/
//手动ack,这里使用手动ack的同时,配置文件也是修改的
Long deliverTag = (Long) heards.get(AmqpHeaders.DELIVERY_TAG);
//手动签收
channel.basicAck(deliverTag, false);
} catch (IOException e) {
e.printStackTrace();
}
}
}
死信队列
什么是死信呢?什么样的消息会变成死信呢?
消息被拒绝(basic.reject或basic.nack)并且requeue=false.
消息TTL过期
队列达到最大长度(队列满了,无法再添加数据到mq中)
伪代码
1.定义一个死信队列
2.定义一个死信交换机
3.绑定死信队列和死信交换机,定义路由键。
当你需要给短信队列添加死信队列的做法:
1.在定义短信队列的时侯需要这样定义:
public Queue fanOutEamilQueue(){
//邮件队列绑定死信队列
Map<String,Object>args = new HashMap<>(2);
args.put(死信队列的标识符,死信交换机);
args.put(死信路由键的标识符,死信路由键);
Queue queue = new Queue(短信队列名,true,false,false,args);
return queue;
}
2.在消费者的代码方法的参数列表Message message, @Headers Map<String, Object> headers, Channel channel里使用
丢弃该消息
channel.basicNack(message.getMessageProperties().getDeliveryTag(),false,false);
3.保证绑定死信队列的队列原本是不存在的
RabbitMQ解决分布式事务原理
实现的思路:
RabbitMQ解决分布式事务原理: 采用最终一致性原理。
需要保证以下三要素
1、确认生产者一定要将数据投递到MQ服务器中(采用MQ消息确认机制)
配置文件
#生产者消息确认
#spring.rabbitmq.publisher-confirms=true
#spring.rabbitmq.template.mandatory=true
生产者实现接口
implements RabbitTemplate.ConfirmCallback
//生产代码发送消息代理添加代码
//构建回调返回的数据
CorrelationData correlationData = new CorrelationData(orderId);
// 发送消息
this.rabbitTemplate.setMandatory(true);
this.rabbitTemplate.setConfirmCallback(this);
// 生产消息确认机制
@Override
public void confirm(CorrelationData correlationData, boolean ack, String cause) {
String orderId = correlationData.getId();
System.out.println("消息id:" + correlationData.getId());
if (ack) {
System.out.println("消息发送确认成功");
} else {
send(orderId);
System.out.println("消息发送确认失败:" + cause);
}
}
2、MQ消费者消息能够正确消费消息,采用手动ACK模式(注意重试幂等性问题)
// 手动签收
channel.basicAck(deliveryTag, false);
3、如何保证第一个事务先执行,采用补偿机制,在创建一个补单消费者进行监听,如果订单没有创建成功,进行补单。